Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 114 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ users = Users.all
# GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }
```

#### JSON API support
#### JSON API support (experimental)

To consume a JSON API 1.0 compliant service, it must return data in accordance with the [JSON API spec](http://jsonapi.org/). The general format
of the data is as follows:
Expand Down Expand Up @@ -626,6 +626,119 @@ Her::API.setup url: 'https://my_awesome_json_api_service' do |c|
end
```

There is also partial support for relationships and compound documents.

```json
{
"data": {
{
"id": 1,
"type": "ballers",
"attributes": { name: "Roger Federer" },
"relationships": {
"sponsors": {
"data": [
{
"type": "sponsors",
"id": 1
},
{
"type": "sponsors",
"id": 2
}
]
},
"country": {
"data": {
"type": "countries",
"id": 1
}
}
}
}
},
included: [
{
type: 'sponsors',
id: 1,
attributes: { company: 'Nike' }
},
{
type: 'sponsors',
id: 2,
attributes: { company: 'Rolex' }
},
{
type: 'countries',
id: 1,
attributes: { name: 'Switzerland' }
}
]
}
```

```ruby
class Baller
include Her::JsonApi::Model

# will populate associations from included resources
has_many :sponsors
belongs_to :country

# defaults to demodulized, pluralized class name, e.g. contributors
type :developers
end

fed = Baller.find(1)
fed.sponsors.map(&:company) # => ["Nike", "Rolex"]
fed.country.name # => 'Switzerland'
```

However, relationships are ignored unless those resources are included. JSON API relationships
are built on the idea that a url can be passed to indicate the location of the resource. The dynamic
nature of this is something the client will look to support down the road.

```json
{
"data": {
{
"id": 1,
"type": "ballers",
"attributes": { "name": "Roger Federer", "country_id": 100},
"relationships": {
"sponsors": {
"data": [
{
"type": "sponsors",
"id": 1
},
{
"type": "sponsors",
"id": 2
}
]
},
"country": {
"data": {
"type": "countries",
"id": 100
}
}
}
}
}
}
```

Note, the presence of country_id is still depended on in order to fetch the belongs_to
association. Thus, given the preceding response which does not have included resources:

```ruby
fed = Baller.find(1) # => GET "/ballers/1"
fed.sponsors # => GET "/ballers/1/sponsors"
fed.country # => GET "/ballers/1/country/100
```

### Custom requests

You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
Expand Down
35 changes: 31 additions & 4 deletions lib/her/middleware/json_api_parser.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Her
module Middleware
# This middleware expects the resource/collection data to be contained in the `data`
# key of the JSON object
# This middleware requires the resource/collection
# data to be contained in the `data` key of the JSON object
class JsonApiParser < ParseJSON
# Parse the response body
#
Expand All @@ -11,13 +11,36 @@ class JsonApiParser < ParseJSON
def parse(body)
json = parse_json(body)

included = json.fetch(:included, [])
primary_data = json.fetch(:data, {})
Array.wrap(primary_data).each do |resource|
resource_relationships = resource.delete(:relationships) { {} }
resource[:attributes].merge!(populate_relationships(resource_relationships, included.dup))
end

{
:data => json[:data] || {},
:data => primary_data || {},
:errors => json[:errors] || [],
:metadata => json[:meta] || {},
}
end

def populate_relationships(relationships, included)
return {} if included.empty?
{}.tap do |built|
relationships.each do |rel_name, linkage|
linkage_data = linkage.fetch(:data, {})
built_relationship = if linkage_data.is_a? Array
linkage_data.map { |l| included.detect { |i| i.values_at(:id, :type) == l.values_at(:id, :type) } }.compact
else
included.detect { |i| i.values_at(:id, :type) == linkage_data.values_at(:id, :type) }
end

built[rel_name] = built_relationship
end
end
end

# This method is triggered when the response has been received. It modifies
# the value of `env[:body]`.
#
Expand All @@ -26,7 +49,11 @@ def parse(body)
def on_complete(env)
env[:body] = case env[:status]
when 204
parse('{}')
{
:data => {},
:errors => [],
:metadata => {},
}
else
parse(env[:body])
end
Expand Down
Loading