class Network::Client

This class is simple JSON client that is meant to be initialized with a single URI. Subsequent calls should target endpoints/paths of that URI.

Constants

DEFAULT_HEADERS
LOG_TAG

Stamp in front of each log written by client +@logger+.

Response

The success response template.

Represents the return of rest-like methods holding two values: HTTP response code, and body (parsed as json if request type is json).

Attributes

auth_token_header[R]
bearer_token[R]
default_headers[R]
errors_to_propagate[RW]

Error list for stop and propagate strategy. Takes priority over +@errors_to_recover+. Do not assign ancestor error classes here that prevent retry for descendant ones.

errors_to_recover[RW]

Error list for retrying strategy. Initially contains common errors encountered usually in net calls.

http[RW]

Gives access to underlying NET::HTTP client instance.

logger[R]
password[R]
tries[R]
user_agent[R]
username[R]

Public Class Methods

new(endpoint:, tries: 2, headers: {}, username: nil, password: nil, user_agent: 'Network Client') click to toggle source

Construct and prepare client for requests targeting endpoint.

Parameters:

endpoint

string Uri for the host with schema and port. any other segment like paths will be discarded.

tries

integer to specify how many is to repeat failed calls. Default is 2.

headers

hash to contain any common HTTP headers to be set in client calls.

username

string for HTTP basic authentication. Applies on all requests. Default to nil.

password

string for HTTP basic authentication. Applies on all requests. Default to nil.

user_agent

string Specifies the User-Agent header value when making requests.

User-Agent header value provided within headers parameter in initialize or on one of request methods will take precedence over user_agent parameter.

Example:

require "network-client"

github_client = Network::Client.new(endpoint: 'https://api.github.com')
github_client.get '/emojis'

#=> { "+1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png?v7",
      "-1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44e.png?v7",
      ... }
# File lib/network/client.rb, line 63
def initialize(endpoint:, tries: 2, headers: {}, username: nil, password: nil,
               user_agent: 'Network Client')
  @uri = URI.parse(endpoint)
  @tries = tries

  set_http_client
  define_error_strategies
  set_default_headers(headers)
  set_basic_auth(username, password)
  set_bearer_auth
  set_token_auth
  set_logger
  set_user_agent(headers['User-Agent'] || user_agent)
end

Public Instance Methods

delete(path, params: {}, headers: {}) click to toggle source

Perform a delete request on the targeted client endpoint.

Parameters:

path

string path on client's target host.

params

hash request parameters to json encoded in request body.

headers

hash set of http request headers.

Returns:

http response data contained in Response struct.

# File lib/network/client.rb, line 149
def delete(path, params: {}, headers: {})
  request_json :delete, path, params, headers
end
get(path, params: {}, headers: {}) click to toggle source

Perform a get request on the targeted client endpoint.

Parameters:

path

string path on client's target host.

params

request parameters to be url encoded. Can be hash or pair of values array.

headers

hash set of http request headers.

Returns:

http response data contained in Response struct.

# File lib/network/client.rb, line 89
def get(path, params: {}, headers: {})
  request_json :get, path, params, headers
end
get_html(path, params: {}, headers: {}) click to toggle source
# File lib/network/client.rb, line 153
def get_html(path, params: {}, headers: {})
  raise NotImplementedError
end
patch(path, params: {}, headers: {}) click to toggle source

Perform a patch request on the targeted client endpoint.

Parameters:

path

string path on client's target host.

params

hash request parameters to json encoded in request body.

headers

hash set of http request headers.

Returns:

http response data contained in Response struct.

# File lib/network/client.rb, line 119
def patch(path, params: {}, headers: {})
  request_json :patch, path, params, headers
end
post(path, params: {}, headers: {}) click to toggle source

Perform a post request on the targeted client endpoint.

Parameters:

path

string path on client's target host.

params

hash request parameters to json encoded in request body.

headers

hash set of http request headers.

Returns:

http response data contained in Response struct.

# File lib/network/client.rb, line 104
def post(path, params: {}, headers: {})
  request_json :post, path, params, headers
end
post_form(path, params: {}, headers: {}) click to toggle source
# File lib/network/client.rb, line 157
def post_form(path, params: {}, headers: {})
  raise NotImplementedError
end
put(path, params: {}, headers: {}) click to toggle source

Perform a put request on the targeted client endpoint.

Parameters:

path

string path on client's target host.

params

hash request parameters to json encoded in request body.

headers

hash set of http request headers.

Returns:

http response data cotained in Response strcut.

# File lib/network/client.rb, line 134
def put(path, params: {}, headers: {})
  request_json :put, path, params, headers
end
set_basic_auth(username, password) click to toggle source
# File lib/network/client.rb, line 180
def set_basic_auth(username, password)
  @username = username.nil? ? '' : username
  @password = password.nil? ? '' : password
end
set_bearer_auth(token: '') click to toggle source

Assigns authentication bearer type token for use in standard HTTP authorization header.

Parameters:

token

string bearer token value.

Returns:

@bearer_token

string the newly assigned +@bearer_token+ value.

# File lib/network/client.rb, line 194
def set_bearer_auth(token: '')
  @bearer_token = token
end
set_logger() { || ... } click to toggle source

Sets the client logger object. Execution is yielded to passed block to set, customize, and returning a logger instance.

Returns:

logger instance variable.

# File lib/network/client.rb, line 168
def set_logger
  @logger = if block_given?
    yield
  elsif defined?(Rails)
    Rails.logger
  else
    logger = Logger.new(STDOUT)
    logger.level = Logger::DEBUG
    logger
  end
end
set_token_auth(header_value: '') click to toggle source

Assigns custom authentication token for use in standard HTTP authorization header. This takes precedence over Bearer authentication if both are set.

Parameters:

header_value

string full authorization header value. _(e.g. Token token=123)_.

Returns:

@auth_token_header

string the newly assigned +@auth_token_header+ value.

# File lib/network/client.rb, line 208
def set_token_auth(header_value: '')
  @auth_token_header = header_value
end
set_user_agent(new_user_agent) click to toggle source

Assigns a new User-Agent header to be sent in any subsequent request.

Parameters:

new_user_agent

string the user-agent header value.

Returns:

@user_agent

string the newly assigned User-Agent header value.

# File lib/network/client.rb, line 221
def set_user_agent(new_user_agent)
  @user_agent = @default_headers['User-Agent'] = new_user_agent
end

Private Instance Methods

authenticate(headers) click to toggle source
# File lib/network/client.rb, line 290
def authenticate(headers)
  headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.empty?
  headers['Authorization'] = auth_token_header        unless auth_token_header.empty?
  headers
end
basic_auth(request) click to toggle source
# File lib/network/client.rb, line 286
def basic_auth(request)
  request.basic_auth(@username, @password) unless @username.empty? && @password.empty?
end
define_error_strategies() click to toggle source
# File lib/network/client.rb, line 237
def define_error_strategies
  @errors_to_recover   = [Net::HTTPTooManyRequests,
                          Net::HTTPServerError,
                          Net::ProtocolError,
                          Net::HTTPBadResponse,
                          Net::ReadTimeout,
                          Net::OpenTimeout,
                          Errno::ECONNREFUSED,
                          Errno::ETIMEDOUT,
                          Errno::ECONNRESET,
                          Errno::EHOSTUNREACH,
                          Timeout::Error,
                          OpenSSL::SSL::SSLError,
                          EOFError,
                          SocketError,
                          IOError]
  @errors_to_propagate = [Net::HTTPRequestURITooLarge,
                          Net::HTTPMethodNotAllowed,
                          Zlib::BufError,
                          OpenSSL::X509::CertificateError]
end
encode_path_params(path, params) click to toggle source
# File lib/network/client.rb, line 332
def encode_path_params(path, params)
  if params.nil? || params.empty?
    path
  else
    params = stringify_keys(params)
    encoded = URI.encode_www_form(params)
    [path, encoded].join("?")
  end
end
formulate_path(path) click to toggle source
# File lib/network/client.rb, line 342
def formulate_path(path)
  path = '/'  if path.nil? || path.empty?
  path = path.strip if path.respond_to?(:strip)
  path.prepend('/') unless path.chars.first == '/'
  path
end
http_request(request) click to toggle source
# File lib/network/client.rb, line 296
def http_request(request)
  tries_count ||= @tries
  finished = ->() { (tries_count -= 1).zero? }

  begin
    response = @http.request(request)
  end until !recoverable?(response) || finished.call
  response

rescue *@errors_to_propagate => error
  log "Request Failed. \nReason: #{error.message}"
  raise

rescue *@errors_to_recover => error
  warn_on_retry "#{error.message}"
  finished.call ? raise : retry
end
log(message) click to toggle source
# File lib/network/client.rb, line 349
def log(message)
  @logger.error("\n#{LOG_TAG} #{message}.")
end
parse_as_json(response_body) click to toggle source
# File lib/network/client.rb, line 323
def parse_as_json(response_body)
  body = response_body
  body = body.nil? || body.empty? ? body : JSON.parse(body)

rescue JSON::ParserError => error
  log "Parsing response body as JSON failed! Returning raw body. \nDetails: \n#{error.message}"
  body
end
recoverable?(response) click to toggle source
# File lib/network/client.rb, line 314
def recoverable?(response)
  if @errors_to_recover.any? { |error_class| response.is_a?(error_class) }
    warn_on_retry "#{response.class} response type."
    true
  else
    false
  end
end
request(http_method, path, params, headers) click to toggle source
# File lib/network/client.rb, line 265
def request(http_method, path, params, headers)
  path = formulate_path(path)
  path = encode_path_params(path, params) if http_method == :get

  headers = @default_headers.merge(headers)
  headers = authenticate(headers)

  request = Net::HTTP::const_get(http_method.to_s.capitalize.to_sym).new(path, headers)
  request.body = params.to_s unless http_method == :get

  basic_auth(request)

  response = http_request(request)

  unless Net::HTTPSuccess === response
    log "endpoint responded with non-success #{response.code} code.\nResponse: #{response.body}"
  end

  response
end
request_json(http_method, path, params, headers) click to toggle source
# File lib/network/client.rb, line 259
def request_json(http_method, path, params, headers)
  response = request(http_method, path, params, headers)
  body = parse_as_json(response.body)
  Response.new(response.code.to_i, body)
end
set_default_headers(headers) click to toggle source
# File lib/network/client.rb, line 233
def set_default_headers(headers)
  @default_headers = DEFAULT_HEADERS.merge(headers)
end
set_http_client() click to toggle source
# File lib/network/client.rb, line 227
def set_http_client
  @http = Net::HTTP.new(@uri.host, @uri.port)
  @http.use_ssl = @uri.scheme == 'https' ? true : false
  @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
stringify_keys(params) click to toggle source
# File lib/network/client.rb, line 357
def stringify_keys(params)
  params.respond_to?(:keys) ? params.collect { |k, v| [k.to_s, v] }.to_h : params
end
warn_on_retry(message) click to toggle source
# File lib/network/client.rb, line 353
def warn_on_retry(message)
  @logger.warn("\n#{LOG_TAG} #{message} \nRetrying now ..")
end