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
Public Class Methods
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
# 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
# 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
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 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
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
# File lib/hal_client.rb, line 209 def as_callable(thing) if thing.respond_to?(:call) thing else ->(*_args) { thing } end end
# File lib/hal_client.rb, line 217 def auth_headers(url) if h_val = auth_helper.call(url) {"Authorization" => h_val} else {} end end
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
# 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
# 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
# File lib/hal_client.rb, line 308 def bmtb(msg, &blk) benchmark(msg) { timebox(msg, &blk) } end
Resets memoized HTTP clients
# File lib/hal_client.rb, line 277 def clear_clients! @base_client = nil @base_client_with_headers = {} end
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
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
# 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
# File lib/hal_client.rb, line 304 def entity_header_field?(field_name) [:content_type, /^content-type$/i].any?{|pat| pat === field_name} end
# 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
# 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