APIClientBase

Abstractions to help author API wrappers in Ruby.

Installation

  • Add api_client_base as a dependency to your gem's gemspec.
  • require 'api_client_base' in lib/yourgem.rb

Usage

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.

Configuring the gem's base module

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 calling MyGem.new(host: "https://api.com", username: "user", password: "password"). If you do not specify an option, it will use the gem's default.

Configuring the Client

Default Options

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

Actions

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, if args: [: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

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
Validation

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']"
Proxy

Requests automatically have proxy. If set and you are relying on using Typhoeus, proxy will be passed on and used by Typhoeus.

Responses

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.

Response hooks

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.

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 tags, and push the .gem file to rubygems.org.

Contributing

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.

License

The gem is available as open source under the terms of the MIT License.