Active Call - Api
Active Call - API is an extension of Active Call that provides a standardized way to build service objects for REST API endpoints.
Before proceeding, please review the Active Call Usage section. It takes just 55 seconds.
Installation
Install the gem and add to the application's Gemfile by executing:
bundle add active_call-api
If bundler is not being used to manage dependencies, install the gem by executing:
gem install active_call-api
Usage
Set up an ActiveCall::Base base service class for the REST API and include ActiveCall::Api.
require 'active_call' require 'active_call/api' class YourGem::BaseService < ActiveCall::Base include ActiveCall::Api self.abstract_class = true ...
Implement a connection method to hold a Faraday::Connection object.
This connection instance will then be used in the call methods of the individual service objects.
class YourGem::BaseService < ActiveCall::Base ... config_accessor :api_key, default: ENV['API_KEY'], instance_writer: false config_accessor :logger, default: Logger.new($stdout), instance_writer: false def connection @_connection ||= Faraday.new do |conn| conn.url_prefix = 'https://example.com/api/v1' conn.request :authorization, 'X-API-Key', api_key conn.request :json conn.response :json conn.response :logger, logger, formatter: Faraday::Logging::ColorFormatter, prefix: { request: 'YourGem', response: 'YourGem' } do |logger| logger.filter(/(Authorization:).*"(.+)."/i, '\1 [FILTERED]') end conn.adapter Faraday.default_adapter end end ...
You can now create a REST API service object like so.
class YourGem::SomeResource::UpdateService < YourGem::BaseService attr_reader :id, :first_name, :last_name validates :id, :first_name, :last_name, presence: true def initialize(id:, first_name:, last_name:) @id = id @first_name = first_name @last_name = last_name end # PUT /api/v1/someresource/:id def call connection.put("someresource/#{id}", first_name: first_name, last_name: last_name) end end
Using call
service = YourGem::SomeResource::UpdateService.call(id: '1', first_name: 'Stan', last_name: 'Marsh') service.success? # => true service.errors # => #<ActiveModel::Errors []> service.response # => #<Faraday::Response ...> service.response.status # => 200 service.response.body # => {}
Using call!
begin service = YourGem::SomeResource::UpdateService.call!(id: '1', first_name: 'Stan', last_name: '') rescue ActiveCall::ValidationError => exception exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=last_name, type=blank, options={}>]> exception.errors.full_messages # => ["Last name can't be blank"] rescue ActiveCall::UnprocessableEntityError => exception exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=unprocessable_entity, options={}>]> exception.errors.full_messages # => ["Unprocessable Entity"] exception.response # => #<Faraday::Response ...> exception.response.status # => 422 exception.response.body # => {} end
Errors
The following exceptions will get raised when using call! and the request was unsuccessful.
| HTTP Status Code | Exception Class |
|---|---|
| 4xx | ActiveCall::ClientError |
| 400 | ActiveCall::BadRequestError |
| 401 | ActiveCall::UnauthorizedError |
| 403 | ActiveCall::ForbiddenError |
| 404 | ActiveCall::NotFoundError |
| 406 | ActiveCall::NotAcceptableError |
| 407 | ActiveCall::ProxyAuthenticationRequiredError |
| 408 | ActiveCall::RequestTimeoutError |
| 409 | ActiveCall::ConflictError |
| 410 | ActiveCall::GoneError |
| 422 | ActiveCall::UnprocessableEntityError |
| 429 | ActiveCall::TooManyRequestsError |
| 5xx | ActiveCall::ServerError |
| 500 | ActiveCall::InternalServerError |
| 501 | ActiveCall::NotImplementedError |
| 502 | ActiveCall::BadGatewayError |
| 503 | ActiveCall::ServiceUnavailableError |
| 504 | ActiveCall::GatewayTimeoutError |
400..499 errors are subclasses of ActiveCall::ClientError.
500..599 errors are subclasses of ActiveCall::ServerError.
For any explicit HTTP status code not listed here, an ActiveCall::ClientError exception gets raised for 4xx HTTP status codes and an ActiveCall::ServerError exception for 5xx HTTP status codes.
Custom Exception Classes
If you want to use your error classes instead, override the exception_mapping class method.
class YourGem::BaseService < ActiveCall::Base ... class << self def exception_mapping { validation_error: YourGem::ValidationError, request_error: YourGem::RequestError, client_error: YourGem::ClientError, server_error: YourGem::ServerError, bad_request: YourGem::BadRequestError, unauthorized: YourGem::UnauthorizedError, forbidden: YourGem::ForbiddenError, not_found: YourGem::NotFoundError, not_acceptable: YourGem::NotAcceptableError, proxy_authentication_required: YourGem::ProxyAuthenticationRequiredError, request_timeout: YourGem::RequestTimeoutError, conflict: YourGem::ConflictError, gone: YourGem::GoneError, unprocessable_entity: YourGem::UnprocessableEntityError, too_many_requests: YourGem::TooManyRequestsError, internal_server_error: YourGem::InternalServerError, not_implemented: YourGem::NotImplementedError, bad_gateway: YourGem::BadGatewayError, service_unavailable: YourGem::ServiceUnavailableError, gateway_timeout: YourGem::GatewayTimeoutError }.freeze end end ...
Error Types
The methods below determine what type of error gets added to the errors object.
service.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=bad_request, options={}>]>
When using .call!, they map to the exception_mapping above, so bad_request? maps to bad_request.
exception.errors # => #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=bad_request, options={}>]>
class YourGem::BaseService < ActiveCall::Base ... def bad_request? response.status == 400 end def unauthorized? response.status == 401 end def forbidden? response.status == 403 end def not_found? response.status == 404 end def not_acceptable? response.status == 406 end def proxy_authentication_required? response.status == 407 end def request_timeout? response.status == 408 end def conflict? response.status == 409 end def gone? response.status == 410 end def unprocessable_entity? response.status == 422 end def too_many_requests? response.status == 429 end def internal_server_error? response.status == 500 end def not_implemented? response.status == 501 end def bad_gateway? response.status == 502 end def service_unavailable? response.status == 503 end def gateway_timeout? response.status == 504 end
These methods can be overridden to add more rules when an API does not respond with the relevant HTTP status code.
A common occurrence is when an API returns an HTTP status code of 400 with an error message in the body for anything related to client errors, sometimes even for a resource that could not be found.
It is not required to override any of these methods since all 4xx and 5xx errors add a client_error or server_error type to the errors object, respectively.
While not required, handling specific errors based on their actual meaning makes for a happier development experience.
You have access to the full Farady::Response object set to the response attribute, so you can use response.status and response.body to determine the type of error.
Perhaps the API does not always respond with a 422 HTTP status code for unprocessable entity requests or a 404 HTTP status for resources not found.
class YourGem::BaseService < ActiveCall::Base ... def not_found? response.status == 404 || (response.status == 400 && response.body['error_code'] == 'not_found') end def unprocessable_entity? response.status == 422 || (response.status == 400 && response.body['error_code'] == 'not_processable') end
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/activecall/active_call-api.
License
The gem is available as open source under the terms of the MIT License.