class Heroics::Link
A link invokes requests with an HTTP server.
Public Class Methods
Instantiate a link.
@param url [String] The URL to use when making requests. Include the
username and password to use with HTTP basic auth.
@param link_schema [LinkSchema] The schema for this link. @param options [Hash] Configuration
for the link. Possible keys
include: - default_headers: Optionally, a set of headers to include in every request made by the client. Default is no custom headers. - cache: Optionally, a Moneta-compatible cache to store ETags. Default is no caching.
# File lib/heroics/link.rb, line 16 def initialize(url, link_schema, options={}) @root_url, @path_prefix = unpack_url(url) @link_schema = link_schema @default_headers = options[:default_headers] || {} @cache = options[:cache] || {} @rate_throttle = options[:rate_throttle] || Heroics::Configuration.defaults.options[:rate_throttle] @status_codes = options[:status_codes] || Heroics::Configuration.defaults.options[:status_codes] end
Public Instance Methods
Make a request to the server.
JSON content received with an ETag is cached. When the server returns a *304 Not Modified* status code content is loaded and returned from the cache. The cache considers headers, in addition to the URL path, when creating keys so that requests to the same path, such as for paginated results, don’t cause cache collisions.
When the server returns a *206 Partial Content* status code the result is assumed to be an array and an enumerator is returned. The enumerator yields results from the response until they’ve been consumed at which point, if additional content is available from the server, it blocks and makes a request to fetch the subsequent page of data. This behaviour continues until the client stops iterating the enumerator or the dataset from the server has been entirely consumed.
@param parameters [Array] The list of parameters to inject into the
path. A request body can be passed as the final parameter and will always be converted to JSON before being transmitted.
@raise [ArgumentError] Raised if either too many or too few parameters
were provided.
@return [String,Object,Enumerator] A string for text responses, an
object for JSON responses, or an enumerator for list responses.
# File lib/heroics/link.rb, line 48 def run(*parameters) path, body = @link_schema.format_path(parameters) path = "#{@path_prefix}#{path}" unless @path_prefix == '/' headers = @default_headers if body case @link_schema.method when :put, :post, :patch headers = headers.merge({'Content-Type' => @link_schema.content_type}) body = @link_schema.encode(body) when :get, :delete if body.is_a?(Hash) query = body else query = MultiJson.load(body) end body = nil end end connection = Excon.new(@root_url, thread_safe_sockets: true) response = request_with_cache(connection, method: @link_schema.method, path: path, headers: headers, body: body, query: query, expects: [200, 201, 202, 204, 206]) content_type = response.headers['Content-Type'] if content_type && content_type =~ /application\/.*json/ body = MultiJson.load(response.body) if response.status == 206 next_range = response.headers['Next-Range'] Enumerator.new do |yielder| while true do # Yield the results we got in the body. body.each do |item| yielder << item end # Only make a request to get the next page if we have a valid # next range. break unless next_range headers = headers.merge({'Range' => next_range}) response = request_with_cache(connection, method: @link_schema.method, path: path, headers: headers, expects: [200, 201, 206]) body = MultiJson.load(response.body) next_range = response.headers['Next-Range'] end end else body end elsif !response.body.empty? response.body end end
Private Instance Methods
# File lib/heroics/link.rb, line 106 def request_with_cache(connection, options) options[:expects] << 304 @status_codes.each do |code| options[:expects] << code end cache_key = "#{options[:path]}:#{options[:headers].hash}" if options[:method] == :get etag = @cache["etag:#{cache_key}"] options[:headers] = options[:headers].merge({'If-None-Match' => etag}) if etag end response = @rate_throttle.call do connection.request(options) end if response.status == 304 body = @cache["data:#{cache_key}"] status = @cache["status:#{cache_key}"] headers = @cache["headers:#{cache_key}"] CachedResponse.new(status, body, headers) else if etag = response.headers['ETag'] @cache["etag:#{cache_key}"] = etag @cache["data:#{cache_key}"] = response.body @cache["status:#{cache_key}"] = response.status @cache["headers:#{cache_key}"] = response.headers end response end end
Unpack the URL and split it into a root URL and a path prefix, if one exists.
@param url [String] The complete base URL to use when making requests. @return [String,String] A (root URL, path) prefix pair.
# File lib/heroics/link.rb, line 142 def unpack_url(url) root_url = [] path_prefix = '' parts = URI.split(url) root_url << "#{parts[0]}://" root_url << "#{parts[1]}@" unless parts[1].nil? root_url << "#{parts[2]}" root_url << ":#{parts[3]}" unless parts[3].nil? path_prefix = parts[5] return root_url.join(''), path_prefix end