codeclimate/json_api_client

Name: json_api_client

Owner: Code Climate

Description: Build client libraries compliant with specification defined by jsonapi.org

Forked from: chingor13/json_api_client

Created: 2017-03-02 21:28:44.0

Updated: 2018-04-13 19:31:09.0

Pushed: 2017-03-02 21:31:03.0

Homepage: null

Size: 492

Language: Ruby

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

JsonApiClient Build Status Code Climate Code Coverage

This gem is meant to help you build an API client for interacting with REST APIs as laid out by http://jsonapi.org. It attempts to give you a query building framework that is easy to understand (it is similar to ActiveRecord scopes).

Note: master is currently tracking the 1.0.0 specification. If you're looking for the older code, see 0.x branch

Usage

You will want to create your own resource classes that inherit from JsonApiClient::Resource similar to how you would create an ActiveRecord class. You may also want to create your own abstract base class to share common behavior. Additionally, you will probably want to namespace your models. Namespacing your model will not affect the url routing to that resource.

le MyApi
this is an "abstract" base class that
ass Base < JsonApiClient::Resource
# set the api base url in an abstract base class
self.site = "http://example.com/"
d

ass Article < Base
d

ass Comment < Base
d

ass Person < Base
d

By convention, we guess the resource route from the class name. In the above example, Article's path is “http://example.com/articles” and Person's path would be “http://example.com/people”.

Some basic example usage:

i::Article.all
i::Article.where(author_id: 1).find(2)
i::Article.where(author_id: 1).all

i::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all

MyApi::Person.new(first_name: "bar", last_name: "foo")
ve

MyApi::Person.find(1).first
date_attributes(
 "b",
 "d"


MyApi::Person.create(
 "b",
 "d"

All class level finders/creators should return a JsonApiClient::ResultSet which behaves like an Array and contains extra data about the api response.

Handling Validation Errors

See specification

Out of the box, json_api_client handles server side validation only.

.create(name: "Bob", email_address: "invalid email")
 false

 = User.new(name: "Bob", email_address: "invalid email")
.save
 false

turns an error collector which is array-like
.errors
 ["Email address is invalid"]

t all error titles
.errors.full_messages
 ["Email address is invalid"]

t errors for a specific parameter
.errors[:email_address]
 ["Email address is invalid"]

 = User.find(1)
.update_attributes(email_address: "invalid email")
 false

.errors
 ["Email address is invalid"]

.email_address
 "invalid email"

For now we are assuming that error sources are all parameters.

If you want to add client side validation, I suggest creating a form model class that uses ActiveModel's validations.

Meta information

See specification

If the response has a top level meta data section, we can access it via the meta accessor on ResultSet.

ample response:

eta": {
"copyright": "Copyright 2015 Example Corp.",
"authors": [
  "Yehuda Katz",
  "Steve Klabnik",
  "Dan Gebhardt"
]

ata": {
// ...


cles = Articles.all

cles.meta.copyright
 "Copyright 2015 Example Corp."

cles.meta.authors
 ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]
Top-level Links

See specification

If the resource returns top level links, we can access them via the links accessor on ResultSet.

cles = Articles.find(1)
cles.links.related
Nested Resources

You can force nested resource paths for your models by using a belongs_to association.

Note: Using belongs_to is only necessary for setting a nested path.

le MyApi
ass Account < JsonApiClient::Resource
belongs_to :user
d


y to find without the nested parameter
i::Account.find(1)
 raises ArgumentError

kes request to /users/2/accounts/1
i::Account.where(user_id: 2).find(1)
 returns ResultSet
Custom Methods

You can create custom methods on both collections (class method) and members (instance methods).

le MyApi
ass User < JsonApiClient::Resource
# GET /users/search
custom_endpoint :search, on: :collection, request_method: :get

# PUT /users/:id/verify
custom_endpoint :verify, on: :member, request_method: :put
d


kes GET request to /users/search?name=Jeff
i::User.search(name: 'Jeff')
 <ResultSet of MyApi::User instances>

 = MyApi::User.find(1)
kes PUT request to /users/1/verify?foo=bar
.verify(foo: 'bar')
Fetching Includes

See specification

If the response returns a compound document, then we should be able to get the related resources.

kes request to /articles/1?include=author,comments.author
lts = Article.includes(:author, :comments => :author).find(1)

ould not have to make additional requests to the server
ors = results.map(&:author)
Sparse Fieldsets

See specification

kes request to /articles?fields[articles]=title,body
cle = Article.select("title", "body").first

ould have fetched the requested fields
cle.title
 "Rails is Omakase"

ould not have returned the created_at
cle.created_at
 raise NoMethodError
Sorting

See specification

kes request to /people?sort=age
gest = Person.order(:age).all

so makes request to /people?sort=age
gest = Person.order(age: :asc).all

kes request to /people?sort=-age
st = Person.order(age: :desc).all
Paginating

See specification

Requesting
kes request to /articles?page=2&per_page=30
cles = Article.page(2).per(30).to_a

so makes request to /articles?page=2&per_page=30
cles = Article.paginate(page: 2, per_page: 30).to_a

Note: The mapping of pagination parameters is done by the query_builder which is customizable.

Browsing

If the response contains additional pagination links, you can also get at those:

cles = Article.paginate(page: 2, per_page: 30).to_a
cles.pages.next
cles.pages.last
Library compatibility

A JsonApiClient::ResultSet object should be paginatable with both kaminari and will_paginate.

Filtering

See specifiation

kes request to /people?filter[name]=Jeff
on.where(name: 'Jeff').all
Schema

You can define schema within your client model. You can define basic types and set default values if you wish. If you declare a basic type, we will try to cast any input to be that type.

The added benefit of declaring your schema is that you can access fields before data is set (otherwise, you'll get a NoMethodError).

Note: This is completely optional. This will set default values and handle typecasting.

Example
s User < JsonApiClient::Resource
operty :name, type: :string
operty :is_admin, type: :boolean, default: false
operty :points_accrued, type: :int, default: 0
operty :averge_points_per_day, type: :float


fault values
User.new

me
 nil

_admin
 false

ints_accrued
 0

sting
erage_points_per_day = "0.3"
erage_points_per_day
 0.3
Types

The basic types that we allow are:

Also, we consider nil to be an acceptable value and will not cast the value.

Note : Do not map the primary key as int.

Customizing
Paths

You can customize this path by changing your resource's table_name:

le MyApi
ass SomeResource < Base
def self.table_name
  "foobar"
end
d


quests http://example.com/foobar
i::SomeResource.all
Custom headers

You can inject custom headers on resource request by wrapping your code into block:

i::SomeResource.with_headers(x_access_token: 'secure_token_here') do
Api::SomeResource.find(1)

Connections

You can configure your API client to use a custom connection that implementes the run instance method. It should return data that your parser can handle. The default connection class wraps Faraday and lets you add middleware.

s NullConnection
f initialize(*args)
d

f run(request_method, path, params = {}, headers = {})
d

f use(*args); end


s CustomConnectionResource < TestResource
lf.connection_class = NullConnection

Connection Options

You can configure your connection using Faraday middleware. In general, you'll want to do this in a base model that all your resources inherit from:

i::Base.connection do |connection|
set OAuth2 headers
nnection.use FaradayMiddleware::OAuth2, 'MYTOKEN'

log responses
nnection.use Faraday::Response::Logger

nnection.use MyCustomMiddleware


le MyApi
ass User < Base
# will use the customized connection
d

Specifying an HTTP Proxy

All resources have a class method `connection_options` used to pass options to the JsonApiClient::Connection initializer.

i::Base.connection_options[:proxy] = 'http://proxy.example.com'
i::Base.connection do |connection|
...


le MyApi
ass User < Base
# will use the customized connection with proxy
d

Custom Parser

You can configure your API client to use a custom parser that implements the parse class method. It should return a JsonApiClient::ResultSet instance. You can use it by setting the parser attribute on your model:

s MyCustomParser
f self.parse(klass, response)
# ?
# returns some ResultSet object
d


s MyApi::Base < JsonApiClient::Resource
lf.parser = MyCustomParser

Custom Query Builder

You can customize how the scope builder methods map to request parameters.

s MyQueryBuilder
f initialize(klass); end

f where(conditions = {})
d

? add order, includes, paginate, page, first, build


s MyApi::Base < JsonApiClient::Resource
lf.query_builder = MyQueryBuilder

Custom Paginator

You can customize how your resources find pagination information from the response.

If the existing paginator fits your requirements but you don't use the default page and per_page params for pagination, you can customise the param keys as follows:

ApiClient::Paginating::Paginator.page_param = "page[number]"
ApiClient::Paginating::Paginator.per_page_param = "page[size]"

Please note that this is a global configuration, so library authors should create a custom paginator that inherits JsonApiClient::Paginating::Paginator and configure the custom paginator to avoid modifying global config.

If the existing paginator does not fit your needs, you can create a custom paginator:

s MyPaginator
f initialize(result_set, data); end
implement current_page, total_entries, etc


s MyApi::Base < JsonApiClient::Resource
lf.paginator = MyPaginator

Type Casting

You can define your own types and its casting mechanism for schema.

ire 'money'
s MyMoneyCaster
f self.cast(value, default)
begin
  Money.new(value, "USD")
rescue ArgumentError
  default
end
d


ApiClient::Schema.register money: MyMoneyCaster

and finally

s Order < JsonApiClient::Resource
operty :total_amount, type: :money

Changelog

See changelog


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.