RestAPIBuilder
A simple wrapper for rest-client aiming to make creation and testing of API clients easier.
Why?
RestClient is great, but after building a few API clients with it you will almost inevitably find yourself re-implementing certain basic things such as:
- Compiling and parsing basic JSON requests/responses
- Handling and extracting details from non-200 responses
- Creating testing interfaces for your API clients
This library tries to solve these and similar issues by providing a set of self-contained helper methods to improve on rest-client features with an optional WebMock testing interface.
Installation
gem install rest_api_builder
RestAPIBuilder::Request
Main RestAPIBuilder module which includes various helper methods for parsing RestClient responses, catching errors and composing request details. handle_*
and compose_*
methods are intended to be used in conjunction, but you can use any of them in any combination without relying on the rest.
If you prefer to access these methods directly without including RestAPIBuilder::Request
, they are available as module_function methods in the corresponding modules: RestAPIBuilder::Request::RequestOptions
and RestAPIBuilder::Request::ResponseHandler
# Basic usage
require 'rest_api_builder'
include RestAPIBuilder::Request
logger = Logger.new(STDOUT)
response = handle_json_response(logger: logger) do
RestClient::Request.execute(
{
**compose_json_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :get
),
log: logger
}
)
end
response[:success] # => true
response[:status] # => 200
response[:body] # => []
Included methods:
#handle_response(options, &block)
Executes given block, expecting to receive RestClient::Response as a result.
Returns plain ruby hash with following keys: :success, :status, :body, :headers
This will gracefully handle non-200 responses, but will throw on any error without defined response(e.g server timeout)
require 'rest_api_builder'
include RestAPIBuilder::Request
# normal response
response = handle_response do
RestClient::Request.execute(method: :get, url: 'https://api.github.com/users/octocat/orgs')
end
response[:success] # => true
response[:status] # => 200
response[:body] # => '[]'
response[:headers] # => {:accept_ranges=>"bytes", :access_control_allow_origin=>"*", ...}
# non-200 response that would result in RestClient::RequestFailed exception otherwise
response = handle_response do
RestClient::Request.execute(method: :get, url: 'https://api.github.com/users/octocat/foobar')
end
response[:success] # => false
response[:status] # => 404
response[:body] # => "{\"message\":\"Not Found\",..."}"
Accepted Options:
Name | Description |
---|---|
logger | Any object with << method, e.g Logger instance. Will be used to log response details in the same way that RestClient's log option logs the request details. Optional |
#handle_json_response(options, &block)
Behaves just like #handle_response
, but will also attempt to decode response :body
, returning it as is if a parsing error occurs.
require 'rest_api_builder'
include RestAPIBuilder::Request
# decodes JSON response body
response = handle_json_response do
RestClient::Request.execute(method: :get, url: 'https://api.github.com/users/octocat/orgs')
end
response[:success] # => true
response[:status] # => 200
response[:body] # => []
# returns body as is if it cannot be decoded
response = handle_json_response do
RestClient::Request.execute(method: :get, url: 'https://github.com/foo/bar/test')
end
response[:success] # => false
response[:status] # => 404
response[:body] # => "Not Found"
handle_response_error(&block)
Low-level API.
You can use this method if you want to work with regular RestClient::Response
objects directly(e.g when using block_response
or raw_response
options). This will handle non-200 exceptions but will not do anything else.
Returns plain ruby hash with :success
and :raw_response
keys.
require 'rest_api_builder'
include RestAPIBuilder::Request
# returns RestClient::Response as :raw_response
response = handle_response_error do
RestClient::Request.execute(method: :get, url: 'https://api.github.com/users/octocat/orgs')
end
response[:success] # => true
response[:raw_response] # => <RestClient::Response 200 "[]">
# handles non-200 responses
response = handle_response_error do
RestClient::Request.execute(
method: :get,
url: 'https://api.github.com/users/octocat/foobar',
raw_response: true
)
end
response[:success] # => false
response[:raw_response] # => <RestClient::RawResponse @code=404, @file=#<Tempfile...>>
#compose_request_options(options)
Provides a more consistent interface for RestClient::Request#execute
.
This method returns a hash of options which you can then pass to RestClient::Request#execute
.
require 'rest_api_builder'
include RestAPIBuilder::Request
# basic usage
response = RestClient::Request.execute(
compose_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :get
)
)
response.request.url # => "https://api.github.com/users/octocat/orgs"
response.body # => '[]'
# advanced options
result = handle_response_error do
RestClient::Request.execute(
compose_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :post,
body: 'Hello',
headers: { content_type: 'foobar' },
query: { foo: 'bar' }
)
)
end
request = result[:raw_response].request
request.url # => "https://api.github.com/users/octocat/orgs?foo=bar"
request.headers # => {:content_type=>"foobar"}
request.payload # => <RestClient::Payload 'Hello'>
Accepted Options:
Name | Description |
---|---|
base_url | Base URL of the request. Required. |
method | HTTP method of the request(e.g :get, :post, :patch). Required. |
path | Path to be appended to base_url . Optional. |
body | Request Body. Optional. |
headers | Request Headers. Optional. |
query | Query hash to be appended to the resulting url. Optional. |
#compose_json_request_options(options)
Same as compose_request_options
but will also convert provided body
(if any) to JSON and append Content-Type: 'application/json'
to headers
require 'rest_api_builder'
include RestAPIBuilder::Request
# basic usage
result = handle_response_error do
RestClient::Request.execute(
compose_json_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :post,
body: {a: 1}
)
)
end
request = result[:raw_response].request
request.headers # => {:content_type=>:json}
request.payload # => <RestClient::Payload "{\"a\":1}">
RestAPIBuilder::APIClient
#define_resource_shortcuts(resources, resources_scope:, init_with:)
Dynamically defines attribute readers for given resources
require 'rest_api_builder'
module ReadmeExamples
module Resources
class Octocat
def orgs
RestClient::Request.execute(method: :get, url: 'https://api.github.com/users/octocat/orgs')
end
end
end
class APIClient
include RestAPIBuilder::APIClient
def initialize
define_resource_shortcuts(
[:octocat],
resources_scope: ReadmeExamples::Resources,
init_with: ->(resource_class) { resource_class.new }
)
end
end
end
GITHUB_API = ReadmeExamples::APIClient.new
response = GITHUB_API.octocat.orgs
response.body # => '[]'
response.code # => 200
Accepted Arguments:
Name | Description |
---|---|
resources | Array of resources to define shortcuts for |
resources_scope | Module or String(path to Module) within which resource classes are contained |
init_with | Lambda which will be called for each resource class. The result will be returned from the defined shortcut. Note: init_with lambda is only called once so resource class must be able to function as a singleton. |
RestAPIBuilder::WebMockRequestExpectations
Optional wrapper around WebMock mocking interface with various improvements. This module must be required explicitly and expects WebMock to be installed as a dependency in your project.
If you prefer to access these methods directly without including RestAPIBuilder::WebMockRequestExpectations
, they are available as module_function methods in the RestAPIBuilder::WebMockRequestExpectations::Expectations
module.
#expect_execute(options)
Defines a request expectation using WebMock's stub_request
.
require 'rest_api_builder'
require 'rest_api_builder/webmock_request_expectations'
include RestAPIBuilder::Request
include RestAPIBuilder::WebMockRequestExpectations
# basic usage with regular webmock interface
expect_execute(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :post
).with(body: {foo: 'bar'}).to_return(body: '[hello]')
response = RestClient::Request.execute(
compose_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :post,
body: {foo: 'bar'}
)
)
response.body # => '[hello]'
# using expect_execute's request and response options
expect_execute(
base_url: 'https://api.github.com',
path: '/users/octocat',
method: :post,
# matches request body and query hashes partially by default
request: { body: {foo: 'bar'}, query: {a: 1} },
response: { body: 'hello' }
)
response = RestClient::Request.execute(
compose_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat',
method: :post,
body: { foo: 'bar', bar: 'baz' },
query: { a: 1, b: 2 }
)
)
response.body # => 'hello'
Accepted Options:
Name | Description |
---|---|
base_url | Base URL of the request expectation. Required. |
path | HTTP method of the request. Required. |
method | Path to be appended to base_url . Regular expressions are also supported. Optional. |
request | Hash of options which will be passed to WebMock's with method with following changes: body hash is converted to hash_including expectation and query hash values are transformed to strings and then it's converted into hash_including expectation. Optional |
response | Hash of options which will be passed to WebMock's to_return method unchanged. Optional |
#expect_json_execute(options)
Same as expect_execute
but will also call JSON encode on response.body
(if one is provided).
require 'rest_api_builder'
require 'rest_api_builder/webmock_request_expectations'
include RestAPIBuilder::Request
include RestAPIBuilder::WebMockRequestExpectations
expect_json_execute(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :get,
response: { body: { foo: 'bar' } }
)
response = RestClient::Request.execute(
compose_request_options(
base_url: 'https://api.github.com',
path: '/users/octocat/orgs',
method: :get
)
)
response.body # => "{\"foo\":\"bar\"}"
License
MIT