lib/heartland/client.rb
require 'rubygems'
require 'faraday'
require 'json'
require 'logger'
require_relative 'client/errors'
##
# HeartlandRetail namespace
module HeartlandRetail
##
# The main point of interaction for the Heartland Retail Client library.
#
# Client code must successfully authenticate with the API via the {#auth}
# method before calling any HTTP methods or the API will return authorization
# errors.
#
# Provides direct access to the URI-oriented interface via the HTTP methods.
# Provides access to the URI-oriented interface via the {#[]} method.
class Client
##
# Default number of records per page when iterating over collection resources
DEFAULT_PER_PAGE = 20
##
# Default request timeout in seconds
DEFAULT_TIMEOUT = 60
##
# Default connection timeout in seconds
DEFAULT_CONNECT_TIMEOUT = 10
##
# @return [URI] The client's base URI
attr_reader :base_uri
##
# @return [Faraday::Connection] Faraday's connection
attr_reader :connection
##
# @param [String] base_uri Base URI
# @option opts [Boolean, String] :debug Pass true to debug to stdout. Pass a String to debug to given filename.
# @option opts [Boolean] :insecure Disable SSL certificate verification
# @option opts [String] :token Heartland Retail API Token
def initialize(base_uri, opts={})
@base_uri = URI.parse(base_uri)
@opts = opts
configure_connection!
end
##
# Set to true to enable debugging to STDOUT or a string to write to the file
# at that path.
#
# @param [String, Boolean] debug
#
# @return [String, Boolean] The debug argument
def debug=(debug)
@opts[:debug] = debug
configure_connection!
end
##
# @deprecated see {#initialize}.
# Passes the given credentials to the server, storing the session token on success.
#
# @raise [AuthFailed] If the credentials were invalid or the server returned an error
#
# @return [true]
#
# @option opts [String] :username Heartland Retail username
# @option opts [String] :password Heartland Retail password
def auth(opts={})
warn "[DEPRECATION] `auth` is deprecated. Please use `HeartlandRetail::Client.new '#{base_uri}', token: 'secret_token'` instead."
unless opts[:username] && opts[:password]
raise "Must specify :username and :password"
end
body = ::URI.encode_www_form \
:auth_key => opts[:username],
:password => opts[:password]
response = post '/auth/identity/callback', body,
'Content-Type' => 'application/x-www-form-urlencoded'
if response.success?
@session_cookie = response.headers['set-cookie']
return true
else
raise AuthFailed, "Heartland Retail auth failed"
end
end
##
# Performs a HEAD request against the given URI and returns the {Response}.
#
# @return [Response]
def head(uri, headers=false); make_request(:head, uri, headers); end
##
# Performs a HEAD request against the given URI. Returns the {Response}
# on success and raises a {RequestFailed} on failure.
#
# @raise [RequestFailed] On error response
#
# @return [Response]
def head!(uri, headers=false); raise_on_fail head(uri, headers); end
##
# Performs a GET request against the given URI and returns the {Response}.
#
# @return [Response]
def get(uri, headers=false); make_request(:get, uri, headers); end
##
# Performs a GET request against the given URI. Returns the {Response}
# on success and raises a {RequestFailed} on failure.
#
# @raise [RequestFailed] On error response
#
# @return [Response]
def get!(uri, headers=false); raise_on_fail get(uri, headers); end
##
# Performs a DELETE request against the given URI and returns the {Response}.
#
# @return [Response]
def delete(uri, headers=false); make_request(:delete, uri, headers); end
##
# Performs a DELETE request against the given URI. Returns the {Response}
# on success and raises a {RequestFailed} on failure.
#
# @raise [RequestFailed] On error response
#
# @return [Response]
def delete!(uri, headers=false); raise_on_fail delete(uri, headers); end
##
# Performs a PUT request against the given URI and returns the {Response}.
#
# @return [Response]
def put(uri, body, headers=false); make_request(:put, uri, headers, body); end
##
# Performs a PUT request against the given URI. Returns the {Response}
# on success and raises a {RequestFailed} on failure.
#
# @raise [RequestFailed] On error response
#
# @return [Response]
def put!(uri, body, headers=false); raise_on_fail put(uri, body, headers); end
##
# Performs a POST request against the given URI and returns the {Response}.
#
# @return [Response]
def post(uri, body, headers=false); make_request(:post, uri, headers, body); end
##
# Performs a POST request against the given URI. Returns the {Response}
# on success and raises a {RequestFailed} on failure.
#
# @raise [RequestFailed] On error response
#
# @return [Response]
def post!(uri, body, headers=false); raise_on_fail post(uri, body, headers); end
##
# Returns a Resource for the given URI path.
#
# @return [Resource]
def [](uri)
Resource.new(self, uri)
end
##
# Iterates over each page of subordinate resources of the given collection
# resource URI and yields the {Response} to the block.
def each_page(uri)
uri = URI.parse(uri)
total_pages = nil
page = 1
uri.query_values = {'per_page' => DEFAULT_PER_PAGE}.merge(uri.query_values || {})
while total_pages.nil? or page <= total_pages
uri.merge_query_values! 'page' => page
response = get!(uri)
yield response
total_pages ||= response['pages']
page += 1
end
end
##
# Iterates over each subordinate resource of the given collection resource
# URI and yields its representation to the given block.
def each(uri)
each_page(uri) do |page|
page['results'].each do |result|
yield result
end
end
end
##
# Returns a count of subordinate resources of the given collection resource
# URI.
#
# @param [#to_s] uri
# @raise [RequestFailed] If the GET fails
# @return [Integer] The subordinate resource count
def count(uri)
uri = URI.parse(uri)
uri.merge_query_values! 'page' => 1, 'per_page' => 1
get!(uri)['total']
end
private
attr_reader :opts, :session_cookie
def prepare_request_body(body)
body.is_a?(Hash) ? JSON.dump(body) : body
end
def make_request(method, uri, headers=false, body=false)
response = connection.__send__( method, prepare_uri(uri)) do |request|
request.headers = headers unless headers === false
request.headers['Cookie'] = session_cookie if session_cookie
request.body = prepare_request_body(body) unless body === false
end
new_response(response)
end
def raise_on_fail(response)
if !response.success?
error = RequestFailed.new "Request failed with status: #{response.status}"
error.response = response
raise error
end
response
end
def prepare_uri(uri)
uri = URI.parse(uri)
uri.to_s
.gsub(/^#{base_uri.to_s}|^#{base_uri.path}/, '')
.gsub(/^\//, '')
end
def new_response(faraday_response)
Response.new faraday_response, self
end
def configure_connection!
@connection = Faraday.new
connection.url_prefix= base_uri.to_s
connection.headers['Content-Type'] = 'application/json'
connection.headers['Authorization'] = "Bearer #{opts[:token]}" if opts[:token]
connection.ssl[:verify] = false if opts.has_key?(:insecure)
connection.options.timeout = DEFAULT_TIMEOUT
connection.options.open_timeout = DEFAULT_CONNECT_TIMEOUT
if debug = opts[:debug]
connection.response :logger, debug_logger(debug), bodies: true
end
end
def debug_logger(debug)
Logger.new(debug == true ? STDOUT : debug)
end
end
end
##
# Springboard namespace as alias of HeartlandRetail namespace for backwards compatability
module Springboard
include HeartlandRetail
##
# HeartlandRetail::Client with added deprecation warning for Springboard namespace
class Client < HeartlandRetail::Client
def initialize(base_uri, opts={})
warn "[DEPRECATION] `Springboard::Client.new` is deprecated. Please use `HeartlandRetail::Client.new` instead."
super
end
end
end
require_relative 'client/resource'
require_relative 'client/response'
require_relative 'client/body'
require_relative 'client/uri'