class HalClient

Adapter used to access resources.

Operations on a HalClient instance are not thread-safe. If you'd like to use a HalClient instance in a threaded environment, consider using the method clone_for_use_in_different_thread to create a copy for each new thread

Constants

HttpClientError

Server response with a 4xx status code

HttpServerError

Server responded with a 5xx status code

InvalidRepresentationError

The representation is not a valid HAL document.

NotACollectionError

The representation is not a HAL collection

NullAuthHelper
TimeoutError
VERSION

Attributes

auth_helper[R]
default_entity_request_headers[R]
default_message_request_headers[R]
headers[R]
logger[R]
retry_duration[R]
retryinator[R]
timeout[R]

Public Class Methods

new(options={}) click to toggle source

Initializes a new client instance

options - hash of configuration options

:accept - one or more content types that should be
  prepended to the `Accept` header field of each request.
:content_type - a single content type that should be
  prepended to the `Content-Type` header field of each request.
:authorization - a `#call`able which takes the url being
  requested and returns the authorization header value to use
  for the request or a string which will always be the value of
  the authorization header
:headers - a hash of other headers to send on each request.
:base_client - An HTTP::Client object to use.
:logger - a Logger object to which benchmark and activity info
   will be written. Benchmark data will be written at info level
   and activity at debug level.
:timeout - number of seconds that after which any request will be
   terminated and an exception raised. Default: Float::INFINITY
# File lib/hal_client.rb, line 47
def initialize(options={})
  @default_message_request_headers = HTTP::Headers.new
  @default_entity_request_headers = HTTP::Headers.new
  @auth_helper = as_callable(options.fetch(:authorization, NullAuthHelper))
  @base_client ||= options[:base_client]
  @logger = options.fetch(:logger, NullLogger.new)
  @timeout = options.fetch(:timeout, Float::INFINITY)
  @base_client_with_headers = {}
  @retry_duration = options.fetch(:retry_duration, Retryinator::DEFAULT_DURATION)

  @retryinator = Retryinator.new(logger: logger, duration: retry_duration)

  default_message_request_headers.set('Accept', options[:accept]) if
    options[:accept]
  # Explicit accept option has precedence over accepts in the
  # headers option.

  options.fetch(:headers, {}).each do |name, value|
    if entity_header_field? name
      default_entity_request_headers.add(name, value)
    else
      default_message_request_headers.add(name, value)
    end
  end

  default_entity_request_headers.set('Content-Type', options[:content_type]) if
    options[:content_type]
  # Explicit content_content options has precedence over content
  # type in the headers option.

  default_entity_request_headers.set('Content-Type', 'application/hal+json') unless
    default_entity_request_headers['Content-Type']
  # We always want a content type. If the user doesn't explicitly
  # specify one we provide a default.

  accept_values = Array(default_message_request_headers.get('Accept')) +
    ['application/hal+json;q=0']
  default_message_request_headers.set('Accept', accept_values.join(", "))
  # We can work with HAL so provide a back stop accept.
end

Protected Class Methods

def_idempotent_unsafe_request(method) click to toggle source
# File lib/hal_client.rb, line 138
def def_idempotent_unsafe_request(method)
  verb = method.to_s.upcase

  define_method(method) do |url, data, headers={}|
    headers = auth_headers(url).merge(headers)

    req_body = if data.respond_to? :to_hal
                 data.to_hal
               elsif data.is_a? Hash
                 data.to_json
               else
                 data
               end

    begin
      client = client_for_post(override_headers: headers)
      resp = bmtb("#{verb} <#{url}>") {
        retryinator.retryable { client.request(method, url, body: req_body) }
      }
      interpret_response resp

    rescue HttpError => e
      fail e.class.new("#{verb} <#{url}> failed with code #{e.response.status}", e.response)
    end
  end
end
def_unsafe_request(method) click to toggle source
# File lib/hal_client.rb, line 111
def def_unsafe_request(method)
  verb = method.to_s.upcase

  define_method(method) do |url, data, headers={}|
    headers = auth_headers(url).merge(headers)

    req_body = if data.respond_to? :to_hal
                 data.to_hal
               elsif data.is_a? Hash
                 data.to_json
               else
                 data
               end

    begin
      client = client_for_post(override_headers: headers)
      resp = bmtb("#{verb} <#{url}>") {
        client.request(method, url, body: req_body)
      }
      interpret_response resp

    rescue HttpError => e
      fail e.class.new("#{verb} <#{url}> failed with code #{e.response.status}", e.response)
    end
  end
end

Public Instance Methods

clone_for_use_in_different_thread() click to toggle source

Returns a copy of this instance that is safe to use in threaded environments

# File lib/hal_client.rb, line 90
def clone_for_use_in_different_thread
  clone.tap { |c| c.clear_clients! }
end
delete(url, headers={}) click to toggle source

Delete a `Representation` or `String` to the resource identified at `url`.

url - The URL of the resource of interest. headers - custom header fields to use for this request

# File lib/hal_client.rb, line 191
def delete(url, headers={})
  headers = auth_headers(url).merge(headers)

  begin
    client = client_for_post(override_headers: headers)
    resp = bmtb("DELETE <#{url}>") { retryinator.retryable { client.request(:delete, url) } }
    interpret_response resp
  rescue HttpError => e
    fail e.class.new("DELETE <#{url}> failed with code #{e.response.status}", e.response)
  end
end
get(url, headers={}) click to toggle source

Returns a `Representation` of the resource identified by `url`.

url - The URL of the resource of interest. headers - custom header fields to use for this request

# File lib/hal_client.rb, line 98
def get(url, headers={})
  headers = auth_headers(url).merge(headers)
  client = client_for_get(override_headers: headers)
  resp = retryinator.retryable { bmtb("GET <#{url}>") { client.get(url) } }
  interpret_response resp

rescue HttpError => e
  fail e.class.new("GET <#{url}> failed with code #{e.response.status}", e.response)
end

Protected Instance Methods

as_callable(thing) click to toggle source
# File lib/hal_client.rb, line 209
def as_callable(thing)
  if thing.respond_to?(:call)
    thing
  else
    ->(*_args) { thing }
  end
end
auth_headers(url) click to toggle source
# File lib/hal_client.rb, line 217
def auth_headers(url)
  if h_val = auth_helper.call(url)
    {"Authorization" => h_val}
  else
    {}
  end
end
base_client() click to toggle source

Returns an HTTP client.

# File lib/hal_client.rb, line 283
def base_client
  @base_client ||= begin
    logger.debug 'Created base_client'
    HTTP::Client.new(follow: true)
  end
end
base_client_with_headers(headers) click to toggle source
# File lib/hal_client.rb, line 290
def base_client_with_headers(headers)
  @base_client_with_headers[headers.to_h] ||= begin
    logger.debug { "Created base_client with headers #{headers.inspect}" }
    base_client.headers(headers)
  end
end
benchmark(msg) { || ... } click to toggle source
# File lib/hal_client.rb, line 323
def benchmark(msg, &blk)
  result = nil
  elapsed = Benchmark.realtime do
    result = yield
  end

  logger.info '%s (%.1fms)' % [ msg, elapsed*1000 ]

  result
end
bmtb(msg, &blk) click to toggle source
# File lib/hal_client.rb, line 308
def bmtb(msg, &blk)
  benchmark(msg) { timebox(msg, &blk) }
end
clear_clients!() click to toggle source

Resets memoized HTTP clients

# File lib/hal_client.rb, line 277
def clear_clients!
  @base_client = nil
  @base_client_with_headers = {}
end
client_for_get(options={}) click to toggle source

Returns the HTTP client to be used to make get requests.

options

:override_headers -
# File lib/hal_client.rb, line 260
def client_for_get(options={})
  headers = default_message_request_headers.merge(options[:override_headers])

  base_client_with_headers(headers)
end
client_for_post(options={}) click to toggle source

Returns the HTTP client to be used to make post requests.

options

:override_headers -
# File lib/hal_client.rb, line 270
def client_for_post(options={})
  headers = default_entity_and_message_request_headers.merge(options[:override_headers])

  base_client_with_headers(headers)
end
default_entity_and_message_request_headers() click to toggle source
# File lib/hal_client.rb, line 299
def default_entity_and_message_request_headers
  @default_entity_and_message_request_headers ||=
    default_message_request_headers.merge(default_entity_request_headers)
end
entity_header_field?(field_name) click to toggle source
# File lib/hal_client.rb, line 304
def entity_header_field?(field_name)
  [:content_type, /^content-type$/i].any?{|pat| pat === field_name}
end
interpret_response(resp) click to toggle source
# File lib/hal_client.rb, line 225
def interpret_response(resp)
  case resp.status
  when 200...300
    location = resp.headers["Location"]

    begin
      Representation.new(hal_client: self, parsed_json: MultiJson.load(resp.to_s),
                         href: location)
    rescue MultiJson::ParseError, InvalidRepresentationError
      if location
        # response doesn't have a HAL body but we know what resource
        # was created so we can be helpful.
        Representation.new(hal_client: self, href: location)
      else
        # nothing useful to be done
        resp
      end
    end

  when 400...500
    raise HttpClientError.new(nil, resp)

  when 500...600
    raise HttpServerError.new(nil, resp)

  else
    raise HttpError.new(nil, resp)

  end
end
timebox(msg) { || ... } click to toggle source
# File lib/hal_client.rb, line 312
def timebox(msg, &blk)
  if timeout < Float::INFINITY
    Timeout.timeout(timeout, &blk)
  else
    yield
  end

rescue Timeout::Error
  timeout_ms = timeout * 1000
  raise TimeoutError, "Killed %s for taking more than %.1fms." % [msg, timeout_ms]
end