
Research
/Security News
Weaponizing Discord for Command and Control Across npm, PyPI, and RubyGems.org
Socket researchers uncover how threat actors weaponize Discord across the npm, PyPI, and RubyGems ecosystems to exfiltrate sensitive data.
api-response-presenter
Advanced tools
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.
This library oficially supports the following Ruby versions:
>=2.7.4
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
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).
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
ApiResponse.config.adapter
: response adapter that you are using.
:faraday
.: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
false
ApiResponse::Presenter.call(response, monad: true) # => Success({})
or Failure({error:, status:, error_key:})
ApiResponse::Presenter.call
with monad: true, you should use #success?
and #failure?
methods to check resultApiResponse.config.monad = true
:
ApiResponse.config.default_status
default status for ApiResponse::Presenter.call
if response is not success. You can provide symbol or integer.
:conflict
ApiResponse.config.symbol_status
option for symbolize status from response (or default status if it an Integer).
true
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
:external_api_error
ApiResponse.config.default_error
default error message for ApiResponse::Presenter.call
if response is not success
'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.
->(body) { body }
.->(b) { b.first }
, ->(b) { b.slice(:id, :name) }
, -> (b) { b.deep_stringify_keys )}
ApiResponse.config.struct
struct for pack your extracted value from body.
nil
MyAwesomeStruct.new(**attrs)
, not Struct.new(attrs)
)ApiResponse.config.raw_response
returns raw response, that you passes into class.
false
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)
false
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
nil
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.
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
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
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.
ApiResponse::Processor::Success
# contains logic for success response (status/code 100-399)ApiResponse::Processor::Failure
# contains logic for failure response (status/code 400-599)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
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
Bug reports and pull requests are welcome on GitHub
See LICENSE
file.
FAQs
Unknown package
We found that api-response-presenter demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Research
/Security News
Socket researchers uncover how threat actors weaponize Discord across the npm, PyPI, and RubyGems ecosystems to exfiltrate sensitive data.
Security News
Socket now integrates with Bun 1.3’s Security Scanner API to block risky packages at install time and enforce your organization’s policies in local dev and CI.
Research
The Socket Threat Research Team is tracking weekly intrusions into the npm registry that follow a repeatable adversarial playbook used by North Korean state-sponsored actors.