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.
# 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.
#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, b: 2} },
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