api-response-presenter

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:
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:
ApiResponse.config.some_option = 'some_value'
ApiResponse.configure do |config|
config.adapter = :faraday
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'
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
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
private def with_presentation(response, **, &)
ApiResponse::Presenter.call(response, **, &)
end
end
class ExtenalApiService < ExternalApiBaseClass
...
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.
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')
ApiResponse::Presenter.call(response) do |config|
config.adapter = :rest_client
config.monad = true
config.struct = Response
config.default_status = 400
config.symbol_status = true
config.default_error = 'ExternalApiCaller api error'
end
end
end
def MyController
def index
result = ExternalApiCaller.call
if result.success?
render json: result.success
else
render json: {error: result.failure[:error]}, status: result.failure[:status]
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)
response = Faraday.get('https://api.example.com/data', params)
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
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
end
end
or with Dry::Initializer
require 'dry/initializer'
class MyClass
extend Dry::Initializer
option :response
option :config, default: -> { ApiResponse.config }
def call
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))
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)
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)
Contributing
Bug reports and pull requests are welcome on GitHub
License
See LICENSE
file.