api-response-presenter
The api-response-presenter
gem provides a flexible and easy-to-use interface for processing 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 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. = {}
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
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)
- Default:
ApiResponse.config.monad
wrap result into dry-monads- Default:
false
- Example:
ApiResponse::Presenter.call(response, monad: true) # => Success({})
orFailure({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 forApiResponse::Presenter.call
if response is not success. You can provide symbol or integer.- Default:
:conflict
- Default:
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:})
- Default:
ApiResponse.config.default_error_key
default error key forApiResponse::Presenter.call
if response is not success- Default:
:external_api_error
- Default:
ApiResponse.config.default_error
default error message forApiResponse::Presenter.call
if response is not success- Default:
'External Api error'
- Default:
- Default:
ApiResponse.config.extract_from_body
procedure that is applied to theresponse.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 )}
- Default:
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)
, notStruct.new(attrs)
) - Recommend to use dry-struct or Ruby#OpenStruct
- Default:
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<...>
- Default:
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"}
- Default:
ApiResponse.config.default_return_value
default value forApiResponse::Presenter.call
if response is not success- Default:
nil
- Example:
ApiResponse::Presenter.call(response, default_return_value: []) # => []
- Default:
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, **)
# or (params, presenter_options = {})
response = Faraday.get('https://api.example.com/data', params) # => body: "{\"data\": [{\"id\": 1, \"name\": \"John\"}]}"
ApiResponse::Presenter.call(response, monad: monad, adapter: adapter) do |c|
c.extract_from_body = ->(body) { Kaminari.paginate_array(body[:data]).page(1).per(5) }
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)
Options
Also you can add custom options to ApiResponse.config.options = {}
and use it in your processor or parser:
ApiResponse.config do |config|
config.[:my_option] = 'my_value'
config.[: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.[: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.[: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.[: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.