Wrappi
Framework to create API clients. The intention is to bring the best practices and standarize the mess it's currently happening with the API clients. It allows to create API clients in a declarative way improving readability and unifying the behavior.
Installation
Add this line to your application's Gemfile:
gem 'wrappi'
And then execute:
$ bundle
Or install it yourself as:
$ gem install wrappi
Usage
Github example:
module Github
class Client < Wrappi::Client
setup do |config|
config.domain = 'https://api.github.com'
config.headers = {
'Content-Type' => 'application/json',
'Accept' => 'application/vnd.github.v3+json',
}
end
end
class User < Wrappi::Endpoint
client Client
verb :get
path "users/:username"
end
end
user = Github::User.new(username: 'arturictus')
user.success? # => true
user.error? # => false
user.status_code # => 200
user.body # => {"login"=>"arturictus", "id"=>1930175, ...}
Configurations
Client
| Name | Type | Default | Required |
|---|---|---|---|
| domain | String | * | |
| params | Hash | ||
| logger | Logger | Logger.new(STDOUT) | |
| headers | Hash | { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } | |
| ssl_context | OpenSSL::SSL::SSLContext | ||
| use_ssl_context | Boolean | false |
Endpoint
| Name | Type | Default | Required |
|---|---|---|---|
| client | Wrappi::Client | * | |
| path | String | * | |
| verb | Symbol | :get | * |
| default_params | Hash | {} | |
| headers | block | proc { client.headers } | |
| basic_auth | Hash, keys: user, pass | ||
| follow_redirects | Boolean | true | |
| body_type | Symbol, one of: :json,:form,:body | :json | |
| cache | Boolean | false | |
| retry_if | block | ||
| retry_options | block | ||
| around_request | block |
Client
Is the main configuration for your service.
It holds the common configuration for all the endpoints (Wrappi::Endpoint).
Required:
- domain: Yep, you know.
ruby config.domain = 'https://api.github.com'
Optionals:
params: Set global params for all the
Endpoints. This is a great place to put theapi_key.config.params = { "api_key" => "asdfasdfoerkwlejrwer" }default:
{}logger: Set your logger.
default:
Logger.new(STDOUT)config.logger = Rails.loggerheaders: Headers for all the endpoints. Format, Authentication.
default:
{ 'Content-Type' => 'application/json', 'Accept' => 'application/json' }config.headers = { "Content-Type" => "application/json", "Accept' => 'application/json", "Auth-Token" => "verysecret" }ssl_context: If you need to set an ssl_context.
default:
nilconfig.ssl_context = OpenSSL::SSL::SSLContext.new.tap do |ctx| ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE enduse_ssl_context: It has to be set to
truefor using thessl_contextdefault:
false
Endpoint
Required:
client:
Wrappi::Clientclassclient MyClientpath: The path to the resource. You can use doted notation and they will be interpolated with the params
class MyEndpoint < Wrappi::Endpoint client MyClient verb :get path "/users/:id" end endpoint = MyEndpoint.new(id: "the_id", other: "foo") endpoint.url_with_params #=> "http://domain.com/users/the_id?other=foo" endpoint.url #=> "http://domain.com/users/the_id" endpoint.consummated_params #=> {"other"=>"foo"}Notice how interpolated params are removed from the query or the body
verb:
default:
:get:get:post:delete:put
Optional:
default_params: Default params for the request. This params will be added to all the instances unless you override them.
default:
{}class MyEndpoint < Wrappi::Endpoint client MyClient verb :get path "/users/:id" default_params do { other: "bar", foo: "foo" } end end endpoint = MyEndpoint.new(id: "the_id", other: "foo") endpoint.consummated_params #=> {"other"=>"foo","foo" => "foo" }headers: You can modify the client headers here. Notice that if you want to use the client headers as well you will have to merge them.
default:
proc { client.headers }class MyEndpoint < Wrappi::Endpoint client MyClient verb :get path "/users" headers do client.headers #=> { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } client.headers.merge('Agent' => 'wrappi') end end endpoint = MyEndpoint.new() endpoint.headers #=> { 'Agent' => 'wrappi', 'Content-Type' => 'application/json', 'Accept' => 'application/json'}basic_auth: If your endpoint requires basic_auth here is the place. keys have to be:
userandpass.default:
nilbasic_auth do { user: 'wrappi', pass: 'secret'} endfollow_redirects: If first request responds a redirect it will follow them.
default:
truebody_type: Body type.
default:
:json- :json
- :form
- :body (Binary data)
Flow Control:
This configs allows you fine tune your request adding middleware, retries and cache. The are executed in this nested stack:
cache
|- retry
|- around_request
Check specs for more examples.
cache: Cache the request if successful.
default:
falseretry_if: Block to evaluate if request has to be retried. In the block are yielded
ResponseandEndpointinstances. If the block returnstruethe request will be retried.retry_if do |response, endpoint| endpoint.class #=> MyEndpoint response.error? # => true or false endUse case:
We have a service that returns an aggregation of hotels available to book for a city. The service will start the aggregation in the background and will return
200if the aggregation is completed if the aggregation is not completed will return201making us know that we should call again to retrieve all the data. This behavior only occurs if we pass the param:onlyIfComplete.retry_if do |response, endpoint| endpoint.consummated_params["onlyIfComplete"] && response.status_code == 201 endNotice that this block will never be executed if an error occur (like timeouts). For retrying on errors use the
retry_optionsretry_options: We are using the great gem retryable to accomplish this behavior. Check the documentation for fine tuning. I just paste some examples for convenience.
do
{ tries: 5, on: [ArgumentError, Wrappi::TimeoutError] } # or
{ tries: :infinite, sleep: 0 }
end
- around_request: This block is executed surrounding the request. The request
will only get executed if you call
request.call.ruby around_request do |request, endpoint| endpoint.logger.info("making a request to #{endpoint.url} with params: #{endpoint.consummated_params}") request.call # IMPORTANT endpoint.logger.info("response status is: #{request.status_code}") end
Development
After checking out the repo, run bin/setup to install dependencies.
Run test:
bin/dev_server
This will run a rails server. The test are running agains it.
bundle exec rspec
You can also run bin/console for an interactive prompt that will allow you to experiment.
Docker
Run dummy server with docker:
docker build -t wrappi/dummy -f spec/dummy/Dockerfile .
docker run -d -p 127.0.0.1:9873:9873 wrappy/dummy /bin/sh -c "bin/rails server -b 0.0.0.0 -p 9873"
Try:
curl 127.0.0.1:9873 #=> {"controller":"pages","action":"show_body"}
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/arturictus/wrappi. 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.