springboardretail/springboard-client-ruby

View on GitHub
lib/heartland/client.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
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'