What is SimpleJSONAPIClient
?
SimpleJSONAPIClient
is a framework for building Ruby clients for JSONAPI-compliant services.
How do I use SimpleJSONAPIClient
?
Setup
First create models inheriting from SimpleJSONAPIClient::Base
, and specifying a few details.
-
COLLECTION_URL
- the path to fetch the resource collection -
INDIVIDUAL_URL
- the path to fetch an individual resource -
TYPE
- the JSONAPI resource type to use when creating a new resource -
attributes
- the names of attributes which can be found on the resource - relationships -
has_one
andhas_many
define relationships, and take these arguments:- relationship name (e.g., the
:goats
inhas_many :goats
) -
class_name
to use when instantiating related objects
- relationship name (e.g., the
They should look like this:
class Post < SimpleJSONAPIClient::Base
COLLECTION_URL = '/posts'
INDIVIDUAL_URL = '/posts/%{id}'
TYPE = 'posts'
attributes :title, :text
meta :copyright
has_one :author, class_name: 'Author'
has_many :comments, class_name: 'Comment'
end
class Author < SimpleJSONAPIClient::Base
COLLECTION_URL = '/authors'
INDIVIDUAL_URL = '/authors/%{id}'
TYPE = 'authors'
attributes :name
has_many :posts, class_name: 'Post'
has_many :comments, class_name: 'Comment'
end
class Comment < SimpleJSONAPIClient::Base
COLLECTION_URL = '/comments'
INDIVIDUAL_URL = '/comments/%{id}'
TYPE = 'comments'
attributes :text
has_one :post, class_name: 'Post'
has_one :author, class_name: 'Author'
end
If you have behavior you'd like to share across models, you may want to first create an abstract class inheriting from SimpleJSONAPIClient::Base
and then have all your models inherit from that.
Next, create a Faraday
connection to handle the domain, authorization strategy, and anything else you need (making sure to include JSON parsing middleware):
def connection(token)
default_headers = {
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => "token=#{token}"
}
@connection ||= Faraday.new(url: 'https://example.com', headers: default_headers) do |connection|
connection.request :json
connection.response :json, :content_type => /\bjson$/ # use middleware to parse JSON when response Content-Type is json
connection.adapter :net_http
end
end
Now you can start making requests!
Fetching
Laziness and SimpleJSONAPIClient
Post.fetch_all(connection: connection)
=> #<Enumerator: #<Enumerator::Generator:0x00562894acd420>:each>
What's going on? SimpleJSONAPIClient
tries to be as lazy as possible while still being convenient. So if you actually want to fetch everything, you'll be able to call Array
methods and it will fetch the resource, paginating through all the results. If it's an endpoint with thousands of pages, you can use Enumerator
methods like #each
and it'll paginate through the results, fetching the next page when it runs out of objects.
Let's call #to_a
to see a bit more detail.
posts = Post.fetch_all(connection: connection).to_a
=> [#<Post id=1 title="A Very Proper Post Title" text="I am absolutely incensed about something." author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>,
#<Post id=2 title="The System is Down" text="The Cheat" author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/2/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/2/comments>>]
Attributes are loaded immediately, but relationships are lazily instantiated. So if we dig a little bit further:
posts.first.author
=> #<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author>
Nope, still lazy! However, once we start fetching details about the author, SimpleJSONAPIClient
knows a request has to be made, and fills in the details:
posts.first.author.id
=> "3"
posts.first.author
=> #<Author id=3 name="Filbert" posts=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Post url=http://jsonapi_app:3000/authors/3/posts> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/authors/3/comments>>
We can read more easily by calling #as_json
:
posts.first.author.as_json
=> {
:data => {
:type => "authors",
:attributes => { :name => "Filbert" },
:relationships => {
:posts => {
:data => [{ :type => "posts", :id => "1" }]
},
:comments => { :data => [] }
}
}
}
More About Fetching Capabilities
You can also explicitly fetch a single item:
post = Post.fetch(connection: connection, url_opts: { id: 1 })
=> #<Post id=1 title="A Very Proper Post Title" text="I am absolutely incensed about something." author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
url_opts
, in all cases where you see them, are passed to the template Strings for INDIVIDUAL_URL
and COLLECTION_URL
in the model.
You've already seen that id
and relationships
are available; attributes
and meta
information also become methods on the object:
post = Post.fetch(connection: connection, url_opts: { id: 1 })
post.title
=> "A Very Proper Post Title"
post.text
=> "I am absolutely incensed about something."
post.copyright
=> "Copyright 2017"
You can also use JSONAPI includes to reduce the number of requests that are necessary:
post = JSONAPIAppClient::Post.fetch(connection: connection, url_opts: { id: 1 }, includes: ['author', 'comments.author'])
post.author # will not make another web request
post.comments.first.author # will not make another web request
SimpleJSONAPIClient
will check the included records for related records you access through the returned model.
And finally, you can use JSONAPI-style filtering and pagination as well:
# Given 3 authors...
JSONAPIAppClient::Author.fetch_all(connection: connection).to_a
=> [#<JSONAPIAppClient::Author id=1 name="Filbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments>>,
#<JSONAPIAppClient::Author id=2 name="Dilbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/2/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/2/comments>>,
#<JSONAPIAppClient::Author id=3 name="Wilbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments>>]
# Filtering by name
JSONAPIAppClient::Author.fetch_all(connection: connection, filter_opts: { name: 'Filbert' }).to_a
=> [#<JSONAPIAppClient::Author id=1 name="Filbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments>>]
# Just grabbing the last page
JSONAPIAppClient::Author.fetch_all(connection: connection, page_opts: { size: 1, number: 3 }).to_a
=> [#<JSONAPIAppClient::Author id=3 name="Wilbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments>>]
# You can adjust the pagination strategy and SimpleJSONAPIClient will follow it, but return the same results
JSONAPIAppClient::Author.fetch_all(connection: connection, page_opts: { size: 1, number: 1 }).to_a
=> [#<JSONAPIAppClient::Author id=1 name="Filbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/1/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/1/comments>>,
#<JSONAPIAppClient::Author id=2 name="Dilbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/2/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/2/comments>>,
#<JSONAPIAppClient::Author id=3 name="Wilbert" posts=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Post url=http://jsonapi_app_console:3002/authors/3/posts> comments=#<SimpleJSONAPIClient::Relationships::ArrayLinkRelationship model_class=JSONAPIAppClient::Comment url=http://jsonapi_app_console:3002/authors/3/comments>>]
Creating
Creating records is available from the model class:
post = Post.fetch(url_opts: { id: 1 }, connection: connection)
=> #<Post id=1 title="A Very Proper Post Title" text="I am absolutely incensed about something." author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
author = Author.fetch(url_opts: { id: 1}, connection: connection)
=> #<Author id=1 name="Filbert" posts=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Post url=http://jsonapi_app:3000/authors/1/posts> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/authors/1/comments>>
Comment.create(connection: connection, text: 'I adore your article!', post: post, author: author)
=> #<Comment id=19 text="I adore your article!" post=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Client::Post url=http://jsonapi_app:3000/comments/19/post> author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/comments/19/author>>
The created record is returned; if creation fails, a SimpleJSONAPIClient::Errors::ApiError
is raised.
Updating
If you want to update a record, you can do it from the model itself:
post = Post.fetch(url_opts: { id: 1 }, connection: connection)
=> #<Post id=1 title="A Very Proper Post Title" text="I am absolutely incensed about something." author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
[2] pry(main)> post.update(attributes: { text: 'foo' })
=> #<Post id=1 title="A Very Proper Post Title" text="foo" author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
If you have the ID of the record handy, you update straight from the model class without fetching the record first:
Post.update(id: 1, url_opts: { id: 1 }, connection: connection, text: 'foo')
=> #<Post id=1 title="A Very Proper Post Title" text="foo" author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
Deleting
You can delete a record from the model itself:
post = Post.fetch(url_opts: { id: 1 }, connection: connection)
=> #<Post id=1 title="A Very Proper Post Title" text="I am absolutely incensed about something." author=#<SimpleJSONAPIClient::Base::SingularLinkRelationship model_class=Author url=http://jsonapi_app:3000/posts/1/author> comments=#<SimpleJSONAPIClient::Base::ArrayLinkRelationship model_class=Comment url=http://jsonapi_app:3000/posts/1/comments>>
post.delete
=> true
Post.fetch(url_opts: { id: 1 }, connection: connection)
=> nil
or from the class, if you have the ID:
Post.delete(url_opts: { id: 1 }, connection: connection)
=> true
Post.fetch(url_opts: { id: 1 }, connection: connection)
=> nil
Installation
Add this line to your application's Gemfile:
gem 'simple_jsonapi_client'
And then execute:
$ bundle
Or install it yourself as:
$ gem install simple_jsonapi_client
Development
You must have Docker and Docker Compose installed to run the tests and use the built-in development utilities.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment, and bin/rails
to interact with the Rails app in spec/jsonapi_app
that is provided for local development and testing.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/amcaplan/simple_jsonapi_client. 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.
Code of Conduct
Everyone interacting in the SimpleJsonapiClient project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.