Class: Zephyr

Inherits:
Object
  • Object
show all
Defined in:
lib/zephyr.rb,
lib/zephyr/failed_request.rb

Overview

A simple front-end for doing HTTP requests quickly and simply.

Defined Under Namespace

Classes: FailedRequest

Constant Summary collapse

@@logger =
Logger.new(STDOUT)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_uri = '') ⇒ Zephyr

Returns a new instance of Zephyr.



51
52
53
# File 'lib/zephyr.rb', line 51

def initialize(root_uri = '')
  @root_uri = URI.parse(root_uri.to_s).freeze
end

Class Method Details

.build_query_string(params) ⇒ Object



85
86
87
88
89
90
91
92
93
# File 'lib/zephyr.rb', line 85

def build_query_string(params)
  params.map do |k, v|
    if v.kind_of? Array
      build_query_string(v.map { |x| [k, x] })
    else
      "#{percent_encode(k)}=#{percent_encode(v)}"
    end
  end.sort.join '&'
end

.debug_modeObject



73
74
75
# File 'lib/zephyr.rb', line 73

def debug_mode
  @debug_mode
end

.debug_mode=(mode) ⇒ Object



77
78
79
# File 'lib/zephyr.rb', line 77

def debug_mode=(mode)
  @debug_mode = mode
end

.loggerObject



65
66
67
# File 'lib/zephyr.rb', line 65

def logger
  @@logger
end

.logger=(logger) ⇒ Object



69
70
71
# File 'lib/zephyr.rb', line 69

def logger=(logger)
  @@logger = logger
end

.percent_encode(value) ⇒ Object



81
82
83
# File 'lib/zephyr.rb', line 81

def percent_encode(value)
  Typhoeus::Curl.easy_escape(typhoeus_easy.handle, value.to_s, value.to_s.bytesize)
end

Instance Method Details

#cleanup!Object



261
262
263
# File 'lib/zephyr.rb', line 261

def cleanup!
  Typheous::Hydra.hydra.cleanup
end

#create_json_response(response, yajl_opts = {}) ⇒ Object



354
355
356
357
358
359
360
361
362
363
# File 'lib/zephyr.rb', line 354

def create_json_response(response, yajl_opts = {})
  return response if response.nil? || !response.key?(:headers) || !response[:headers].key?('content-type')
  content_type = response[:headers]['content-type']
  content_type = content_type.first if content_type.respond_to?(:first)

  if response[:body] && content_type.to_s.strip.match(/^application\/json/)
    response[:json] = Yajl::Parser.parse(response[:body], yajl_opts)
  end
  response
end

#create_request_headers(hash) ⇒ Object



344
345
346
347
348
349
350
351
352
# File 'lib/zephyr.rb', line 344

def create_request_headers(hash)
  h = Headers.new
  hash.each {|k,v| h.add_field(k,v) }
  [].tap do |arr|
    h.each_capitalized do |k,v|
      arr << "#{k}: #{v}"
    end
  end
end

#custom(method, expected_statuses, timeout, path_components, headers = {}) ⇒ Object

Performs a custom HTTP method request to the specified resource.

A PURGE request to /users/#Zephyr.@[email protected] which is expecting a 200 OK within 666ms

http.custom(:purge, 200, 666, ["users", @user.id])

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


233
234
235
236
237
# File 'lib/zephyr.rb', line 233

def custom(method, expected_statuses, timeout, path_components, headers={})
  headers = default_headers.merge(headers)
  verify_path!(path_components)
  perform(method, path_components, headers, expected_statuses, timeout)
end

#default_headersObject



55
56
57
58
59
60
# File 'lib/zephyr.rb', line 55

def default_headers
  {
    'Accept'      => 'application/json;q=0.7, */*;q=0.5',
    'User-Agent'  => 'zephyr',
  }
end

#delete(expected_statuses, timeout, path_components, headers = {}) ⇒ Object

Performs a DELETE request to the specified resource.

A request to /users/#Zephyr.@[email protected]/things?q=woof which is expecting a 204 No Content within 666ms

http.put(200, 666, ["users", @user.id, "things", {"q" => "woof"}])

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


217
218
219
220
221
# File 'lib/zephyr.rb', line 217

def delete(expected_statuses, timeout, path_components, headers={})
  headers = default_headers.merge(headers)
  verify_path!(path_components)
  perform(:delete, path_components, headers, expected_statuses, timeout)
end

#get(expected_statuses, timeout, path_components, headers = {}) ⇒ Object

Performs a GET request to the specified resource.

A request to /users/#Zephyr.@[email protected]/things?q=woof with an Accept header of “text/plain” which is expecting a 200 OK within 50ms

http.get(200, 50 ["users", @user.id, "things", {"q" => "woof"}], "Accept" => "text/plain")

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


137
138
139
140
141
# File 'lib/zephyr.rb', line 137

def get(expected_statuses, timeout, path_components, headers={})
  headers   = default_headers.merge(headers)
  verify_path!(path_components)
  perform(:get, path_components, headers, expected_statuses, timeout)
end

#get_json(expected_statuses, timeout, path_components, headers = {}, yajl_opts = {}) ⇒ Object

The same thing as #get, but decodes the response entity as JSON (if it’s application/json) and adds it under the :json key in the returned hash.



145
146
147
148
# File 'lib/zephyr.rb', line 145

def get_json(expected_statuses, timeout, path_components, headers={}, yajl_opts={})
  response = get(expected_statuses, timeout, path_components, headers)
  create_json_response(response, yajl_opts)
end

#head(expected_statuses, timeout, path_components, headers = {}) ⇒ Object

Performs a HEAD request to the specified resource.

A request to /users/#Zephyr.@[email protected]/things?q=woof with an Accept header of “text/plain” which is expecting a 200 OK within 50ms

http.head(200, 50, ["users", @user.id, "things", {"q" => "woof"}], "Accept" => "text/plain")

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


120
121
122
123
124
# File 'lib/zephyr.rb', line 120

def head(expected_statuses, timeout, path_components, headers={})
  headers = default_headers.merge(headers)
  verify_path!(path_components)
  perform(:head, path_components, headers, expected_statuses, timeout)
end

#inspectObject

Comes handy in IRB



257
258
259
# File 'lib/zephyr.rb', line 257

def inspect
  '#<%s:0x%s root_uri=%s>' % [ self.class.to_s, object_id.to_s(16), uri.to_s ]
end

#perform(method, path_components, headers, expect, timeout, data = nil) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/zephyr.rb', line 288

def perform(method, path_components, headers, expect, timeout, data=nil)
  params           = {}
  params[:headers] = headers
  params[:timeout] = timeout
  params[:follow_location] = false

  if path_components.last.is_a?(Hash) && (!data || data.empty?)
    params[:params]  = path_components.pop
  end

  params[:method]  = method

  # seriously, why is this on by default
  Typhoeus::Hydra.hydra.disable_memoization

  # if you want debugging
  params[:verbose] = Zephyr.debug_mode

  # have a vague feeling this isn't going to work as expected
  if method == :post || method == :put
    data = data.read if data.respond_to?(:read)
    params[:body] = data if data != ''
  end

  http_start = Time.now.to_f
  response   = Typhoeus::Request.run(uri(path_components).to_s, params)
  http_end   = Time.now.to_f

  Zephyr.logger.info "[zephyr:#{$$}:#{Time.now.to_f}] \"%s %s\" %s %0.4f" % [
    method.to_s.upcase, response.request.url, response.code, (http_end - http_start)
  ]

  # be consistent with what came before
  response_headers = Headers.new.tap do |h|
    response.headers.split(/\n/).each do |header_line|
      h.parse(header_line)
    end
  end

  if !response.timed_out? && valid_response?(expect, response.code)
    result = { :headers => response_headers.to_hash, :status => response.code }
    if return_body?(method, response.code)
      result[:body] = response.body
    end
    result
  else
    failed_request = FailedRequest.new(:method        => method,
                                       :uri           => response.request.url,
                                       :expected_code => expect,
                                       :timeout       => timeout,
                                       :response      => response)
    Zephyr.logger.error "[zephyr:#{$$}:#{Time.now.to_f}]: #{failed_request}"
    raise failed_request
  end
end

#post(expected_statuses, timeout, path_components, entity, headers = {}) ⇒ Object

Performs a POST request to the specified resource.

A request to /users/#Zephyr.@[email protected]/things?q=woof with an Content-Type header of “text/plain” and a request entity of “yay” which is expecting a 201 Created within 500ms

http.post(201, 500, ["users", @user.id, "things", {"q" => "woof"}], "yay", "Content-Type" => "text/plain")

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


187
188
189
190
191
# File 'lib/zephyr.rb', line 187

def post(expected_statuses, timeout, path_components, entity, headers={})
  headers   = default_headers.merge(headers)
  verify_path_and_entity!(path_components, entity)
  perform(:post, path_components, headers, expected_statuses, timeout, entity)
end

#post_json(expected_statuses, timeout, path_components, entity, headers = {}) ⇒ Object

The same thing as #post, but encodes the entity as JSON and specifies “application/json” as the request entity content type.



195
196
197
198
199
200
201
202
203
204
# File 'lib/zephyr.rb', line 195

def post_json(expected_statuses, timeout, path_components, entity, headers={})
  response = post(
                  expected_statuses,
                  timeout,
                  path_components,
                  Yajl::Encoder.encode(entity),
                  headers.merge("Content-Type" => "application/json")
                 )
  create_json_response(response)
end

#put(expected_statuses, timeout, path_components, entity, headers = {}) ⇒ Object

Performs a PUT request to the specified resource.

A request to /users/#Zephyr.@[email protected]/things?q=woof with an Content-Type header of “text/plain” and a request entity of “yay” which is expecting a 204 No Content within 1000ms

http.put(204, 1000, ["users", @user.id, "things", {"q" => "woof"}], "yay", "Content-Type" => "text/plain")

This returns a hash with three keys:

:status       The numeric HTTP status code
:body         The body of the response entity, if any
:headers      A hash of header values


162
163
164
165
166
# File 'lib/zephyr.rb', line 162

def put(expected_statuses, timeout, path_components, entity, headers={})
  headers = default_headers.merge(headers)
  verify_path_and_entity!(path_components, entity)
  perform(:put, path_components, headers, expected_statuses, timeout, entity)
end

#put_json(expected_statuses, timeout, path_components, entity, headers = {}) ⇒ Object

The same thing as #put, but encodes the entity as JSON and specifies “application/json” as the request entity content type.



170
171
172
173
# File 'lib/zephyr.rb', line 170

def put_json(expected_statuses, timeout, path_components, entity, headers={})
  response = put(expected_statuses, timeout, path_components, Yajl::Encoder.encode(entity), headers.merge("Content-Type" => "application/json"))
  create_json_response(response)
end

#return_body?(method, code) ⇒ Boolean

Returns:

  • (Boolean)


284
285
286
# File 'lib/zephyr.rb', line 284

def return_body?(method, code)
  method != :head && !valid_response?([204,205,304], code)
end

#uri(given_parts = []) ⇒ Object

Creates a URI object, combining the root_uri passed on initialization with the given parts.

Example:

http = Zephyr.new 'http://host/'
http.uri(['hi', 'bob', {:foo => 'bar'}]) => http://host/hi/bob?foo=bar


247
248
249
250
251
252
253
# File 'lib/zephyr.rb', line 247

def uri(given_parts = [])
  @root_uri.dup.tap do |uri|
    parts     = given_parts.dup.unshift(uri.path) # URI#merge is broken.
    uri.query = Zephyr.build_query_string(parts.pop) if parts.last.is_a? Hash
    uri.path  = ('/%s' % parts.join('/')).gsub(/\/+/, '/')
  end
end

#valid_response?(expected, actual) ⇒ Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/zephyr.rb', line 280

def valid_response?(expected, actual)
  Array(expected).map { |code| code.to_i }.include?(actual.to_i)
end

#verify_path!(path_components) ⇒ Object

Raises:

  • (ArgumentError)


275
276
277
278
# File 'lib/zephyr.rb', line 275

def verify_path!(path_components)
  path_components = Array(path_components).flatten
  raise ArgumentError, "Resource path too short" unless path_components.length > 0
end

#verify_path_and_entity!(path_components, entity) ⇒ Object

Raises:

  • (ArgumentError)


265
266
267
268
269
270
271
272
273
# File 'lib/zephyr.rb', line 265

def verify_path_and_entity!(path_components, entity)
  begin
    verify_path!(path_components)
  rescue ArgumentError
    raise ArgumentError, "You must supply both a resource path and a body."
  end

  raise ArgumentError, "Request body must be a string or IO." unless String === entity || IO === entity
end