Subjoin
A practical wrapper for JSON-API interactions.
Installation
Add this line to your application's Gemfile:
gem 'subjoin'And then execute:
$ bundle
Or install it yourself as:
$ gem install subjoin
Documentation
For full documentation run
$ yardoc
$ yard server
Then load http://localhost:8808 in your browser
Usage
Document
Everything starts with a document, specifically a Subjoin::Document -- the equivalent of a JSON-API document which you
can create with a URI:
require "subjoin"
doc = Subjoin::Document.new(URI("http://example.com/articles"))
(all examples here based on examples in the JSON-API documentation)
Note that you must pass a
URI object. A
string would be interpreted as a JSON-API type.
A Subjoin::Document probably has "primary data" which, if present is an Array
of Subjoin::Resource objects:
doc.has_data? # true if there is primary data
=> true
doc.data # Array of Subjoin::Resource objects
doc.data.first # One resource
The data member of a JSON-API document can be either a single resource object
or an array of resource objects. Subjoin::Document#data always returns an
Array. In a document with a single resource object, the Array will have one
element.
You can access all the other members of the top-level document (all the objects returned are covered below):
doc.links # Hash of Link objects
doc.included # Inclusions object
doc.meta # Meta object
doc.jsonapi # JsonApi object
There are, in addition, methods to test whether any of the above members are present:
doc.has_data?
doc.has_links?
doc.has_included?
doc.has_meta?
doc.has_jsonapi?
Resources
Every Subjoin::Resource has a type and id. The JSON response:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON API paints my bikeshed!"
},
...
}
would correspond to:
article = doc.data.first
article.type
=> "articles"
article.id
=> "1"
The attributes of a Subjoin::Resource object, or any object that
includes Subjoin::Attributable, can be accessed like hash on the
object itself:
article["title"]
=> "JSON API paints my bikeshed!"
You can also get the entire attributes Hash as
Subjoin::Attributable#attributes:
article.attributes # Hash
article.attributes.keys
=> ["title"]
article.attributes["title"]
=> "JSON API paints my bikeshed!"
The other expected members of a resource object are available. The objects returned by these methods are all explained below:
article.links # Hash of Link objects
article.relationships # Array of Relationship objects
article.meta # Meta object
As with Subjoin::Document, there are methods to see if any of the above are available
article.has_links?
article.has_meta?
Links
Subjoin::Document, Subjoin::Resource, and Subjoin::Relationship can all
have links. They all have
the Subjoin::Linkable#links method which returns a Hash of Subjoin::Link
objects:
article.links.keys
=> ["self"]
article.links["self"].href.to_s
=> "http://example.com/articles/1"
JSON-API allows for two link object formats. One simply has a link
"links": {
"self": "http://example.com/articles/1"
}
and one has an href attribute and meta object:
"links": {
"related": {
"href": "http://example.com/articles/1/comments",
"meta": {
"count": 10
}
}
}
Subjoin treats either variation like the latter:
article.links["self"].href.to_s
=> "http://example.com/articles/1"
article.links["self"].has_meta?
=> false
article.links["self"].meta?
=> nil
article.links["related"].href.to_s
=> "http://example.com/articles/1/comments"
article.links["related"].has_meta?
=> true
article.links["related"].meta["count"]
=> 10
Note that the href is always returned as a URI object. If you have a Subjoin::Link you can get the corresponding Subjoin::Document:
article.links["related"].get # Same thing as Subjoin::Document.new
# with the URL
Resource Identifiers
Before getting to relationships, we should take a minute to look at
resource identifiers. Above,
we saw that every Subjoin::Resource has a type and id.
article.type # "articles"
article.id # "1"
Though the above attributes exist individually, these two attributes
work together as a compound key and are, in fact put together in
Subjoin as a Subjoin::Identifier object:
article.identifier # Identifier object
article.identifier.type # "articles"
article.identifier.id # "1"
Subjoin::Identifier objects are used for equality: two
Subjoin::Resource objects are considered equal if they have equal
Identifers:
article1 == article2 # Really tests...
article1.identifier == article2.identifier # Really tests...
article1.identifier.type == article2.identifier.type &&
article1.identifer.id == article2.identifier.id
More importantly, identifiers occur in Relationship objects as pointers to other resources. These pointers are called linkages:
article.relationships.author.linkages # Array of Identifier objects
They may have, optionally, a meta attribute as well, and meta
attributes are ignored in tests for equality.
Relationships, Linkages, Included Resources
Okay, now we can get to how you'll really use JSON-API resources and why you would JSON-API over other options: resource linking and included resources.
In many RESTful APIs, resources have embedded child resources which is, in my experience, a principle source of the bikeshedding arguments that JSON-API tries to avoid ("should X be a child of Y, or Y a child of X?" "How should the X response be different when it is achild of another resource?"). Instead of nesting and embedding, in JSON-API resources may have relationships to other resources.
article.relationships # A Hash of Subjoin::Relationship objects
article.relationships.keys
=> ["author", "comments"]
This much tells you that an "article" can have an "author" and
"comments". In Subjoin, relationships are instantiated as
Subjoin::Relationship objects whose two important properties are
links and linkages. Subjoin::Relationship are
Subjoin::Linkable so links works as it does in other objects.
author = article.relationships["author"] # Relationship object
author.links.keys
=> ["self", "related"]
author.links["related"].to_s
=> "http://example.com/articles/1/author"
author.links["related"].get # Get a new Document
Alongside links, linkages give you resource identifiers for the
related resources. while the "comments" link tells us how to get a
document with all the related comments:
comments.links["self"].to_s
=> "http://example.com/articles/1/relationships/comments"
The corresponding linkages returns an Array of
Subjoin::Identifier that are pointers to specific resources:
comments = article.relationships["comments"]
comments.linkages.count
=> 2
This tells us that there are two related comments. If we look at one, we can get the type and id:
comments.linkages[0].type
=> "comments"
comments.linkages[0].id
=> "5"
So far so good, but now what? Inclusion
With Subjoin, you can request that these related resources be included in the document, one of three ways:
# URI parameters
doc = Subjoin::Document.new(URI("http://example.com/articles/1?include=author,comments"))
# Parameters Hash with a string
doc = Subjoin::Document.new(URI("http://example.com/article/1s", {"include" => "author,comments"}))
# Parameters Hash with an array of strings
doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
All three are equivalent. The array of strings version works especially well with relationship keys
doc2 = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => articles.relationships.keys}))
When a document is requested with included resources, they can be accessed via included
# Get the document
doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
# Get the article
article = doc.data.first
# Get the realted author identifier
auth_identifier = article.related["author"].linkages.first
auth_identifier.type
=> "people"
auth_identifier.id
=> "9"
# Get the embedded resource
doc.has_included?
=> true
# Look up included resource by identifier
author = doc.included[auth_identifier]
# Now we have access to the whole author resource
author.type
=> "people"
author.id
=> "9"
author["twitter"]
=> "dgeb"
If that sounds kind of complicated, it is. But you can...
Let Subjoin resolve the linkages for you
To make all this easier, Subjoin::Resource provides a rels method that does all this under the hood:
article.rels.keys
=> ["author", "comments"]
author = article.rels["author"] # Returns the author Resource
author["twitter"]
=> "dgeb"
Meta Information
Meta information is represented by
Subjoin::Meta objects. Any object that might have meta information will
have a #meta attribute. Meta object attributes are accessible by
name like other attributes.
Given this JSON:
{
"meta": {
"copyright": "Copyright 2015 Example Corp.",
"authors": [
"Yehuda Katz",
"Steve Klabnik",
"Dan Gebhardt",
"Tyler Kellen"
]
},
"data": {
// ...
}
}
The data might be accessed in this way:
article.meta # Meta object
article.meta.copyright # "Copyright 2015 Example Corp."
Using Inheritance
Another way to use Subjoin is via inheritance. Using this approach you
create your own classes to represent JSON-API resource types of a
specific JSON-API server implementation. These classes must be
sub-classes of Subjoin::Resource and must include
Subjoin::Inheritable, which adds some constants and methods that
Subjoin::Document will use to instantiate objects correctly. Next
you must override a class variable, ROOT_URI, which should be the
root of all URIs of the API. For instance, in the examples above,
"http://example.com" would be the value of ROOT_URI. By default,
Subjoin will use the lower-cased name of the class as the type in
URIs. If the class name does not match the type, you can further
override TYPE_PATH to indicate the name (or longer URI fragment)
that should be used in URIs to request the resource type.
Your custom classes must also be part of the Subjoin module. You
should probably create one sub-class of Subjoin::Resource that
overrides ROOT_URI, and then create other classes as sub-classes of
this:
require "subjoin"
module Subjoin
# Use this class as the parent of further subclasses.
# They will inherit the ROOT_URI defined here
class ExampleResource < Subjoin::Resource
include Inheritable
ROOT_URI="http://example.com"
end
# Subjoin will make requests to http://example.com/articles
class Articles < ExampleResource
end
# Use TYPE_PATH if you don't want to name the class the same thing as
# the type
class ArticleComments < ExampleResource
TYPE_PATH="comments"
end
end
Now, when you get a document, the resources in it will be mapped to an available type:
doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
# We mapped the article type to the Article class
article = doc.data.first
article.class
=> Subjoin::Article
# We mapped the comment type to the ArticleComment class
comment = article.rels["comments"].first
comment.class
=> Subjoin::ArticleComment
# We didn't map the person type to anything, so we get a Resource
author = article.rels["author"].first
author.class
=> Subjoin::Resource
Limitations
Subjoin currently only supports GET requests.
Some features, such as pagination, should work generically but could have more convenient methods to handle them.
Why is it called "Subjoin"
Nice word. Sounds coder-y. Has most of the letters of "Ruby JSON-API".
Contributing
- Fork it ( https://github.com/seanredmond/subjoin/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request