iriberri / wrappi

Framework to create API clients

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Build Status Maintainability Test Coverage

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.
    config.domain = 'https://api.github.com'

Optionals:

  • params: Set global params for all the Endpoints. This is a great place to put the api_key.

    config.params = { "api_key" => "asdfasdfoerkwlejrwer" }

    default: {}

  • logger: Set your logger.

    default: Logger.new(STDOUT)

    config.logger = Rails.logger
  • headers: 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: nil

    config.ssl_context = OpenSSL::SSL::SSLContext.new.tap do |ctx|
                           ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
                         end
  • use_ssl_context: It has to be set to true for using the ssl_context

    default: false

Endpoint

Required:

  • client: Wrappi::Client class

      client MyClient
  • path: 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: user and pass.

    default: nil

      basic_auth do
        { user: 'wrappi', pass: 'secret'}
      end
  • follow_redirects: If first request responds a redirect it will follow them.

    default: true

  • body_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: false

  • retry_if: Block to evaluate if request has to be retried. In the block are yielded Response and Endpoint instances. If the block returns true the request will be retried.

      retry_if do |response, endpoint|
        endpoint.class #=> MyEndpoint
        response.error? # => true or false
      end

    Use 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 200 if the aggregation is completed if the aggregation is not completed will return 201 making 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
      end

    Notice that this block will never be executed if an error occur (like timeouts). For retrying on errors use the retry_options

  • retry_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.

  retry_options 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.
  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.

About

Framework to create API clients

License:MIT License


Languages

Language:Ruby 98.1%Language:Dockerfile 1.0%Language:HTML 0.5%Language:Shell 0.4%