Skip to content

Commit db788a5

Browse files
committed
Merge pull request #853 from mateomurphy/jsonapi-format-updates
RC3 Updates for JSON API
2 parents b68d7f4 + 9480b56 commit db788a5

17 files changed

+403
-237
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
* adds support for `meta` and `meta_key` [@kurko]
44
* adds method to override association [adcb99e, @kurko]
5-
* add `has_one` attribute for backwards compatibility [@ggordon]
5+
* adds `has_one` attribute for backwards compatibility [@ggordon]
6+
* updates JSON API support to RC3 [@mateomurphy]

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ end
181181

182182
#### JSONAPI
183183

184-
This adapter follows the format specified in
184+
This adapter follows RC3 of the format specified in
185185
[jsonapi.org/format](http://jsonapi.org/format). It will include the associated
186-
resources in the `"linked"` member when the resource names are included in the
186+
resources in the `"included"` member when the resource names are included in the
187187
`include` option.
188188

189189
```ruby

lib/active_model/serializer.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ def json_key
164164
end
165165
end
166166

167+
def id
168+
object.id if object
169+
end
170+
171+
def type
172+
object.class.to_s.demodulize.underscore.pluralize
173+
end
174+
167175
def attributes(options = {})
168176
attributes =
169177
if options[:fields]
@@ -172,6 +180,8 @@ def attributes(options = {})
172180
self.class._attributes.dup
173181
end
174182

183+
attributes += options[:required_fields] if options[:required_fields]
184+
175185
attributes.each_with_object({}) do |name, hash|
176186
hash[name] = send(name)
177187
end

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@ def initialize(serializer, options = {})
1616
end
1717

1818
def serializable_hash(options = {})
19-
@root = (@options[:root] || serializer.json_key.to_s.pluralize).to_sym
20-
2119
if serializer.respond_to?(:each)
22-
@hash[@root] = serializer.map do |s|
23-
self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[@root]
20+
@hash[:data] = serializer.map do |s|
21+
self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[:data]
2422
end
2523
else
2624
@hash = cached_object do
27-
@hash[@root] = attributes_for_serializer(serializer, @options)
28-
add_resource_links(@hash[@root], serializer)
25+
@hash[:data] = attributes_for_serializer(serializer, @options)
26+
add_resource_links(@hash[:data], serializer)
2927
@hash
3028
end
3129
end
@@ -35,58 +33,40 @@ def serializable_hash(options = {})
3533
private
3634

3735
def add_links(resource, name, serializers)
38-
type = serialized_object_type(serializers)
3936
resource[:links] ||= {}
40-
41-
if name.to_s == type || !type
42-
resource[:links][name] ||= []
43-
resource[:links][name] += serializers.map{|serializer| serializer.id.to_s }
44-
else
45-
resource[:links][name] ||= {}
46-
resource[:links][name][:type] = type
47-
resource[:links][name][:ids] ||= []
48-
resource[:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s }
49-
end
37+
resource[:links][name] ||= { linkage: [] }
38+
resource[:links][name][:linkage] += serializers.map { |serializer| { type: serializer.type, id: serializer.id.to_s } }
5039
end
5140

5241
def add_link(resource, name, serializer)
5342
resource[:links] ||= {}
54-
resource[:links][name] = nil
43+
resource[:links][name] = { linkage: nil }
5544

5645
if serializer && serializer.object
57-
type = serialized_object_type(serializer)
58-
if name.to_s == type || !type
59-
resource[:links][name] = serializer.id.to_s
60-
else
61-
resource[:links][name] ||= {}
62-
resource[:links][name][:type] = type
63-
resource[:links][name][:id] = serializer.id.to_s
64-
end
46+
resource[:links][name][:linkage] = { type: serializer.type, id: serializer.id.to_s }
6547
end
6648
end
6749

68-
def add_linked(resource_name, serializers, parent = nil)
50+
def add_included(resource_name, serializers, parent = nil)
6951
serializers = Array(serializers) unless serializers.respond_to?(:each)
7052

7153
resource_path = [parent, resource_name].compact.join('.')
7254

73-
if include_assoc?(resource_path) && resource_type = serialized_object_type(serializers)
74-
plural_name = resource_type.pluralize.to_sym
75-
@top[:linked] ||= {}
76-
@top[:linked][plural_name] ||= []
55+
if include_assoc?(resource_path)
56+
@top[:included] ||= []
7757

7858
serializers.each do |serializer|
7959
attrs = attributes_for_serializer(serializer, @options)
8060

81-
add_resource_links(attrs, serializer, add_linked: false)
61+
add_resource_links(attrs, serializer, add_included: false)
8262

83-
@top[:linked][plural_name].push(attrs) unless @top[:linked][plural_name].include?(attrs)
63+
@top[:included].push(attrs) unless @top[:included].include?(attrs)
8464
end
8565
end
8666

8767
serializers.each do |serializer|
8868
serializer.each_association do |name, association, opts|
89-
add_linked(name, association, resource_path) if association
69+
add_included(name, association, resource_path) if association
9070
end if include_nested_assoc? resource_path
9171
end
9272
end
@@ -97,14 +77,16 @@ def attributes_for_serializer(serializer, options)
9777
result = []
9878
serializer.each do |object|
9979
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
80+
options[:required_fields] = [:id, :type]
10081
attributes = object.attributes(options)
101-
attributes[:id] = attributes[:id].to_s if attributes[:id]
82+
attributes[:id] = attributes[:id].to_s
10283
result << attributes
10384
end
10485
else
10586
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
87+
options[:required_fields] = [:id, :type]
10688
result = serializer.attributes(options)
107-
result[:id] = result[:id].to_s if result[:id]
89+
result[:id] = result[:id].to_s
10890
end
10991

11092
result
@@ -128,18 +110,8 @@ def check_assoc(assoc)
128110
end
129111
end
130112

131-
def serialized_object_type(serializer)
132-
return false unless Array(serializer).first
133-
type_name = Array(serializer).first.object.class.to_s.demodulize.underscore
134-
if serializer.respond_to?(:first)
135-
type_name.pluralize
136-
else
137-
type_name
138-
end
139-
end
140-
141113
def add_resource_links(attrs, serializer, options = {})
142-
options[:add_linked] = options.fetch(:add_linked, true)
114+
options[:add_included] = options.fetch(:add_included, true)
143115

144116
serializer.each_association do |name, association, opts|
145117
attrs[:links] ||= {}
@@ -150,9 +122,9 @@ def add_resource_links(attrs, serializer, options = {})
150122
add_link(attrs, name, association)
151123
end
152124

153-
if @options[:embed] != :ids && options[:add_linked]
125+
if options[:add_included]
154126
Array(association).each do |association|
155-
add_linked(name, association)
127+
add_included(name, association)
156128
end
157129
end
158130
end

test/action_controller/adapter_selector_test.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ def test_render_using_default_adapter
2929

3030
def test_render_using_adapter_override
3131
get :render_using_adapter_override
32-
assert_equal '{"profiles":{"name":"Name 1","description":"Description 1"}}', response.body
32+
33+
expected = {
34+
data: {
35+
name: "Name 1",
36+
description: "Description 1",
37+
id: assigns(:profile).id.to_s,
38+
type: "profiles"
39+
}
40+
}
41+
42+
assert_equal expected.to_json, response.body
3343
end
3444

3545
def test_render_skipping_adapter

test/action_controller/json_api_linked_test.rb

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,80 +83,87 @@ def render_collection_with_include
8383
def test_render_resource_without_include
8484
get :render_resource_without_include
8585
response = JSON.parse(@response.body)
86-
refute response.key? 'linked'
86+
refute response.key? 'included'
8787
end
8888

8989
def test_render_resource_with_include
9090
get :render_resource_with_include
9191
response = JSON.parse(@response.body)
92-
assert response.key? 'linked'
93-
assert_equal 1, response['linked']['authors'].size
94-
assert_equal 'Steve K.', response['linked']['authors'].first['name']
92+
assert response.key? 'included'
93+
assert_equal 1, response['included'].size
94+
assert_equal 'Steve K.', response['included'].first['name']
9595
end
9696

9797
def test_render_resource_with_nested_has_many_include
9898
get :render_resource_with_nested_has_many_include
9999
response = JSON.parse(@response.body)
100-
expected_linked = {
101-
"authors" => [{
100+
expected_linked = [
101+
{
102102
"id" => "1",
103+
"type" => "authors",
103104
"name" => "Steve K.",
104105
"links" => {
105-
"posts" => [],
106-
"roles" => ["1", "2"],
107-
"bio" => nil
106+
"posts" => { "linkage" => [] },
107+
"roles" => { "linkage" => [{ "type" =>"roles", "id" => "1" }, { "type" =>"roles", "id" => "2" }] },
108+
"bio" => { "linkage" => nil }
108109
}
109-
}],
110-
"roles"=>[{
110+
}, {
111111
"id" => "1",
112+
"type" => "roles",
112113
"name" => "admin",
113114
"links" => {
114-
"author" => "1"
115+
"author" => { "linkage" => { "type" =>"authors", "id" => "1" } }
115116
}
116117
}, {
117118
"id" => "2",
119+
"type" => "roles",
118120
"name" => "colab",
119121
"links" => {
120-
"author" => "1"
122+
"author" => { "linkage" => { "type" =>"authors", "id" => "1" } }
121123
}
122-
}]
123-
}
124-
assert_equal expected_linked, response['linked']
124+
}
125+
]
126+
assert_equal expected_linked, response['included']
125127
end
126128

127129
def test_render_resource_with_nested_include
128130
get :render_resource_with_nested_include
129131
response = JSON.parse(@response.body)
130-
assert response.key? 'linked'
131-
assert_equal 1, response['linked']['authors'].size
132-
assert_equal 'Anonymous', response['linked']['authors'].first['name']
132+
assert response.key? 'included'
133+
assert_equal 1, response['included'].size
134+
assert_equal 'Anonymous', response['included'].first['name']
133135
end
134136

135137
def test_render_collection_without_include
136138
get :render_collection_without_include
137139
response = JSON.parse(@response.body)
138-
refute response.key? 'linked'
140+
refute response.key? 'included'
139141
end
140142

141143
def test_render_collection_with_include
142144
get :render_collection_with_include
143145
response = JSON.parse(@response.body)
144-
assert response.key? 'linked'
146+
assert response.key? 'included'
145147
end
146148

147149
def test_render_resource_with_nested_attributes_even_when_missing_associations
148150
get :render_resource_with_missing_nested_has_many_include
149151
response = JSON.parse(@response.body)
150-
assert response.key? 'linked'
151-
refute response['linked'].key? 'roles'
152+
assert response.key? 'included'
153+
refute has_type?(response['included'], 'roles')
152154
end
153155

154156
def test_render_collection_with_missing_nested_has_many_include
155157
get :render_collection_with_missing_nested_has_many_include
156158
response = JSON.parse(@response.body)
157-
assert response.key? 'linked'
158-
assert response['linked'].key? 'roles'
159+
assert response.key? 'included'
160+
assert has_type?(response['included'], 'roles')
161+
end
162+
163+
def has_type?(collection, value)
164+
collection.detect { |i| i['type'] == value}
159165
end
166+
160167
end
161168
end
162169
end

test/action_controller/serialization_scope_name_test.rb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
require 'pathname'
33

44
class DefaultScopeNameTest < ActionController::TestCase
5-
TestUser = Struct.new(:name, :admin)
6-
75
class UserSerializer < ActiveModel::Serializer
86
attributes :admin?
97
def admin?
@@ -17,25 +15,23 @@ class UserTestController < ActionController::Base
1715
before_filter { request.format = :json }
1816

1917
def current_user
20-
TestUser.new('Pete', false)
18+
User.new(id: 1, name: 'Pete', admin: false)
2119
end
2220

2321
def render_new_user
24-
render json: TestUser.new('pete', false), serializer: UserSerializer, adapter: :json_api
22+
render json: User.new(id: 1, name: 'Pete', admin: false), serializer: UserSerializer, adapter: :json_api
2523
end
2624
end
2725

2826
tests UserTestController
2927

3028
def test_default_scope_name
3129
get :render_new_user
32-
assert_equal '{"users":{"admin?":false}}', @response.body
30+
assert_equal '{"data":{"admin?":false,"id":"1","type":"users"}}', @response.body
3331
end
3432
end
3533

3634
class SerializationScopeNameTest < ActionController::TestCase
37-
TestUser = Struct.new(:name, :admin)
38-
3935
class AdminUserSerializer < ActiveModel::Serializer
4036
attributes :admin?
4137
def admin?
@@ -50,18 +46,18 @@ class AdminUserTestController < ActionController::Base
5046
before_filter { request.format = :json }
5147

5248
def current_admin
53-
TestUser.new('Bob', true)
49+
User.new(id: 2, name: 'Bob', admin: true)
5450
end
5551

5652
def render_new_user
57-
render json: TestUser.new('pete', false), serializer: AdminUserSerializer, adapter: :json_api
53+
render json: User.new(id: 1, name: 'Pete', admin: false), serializer: AdminUserSerializer, adapter: :json_api
5854
end
5955
end
6056

6157
tests AdminUserTestController
6258

6359
def test_override_scope_name_with_controller
6460
get :render_new_user
65-
assert_equal '{"admin_users":{"admin?":true}}', @response.body
61+
assert_equal '{"data":{"admin?":true,"id":"1","type":"users"}}', @response.body
6662
end
6763
end

0 commit comments

Comments
 (0)