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



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

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

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



349
350
351
352
353
354
355
356
357
358
# File 'lib/zephyr.rb', line 349

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



339
340
341
342
343
344
345
346
347
# File 'lib/zephyr.rb', line 339

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



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

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



287
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
# File 'lib/zephyr.rb', line 287

def perform(method, path_components, headers, expect, timeout, data=nil)
  params           = {}
  params[:headers] = headers
  params[:timeout] = timeout
  params[:follow_location] = false
  params[:params]  = path_components.pop if path_components.last.is_a?(Hash)
  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)


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

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
# 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.path  = ('/%s' % parts.join('/')).gsub(/\/+/, '/')
  end
end

#valid_response?(expected, actual) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#verify_path!(path_components) ⇒ Object

Raises:

  • (ArgumentError)


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

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)


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

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