Abstractions to help author API wrappers in Ruby.
- Add
api_client_base
as a dependency to your gem's gemspec. require 'api_client_base'
inlib/yourgem.rb
This gem assumes your gem will follow a certain structure.
- Actions that your gem can perform are done through a class, like
APIWrapper::Client
. - Each action has request and response classes.
- Request class takes care of appending the endpoint's path to the host, preparing params, and specifying the right http method to call
- Response class takes care of parsing the response and making the data easily accessible for consumption.
Do this:
module MyGem
include APIClientBase::Base.module
with_configuration do
has :host, classes: String, default: "https://production.com"
has :username, classes: String
has :password, classes: String
end
end
And you can
- configure (thanks to gem_config) the gem's base module with defaults:
MyGem.configure do |c|
c.host = "https://test.api.com"
end
Note: there is a default configuration setting called after_response
. See the "Response hooks" section for more details.
- instantiate
MyGem::Client
by callingMyGem.new(host: "https://api.com", username: "user", password: "password")
. If you do not specify an option, it will use the gem's default.
Given this config:
module MyGem
include APIClientBase::Base.module
with_configuration do
has :host, classes: String, default: "https://production.com"
has :username, classes: String
has :password, classes: String
end
end
Configure the Client
like this:
module MyGem
class Client
# specifying a symbol for `default_opts` will look for a method of that name
# and pass that into the request
include APIClientBase::Client.module(default_opts: :default_opts)
private
def default_opts
# Pass along all the things your requests need.
# The methods `host`, `username`, and `password` are available
# to your `Client` instance because it inherited these from the configuration.
{ host: host, username: username, password: password }
end
end
end
module MyGem
class Client
include APIClientBase::Client.module(default_opts: :all_opts)
api_action :get_user
# `api_action` basically creates a method like:
# def get_user(opts={})
# request = GetUserRequest.new(all_opts.merge(opts))
# raw_response = request.()
# GetUserResponse.new(raw_response: raw_response)
# end
private
def all_opts
{ host: "http://prod.com" }
end
end
end
api_action
accepts the method name, and optional hash of arguments. These may contain:args
: if not defined, the args expected by the method is nothing, or a hash. If given, it must be an array of symbols. For example, ifargs: [:id, :name]
is given, then the method defined is effectively:def my_action(id, name)
but what is passed into the request object is still{id: "id-value", name: "name-value"}
You still need to create MyGem::GetUserRequest
and MyGem::GetUserResponse
. See the "requests" and "responses" section below.
Requests assume a REST-like structure. This currently does not play well with a SOAP server. You could still use the Base
, Client
, and Response
modules however. For SOAP APIs, write your own Request class that defines #call
. This method needs to return the raw_response
.
module MyGem
class GetUserRequest
# You must install typhoeus if you use the `APIClientBase::Request`. Add it to your gemfile.
include APIClientBase::Request.module(
# you may define the action by `action: :post`. Defaults to `:get`.
# You may also opt to define `#default_action` (see below)
)
private
def path
# all occurrences of `/:\w+/` in the string will have the matches replaced with the
# request object's value of that method. For example, if `request.user_id` is 33
# then you will get "/api/v1/users/33" as the path
"/api/v1/users/:user_id"
# Or you can interpolate it yourself if you want
# "/api/v1/users/#{self.user_id}"
end
# Following methods are optional. Override them if you need to send something specific
def headers
{"Content-Type" => "application/json"}
end
def body
{secret: "my-secret"}.to_json
end
def params
{my: "params"}
end
def default_action
:post # defaults to :get
end
def before_call
# You need not define this, but it might be helpful if you want to log things, for example, before the http request is executed.
# The following example would require you to define a `logger` attribute
self.logger.debug "Will make a #{action} call: #{body}"
end
end
end
APIClientBase supports validation so that your calls fail early before even hitting the server. The validation library that this supports (by default, and the only one) is dry-validations.
Given this request:
module MyGem
class GetUserRequest
attribute :user_id, Integer
end
end
Create a dry-validations schema following this pattern:
module MyGem
GetUserRequestSchema = Dry::Validation.Schema do
required(:user_id).filled(:int?)
end
end
This will raise an error when you call the method:
client = MyGem::Client.new #...
client.get_user(user_id: nil) # -> this raises an ArgumentError "[user_id: 'must be filled']"
Requests automatically have proxy
. If set and you are relying on using Typhoeus, proxy will be passed on and used by Typhoeus.
module MyGem
class GetUserResponse
include APIClientBase::Response.module
# - has `#status` method that is delegated to `raw_response.status`
# - has `#code` method to get the response's code
# - has `#raw_response` which is a Typhoeus response object
# - has `#success` which is delegated to `raw_response.success?`. May be accessed via `#success?`
# You're encouraged to use Virtus attributes to extract information cleanly
attribute :id, Integer, lazy: true, default: :default_id
attribute :name, String, lazy: true, default: :default_name
attribute :body, Object, lazy: true, default: :default_body
private
# Optional: define `#default_success` to determine what it means for
# the response to be successful. For example, the code might be 200
# but you consider it a failed call if there are no results
def default_success
code == 200 && !body[:users].empty?
end
def default_body
JSON.parse(raw_response.body).with_indifferent_access
end
def default_id
body[:id]
end
def default_name
body[:name]
end
end
end
You can an example gem here.
If you want to give the applications access to the request and response objects after each response (useful when tracking rate limits that are reported in the response, for example), then:
MyGem.configure do |c|
c.after_request = ->(request, response) do
if response.header("remaining-requets") < 20
Rails.logger.warn "mayday!"
end
end
end
Note: the request and response objects are the request and response instances such as:
GetUserResponse
GetUserRequest
You can assign any object that response to call
:
class AfterMyGemResponse
def self.call(request, response)
end
end
Note: avoid putting long running/expensive requests here because this will block the Ruby process.
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 tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/bloom-solutions/api_client-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.