Subjoin
Ruby library for parsing JSON-API
Install / Use
/learn @seanredmond/SubjoinREADME
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](http://json
Related Skills
node-connect
344.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
96.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
