contactually/zuora-ruby

View on GitHub
lib/zuora/rest/client.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# encoding: utf-8
# frozen_string_literal: true

require 'faraday'
require 'faraday_middleware'
require 'json'

module Zuora
  module Rest
    class Client
      attr_reader :connection

      # Creates a connection instance.
      # Makes an initial HTTP request to fetch session token.
      # Subsequent requests made with .get, .post, and .put
      # contain the authenticated session id in their headers.
      # @param [String] username
      # @param [String] password
      # @param [Boolean] sandbox
      # @return [Zuora::Client] with .connection, .put, .post
      def initialize(username, password, sandbox = false, modern_endpoint = false)
        @modern_endpoint = modern_endpoint
        base_url = api_url sandbox
        conn = connection base_url

        response = conn.post do |req|
          set_auth_request_headers! req, username, password
        end

        case response.status
        when 200
          @auth_cookie = response.headers['set-cookie'].split(' ')[0]
          @connection = conn
        when 429
          sleep(Zuora::RETRY_WAITING_PERIOD)
          return initialize(username, password, sandbox, modern_endpoint)
        else
          raise Zuora::Rest::ConnectionError, response.body['reasons']
        end
      end

      # @param [String] url - URL of request
      # @return [Faraday::Response] A response, with .headers, .status & .body
      %i[get delete].each do |method|
        define_method(method) do |url|
          response = @connection.send(method) do |req|
            set_request_headers! req, url
          end

          # Handle rate limiting
          return handle_rate_limiting(method, url) if response.status == 429

          fail_or_response(response)
        end
      end

      # @param [String] url - URL for HTTP POST / PUT request
      # @param [Params] params - Data to be sent in request body
      # @return [Faraday::Response] A response, with .headers, .status & .body
      %i[post put].each do |method|
        define_method method do |url, params|
          response = @connection.send(method) do |req|
            set_request_headers! req, url
            req.body = JSON.generate params
          end

          # Handle rate limiting
          if response.status == 429
            return handle_rate_limiting(method, url, params)
          end

          fail_or_response(response)
        end
      end

      private

      # @param [String] method
      # @param [String] url
      # @param [Hash] params
      def handle_rate_limiting(method, url, params = nil)
        sleep(Zuora::RETRY_WAITING_PERIOD)
        if params.present?
          send(method, url, params)
        else
          send(method, url)
        end
      end


      # @param [Faraday::Response] response
      # @throw [ErrorResponse] if unsuccessful
      # @return [Faraday::Response]
      def fail_or_response(response)
        if response.status != 200
          raise(ErrorResponse.new("HTTP Status #{response.status}", response))
        elsif response.body.kind_of?(Array)
          if !response.body.first['Success']
            message = parse_legacy_error(response.body.first)
            raise(ErrorResponse.new(message, response))
          end
        elsif response.body.kind_of?(Hash)
          if response.body.key?('success') && !response.body['success']
            message = parse_error(response.body)
            raise(ErrorResponse.new(message, response))
          elsif response.body.key?('Success') && !response.body['Success']
            message = parse_legacy_error(response.body)
            raise(ErrorResponse.new(message, response))
          end
        end
        response
      end

      # For all REST API endpoints that are 2017+
      # @param [Hash] body - the structure containing errors
      # @return [String] message - human readable error message in string format
      def parse_error(body)
        message = ""
        if body['reasons']
          reasons = body['reasons'].map do |reason|
            "Error #{reason['code']}: #{reason['message']}"
          end
          message += reasons.join(', ')
        end
        message
      end

      # For all REST API endpoints that use the SOAP api behind the scenes,
      # such as ones starting with /v1/object/, the error formatting is different
      # @param [Hash] body - the structure containing errors
      # @return [String] human readable error message in string format
      def parse_legacy_error(body)
        message = ""
        if body['Errors']
          errors = body['Errors'].map do |error|
            "Error #{error['Code']}: #{error['Message']}"
          end
          message += errors.join(', ')
        end
        message
      end

      # @param [Faraday::Request] req - Faraday::Request builder
      # @param [String] username
      # @param [String] password
      def set_auth_request_headers!(req, username, password)
        if use_modern_rest?
          req.url '/v1/connections'
        else
          req.url '/rest/v1/connections'
        end
        req.headers['apiAccessKeyId'] = username
        req.headers['apiSecretAccessKey'] = password
        req.headers['Content-Type'] = 'application/json'
      end

      # @param [Faraday::Request] request - Faraday Request builder
      # @param [String] url - Relative URL for HTTP request
      def set_request_headers!(request, url)
        request.url url
        request.headers['Content-Type'] = 'application/json'
        request.headers['Cookie'] = @auth_cookie

        if ENV['ZUORA_API_VERSION'].present?
          request.headers['zuora-version'] = ENV['ZUORA_API_VERSION']
        end
      end

      # @param [String] url
      # @return [Faraday::Client]
      def connection(url)
        Faraday.new(url, ssl: { verify: true }) do |conn|
          conn.request :json
          conn.response :json, content_type: /\bjson$/
          conn.use :instrumentation
          conn.adapter Faraday.default_adapter
        end
      end

      # @param [Boolean] sandbox - Use the sandbox url?
      # @return [String] url
      def api_url(sandbox)
        if use_modern_rest?
          sandbox ? Zuora::Rest::NEWEST_SANDBOX_URL : Zuora::Rest::NEWEST_API_URL
        else
          sandbox ? Zuora::Rest::SANDBOX_URL : Zuora::Rest::API_URL
        end
      end

      # @return [Boolean]
      def use_modern_rest?
        @modern_endpoint == true
      end
    end
  end
end