# 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: ```ruby 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](https://github.com/krautcomputing/gem_config)) the gem's base module with defaults: ```ruby 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: ```ruby 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: ```ruby 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 ```ruby 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`. ```ruby 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: ```ruby module MyGem class GetUserRequest attribute :user_id, Integer end end ``` Create a dry-validations schema following this pattern: ```ruby 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 ```ruby 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](https://github.com/bloom-solutions/binance_client-ruby). #### 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: ```ruby 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`: ```ruby 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](https://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](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).