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
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.
moduleMyApi# this is an "abstract" base class thatclassBase < JsonApiClient::Resource# set the api base url in an abstract base classself.site="http://example.com/"endclassArticle < BaseendclassComment < BaseendclassPerson < BaseendendBy 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:
MyApi::Article.allMyApi::Article.where(author_id: 1).find(2)MyApi::Article.where(author_id: 1).allMyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences,:cars).allu=MyApi::Person.new(first_name: "bar",last_name: "foo")u.saveu=MyApi::Person.find(1).firstu.update_attributes(a: "b",c: "d")u=MyApi::Person.create(a: "b",c: "d")All class level finders/creators should return a JsonApiClient::ResultSet which behaves like an Array and contains extra data about the api response.
Out of the box, json_api_client handles server side validation only.
User.create(name: "Bob",email_address: "invalid email")# => falseuser=User.new(name: "Bob",email_address: "invalid email")user.save# => false# returns an error collector which is array-likeuser.errors# => ["Email address is invalid"]# get all error titlesuser.errors.full_messages# => ["Email address is invalid"]# get errors for a specific parameteruser.errors[:email_address]# => ["Email address is invalid"]user=User.find(1)user.update_attributes(email_address: "invalid email")# => falseuser.errors# => ["Email address is invalid"]user.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.
If the response has a top level meta data section, we can access it via the meta accessor on ResultSet.
# Example response:{"meta": {"copyright": "Copyright 2015 Example Corp.","authors": ["Yehuda Katz","Steve Klabnik","Dan Gebhardt"]},"data": {// ...
}}articles=Articles.allarticles.meta.copyright# => "Copyright 2015 Example Corp."articles.meta.authors# => ["Yehuda Katz", "Steve Klabnik", "Dan Gebhardt"]If the resource returns top level links, we can access them via the links accessor on ResultSet.
articles=Articles.find(1)articles.links.relatedYou 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.
moduleMyApiclassAccount < JsonApiClient::Resourcebelongs_to:userendend# try to find without the nested parameterMyApi::Account.find(1)# => raises ArgumentError# makes request to /users/2/accounts/1MyApi::Account.where(user_id: 2).find(1)# => returns ResultSetYou can create custom methods on both collections (class method) and members (instance methods).
moduleMyApiclassUser < JsonApiClient::Resource# GET /users/searchcustom_endpoint:search,on: :collection,request_method: :get# PUT /users/:id/verifycustom_endpoint:verify,on: :member,request_method: :putendend# makes GET request to /users/search?name=JeffMyApi::User.search(name: 'Jeff')# => <ResultSet of MyApi::User instances>user=MyApi::User.find(1)# makes PUT request to /users/1/verify?foo=baruser.verify(foo: 'bar')If the response returns a compound document, then we should be able to get the related resources.
# makes request to /articles/1?include=author,comments.authorresults=Article.includes(:author,:comments=>:author).find(1)# should not have to make additional requests to the serverauthors=results.map(&:author)# makes request to /articles?fields[articles]=title,bodyarticle=Article.select("title,body").first# should have fetched the requested fieldsarticle.title# => "Rails is Omakase"# should not have returned the created_atarticle.created_at# => raise NoMethodError# makes request to /people?sort=ageyoungest=Person.sort(:age).all# also makes request to /people?sort=ageyoungest=Person.sort(age: :asc).all# makes request to /people?sort=-ageoldest=Person.sort(age: :desc).all# makes request to /articles?page=2&per_page=30articles=Article.page(2).per(30).to_a# also makes request to /articles?page=2&per_page=30articles=Article.paginate(page: 2,per_page: 30).to_aNote: The mapping of pagination parameters is done by the query_builder which is customizable.
If the response contains additional pagination links, you can also get at those:
articles=Article.paginate(page: 2,per_page: 30).to_aarticles.pages.nextarticles.pages.lastA JsonApiClient::ResultSet object should be paginatable with both kaminari and will_paginate.
# makes request to /people?filter[name]=JeffPerson.where(name: 'Jeff').allYou 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.
classUser < JsonApiClient::Resourceproperty:name,type: :stringproperty:is_admin,type: :boolean,default: falseproperty:points_accrued,type: :int,default: 0property:averge_points_per_day,type: :floatend# default valuesu=User.newu.name# => nilu.is_admin# => falseu.points_accrued# => 0# castingu.average_points_per_day="0.3"u.average_points_per_day# => 0.3The basic types that we allow are:
:intor:integer:float:string:time- *Note: Include the time zone in the string if it's different than local time.:boolean- Note: we will cast the string version of "true" and "false" to their respective values
Also, we consider nil to be an acceptable value and will not cast the value.
Note : Do not map the primary key as int.
You can customize this path by changing your resource's table_name:
moduleMyApiclassSomeResource < Basedefself.table_name"foobar"endendend# requests http://example.com/foobarMyApi::SomeResource.allYou 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.
classNullConnectiondefinitialize(*args)enddefrun(request_method,path,params={},headers={})enddefuse(*args);endendclassCustomConnectionResource < TestResourceself.connection_class=NullConnectionendYou 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:
MyApi::Base.connectiondo |connection|
# set OAuth2 headersconnection.useFaradayMiddleware::OAuth2,'MYTOKEN'# log responsesconnection.useFaraday::Response::Loggerconnection.useMyCustomMiddlewareendmoduleMyApiclassUser < Base# will use the customized connectionendendAll resources have a class method connection_options used to pass options to the JsonApiClient::Connection initializer.
MyApi::Base.connection_options[:proxy]='http://proxy.example.com'MyApi::Base.connectiondo |connection|
# ...endmoduleMyApiclassUser < Base# will use the customized connection with proxyendendYou 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:
classMyCustomParserdefself.parse(klass,response)# …# returns some ResultSet objectendendclassMyApi::Base < JsonApiClient::Resourceself.parser=MyCustomParserendYou can customize how the scope builder methods map to request parameters.
classMyQueryBuilderdefinitialize(klass);enddefwhere(conditions={})end# … add order, includes, paginate, page, first, buildendclassMyApi::Base < JsonApiClient::Resourceself.query_builder=MyQueryBuilderendYou can customize how your resources find pagination information from the response.
classMyPaginatordefinitialize(result_set,data);end# implement current_page, total_entries, etcendclassMyApi::Base < JsonApiClient::Resourceself.paginator=MyPaginatorendSee changelog


