Introducing Socket Firewall: Free, Proactive Protection for Your Software Supply Chain.Learn More
Socket
Book a DemoInstallSign in
Socket

api-response-presenter

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

api-response-presenter

bundlerRubygems
Version
0.0.2
Version published
Maintainers
1
Created
Source

api-response-presenter

Gem Version Build Status Coverage Status Inline docs

The api-response-presenter gem provides a flexible and easy-to-use interface for presenting API responses using Faraday or RestClient with the possibility to configure global settings or per-instance settings. It leverages the Dry::Configurable for configurations, ensuring high performance and full test coverage.

Supported Ruby Versions

This library oficially supports the following Ruby versions:

  • MRI >=2.7.4

Installation

Add this line to your application's Gemfile:

gem 'api-response-presenter'

And then execute:

bundle install

Or install it yourself as:

gem install api-response-presenter

Usage

Configuration

You can configure api-response-presenter globally in an initializer or setup block:

# config/initializers/api_response.rb

ApiResponse.config.some_option = 'some_value'

# or

ApiResponse.configure do |config|
  config.adapter = :faraday # or :rest_client, :excon, :http
  config.monad = false
  config.extract_from_body = ->(body) { body }
  config.struct = nil
  config.raw_response = false
  config.error_json = false
  config.default_return_value = nil
  config.default_status = :conflict
  config.default_error_key = :external_api_error
  config.default_error = 'External Api error'

  # dependency injection
  config.success_processor = ApiResponse::Processor::Success
  config.failure_processor = ApiResponse::Processor::Failure
  config.parser = ApiResponse::Parser
  config.options = {}
end

or on instance config, provide block (see: BasicUsage).

Basic Example

Here is a basic example of using api_response to process an API response:

response = Faraday.get('https://api.example.com/data')
result = ApiResponse::Presenter.call(response) do |config|
  config.monad = true
end

# or 
# Usefull for using in another libraries
result ||= ApiResponse::Presenter.call(response, monad: true)

if result.success?
  puts "Success: #{result.success}"
else
  puts "Error: #{result.failure}"
end

Also you can create decorator for using functionality e.g.

module ApiResponseHandler # or ExternalApiBaseClass
  private def with_presentation(response, **, &)
    ApiResponse::Presenter.call(response, **, &)
  end
end

class ExtenalApiService < ExternalApiBaseClass
  # or include ApiResponseHandler

  ...

  def get_external_data(*, **, &)
    response = get('/data', *)

    with_presentation(response, **, &)
  end
  
  ...
end

Config options

  • ApiResponse.config.adapter: response adapter that you are using.
    • Default: :faraday.
    • Available values: :rest_client, :excon, :http and others. Checks that response respond to #status (only Faraday and Excon) or #code (others)
  • ApiResponse.config.monad wrap result into dry-monads
    • Default: false
    • Example: ApiResponse::Presenter.call(response, monad: true) # => Success({}) or Failure({error:, status:, error_key:})
    • Note: if you use ApiResponse::Presenter.call with monad: true, you should use #success? and #failure? methods to check result
    • Options only for ApiResponse.config.monad = true:
      • ApiResponse.config.default_status default status for ApiResponse::Presenter.call if response is not success. You can provide symbol or integer.
        • Default: :conflict
      • ApiResponse.config.symbol_status option for symbolize status from response (or default status if it an Integer).
        • Default: true
        • Example: ApiResponse::Presenter.call(response, monad: true, default_status: 500, symbol_status: false) # => Failure({error:, status: 500, error_key:})
      • ApiResponse.config.default_error_key default error key for ApiResponse::Presenter.call if response is not success
        • Default: :external_api_error
      • ApiResponse.config.default_error default error message for ApiResponse::Presenter.call if response is not success
        • Default: 'External Api error'
  • ApiResponse.config.extract_from_body procedure that is applied to the response.body after it has been parsed from JSON string to Ruby hash with symbolize keys.
    • Default: ->(body) { body }.
    • Example lambdas: ->(b) { b.first }, ->(b) { b.slice(:id, :name) }, -> (b) { b.deep_stringify_keys )}
  • ApiResponse.config.struct struct for pack your extracted value from body.
    • Default: nil
    • Note: packing only into classes with key value constructors (e.g. MyAwesomeStruct.new(**attrs), not Struct.new(attrs))
    • Recommend to use dry-struct or Ruby#OpenStruct
  • ApiResponse.config.raw_response returns raw response, that you passes into class.
    • Default: false
    • Example: ApiResponse::Presenter.call(Faraday::Response<...>, raw_response: true) # => Faraday::Response<...>
  • ApiResponse.config.error_json returns error message from response body if it is JSON (parsed with symbolize keys)
    • Default: false
    • Example: ApiResponse::Presenter.call(Response<body: "{\"error\": \"some_error\"}">, error_json: true) # => {error: "some_error"}
  • ApiResponse.config.default_return_value default value for ApiResponse::Presenter.call if response is not success
    • Default: nil
    • Example: ApiResponse::Presenter.call(response, default_return_value: []) # => []

NOTE: You can override global settings on instance config, provide block (see: BasicUsage). Params options has higher priority than global settings and block settings.

Examples:

Interactors:

class ExternalApiCaller < ApplicationInteractor
  class Response < Dry::Struct
    attribute :data, Types::Array
  end
  
  def call
    response = RestClient.get('https://api.example.com/data') # => body: "{\"data\": [{\"id\": 1, \"name\": \"John\"}]}"
    ApiResponse::Presenter.call(response) do |config|
      config.adapter = :rest_client
      config.monad = true
      config.struct = Response
      config.default_status = 400 # no matter what status came in fact
      config.symbol_status = true # return :bad_request instead of 400
      config.default_error = 'ExternalApiCaller api error' # instead of response error field (e.g. body[:error])
    end
  end
end

def MyController
  def index
    result = ExternalApiCaller.call
    if result.success?
      render json: result.success # => ExternalApiCaller::Response<data: [{id: 1, name: "John"}]> => {data: [{id: 1, name: "John"}]}
    else
      render json: {error: result.failure[:error]}, status: result.failure[:status] # => {error: "ExternalApiCaller api error"}, status: 400
    end
  end
end

ExternalApi services


class EmployeeApiService
  class Employee < Dry::Struct
    attribute :id, Types::Integer
    attribute :name, Types::String
  end

  def self.get_employees(monad: false, adapter: :faraday, **options)
    # or (params, presenter_options = {})
    response = Faraday.get('https://api.example.com/data', params) # => body: "{\"data\": [{\"id\": 1, \"name\": \"John\"}]}"

    page = options.fetch(:page, 1)
    per = options.fetch(:per, 5)

    ApiResponse::Presenter.call(response, monad: monad, adapter: adapter) do |c|  
      c.extract_from_body = ->(body) { Kaminari.paginate_array(body[:data]).page(page).per(per) }
      c.struct = Employee
      c.default_return_value = []
    end
  end
end

class MyController
  def index
    employees = EmployeeApiService.get_employees(page: 1, per: 5)
    if employees.any?
      render json: employees # => [Employee<id: 1, name: "John">] => [{id: 1, name: "John"}]
    else
      render json: {error: 'No employees found'}, status: 404
    end
  end
end

Customization

Processors

You can customize the response processing by providing a block to ApiResponse::Presenter.call or redefine global processors and parser: All of them must implement .new(response, config: ApiResponse.config).call method. You can use not default config in your processor, just pass it as a second named argument.

  • Redefine ApiResponse::Processor::Success # contains logic for success response (status/code 100-399)
  • Redefine ApiResponse::Processor::Failure # contains logic for failure response (status/code 400-599)
  • Redefine ApiResponse::Parser # contains logic for parsing response body (e.g. Oj.load(response.body))
class MyClass
  def initialize(response, config: ApiResponse.config)
    @response = response
    @config = config
  end
  
  def call
    # your custom logic
  end
end

or with Dry::Initializer

require 'dry/initializer'

class MyClass
  extend Dry::Initializer
  option :response
  option :config, default: -> { ApiResponse.config }
  
  def call
    # your custom logic
  end
end

You can use your custom processor or parser in ApiResponse::Presenter.call or redefine in global settings:

ApiResponse.config.success_processor = MyClass
ApiResponse.config.failure_processor = MyClass
ApiResponse.config.parser = MyClass

or

ApiResponse::Presenter.call(response, success_processor: MyClass, failure_processor: MyClass, parser: MyClass)

NOTE: If you are using Faraday with Oj middleware to parse json body already, you should redefine parser like this (in next gem version will be available configuring parsing (on/off))

# config/initializers/api_response.rb

require 'api_response'

class EmptyParser
  attr_reader :response, :config

  def initialize(response, config: nil)
    @response = response
    @config = config
  end

  def call
    response.body
  end
end

ApiResponse.configure do |config|
  config.parser = EmptyParser
end

Options

Also you can add custom options to ApiResponse.config.options = {} and use it in your processor or parser:

ApiResponse.config do |config| 
  config.options[:my_option] = 'my_value'
  config.options[:my_another_option] = 'my_another_value'
end

or

ApiResponse::Presenter.call(response, success_processor: MyClass, options: {my_option: 'my_value', my_another_option: 'my_another_value'})

Example:


class MyCustomParser
  attr_reader :response, :config

  def initialize(response, config: ApiResponse.config)
    @response = response
    @config = config
  end

  def call
    JSON.parse(response.body, symbolize_names: true) # or Oj.load(response.body, symbol_keys: true)
  rescue JSON::ParserError => e
    raise ::ParseError.new(e) if config.options[:raise_on_failure]
    response.body
  end
end

class MyCustomFailureProcessor
  class BadRequestError < StandardError; end

  attr_reader :response, :config

  def initialize(response, config: ApiResponse.config)
    @response = response
    @config = config
  end

  def call
    parsed_body = config.parser.new(response).call
    raise BadRequestError.new(parsed_body) if config.options[:raise_on_failure]

    {error: parsed_body, status: response.status || config.default_status, error_key: :external_api_error}
  end
end

ApiResponse.config do |config|
  config.failure_processor = MyCustomFailureProcessor
  config.parser = MyCustomParser
  config.options[:raise_on_failure] = true
end

response = Faraday.get('https://api.example.com/endpoint_that_will_fail')
ApiResponse::Presenter.call(response) # => raise BadRequestError

Contributing

Bug reports and pull requests are welcome on GitHub

License

See LICENSE file.

FAQs

Package last updated on 26 Mar 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts