Skip to content

Commit 796a105

Browse files
committed
Add support for relationship-level links and meta.
1 parent 20ddc5e commit 796a105

File tree

7 files changed

+177
-50
lines changed

7 files changed

+177
-50
lines changed

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class JsonApi < Base
66
autoload :PaginationLinks
77
autoload :FragmentCache
88
autoload :Link
9+
autoload :Association
10+
autoload :ResourceIdentifier
911
autoload :Deserialization
1012

1113
# TODO: if we like this abstraction and other API objects to it,
@@ -97,7 +99,7 @@ def resource_objects_for(serializers)
9799
end
98100

99101
def process_resource(serializer, primary)
100-
resource_identifier = resource_identifier_for(serializer)
102+
resource_identifier = JsonApi::ResourceIdentifier.new(serializer).as_json
101103
return false unless @resource_identifiers.add?(resource_identifier)
102104

103105
resource_object = resource_object_for(serializer)
@@ -127,45 +129,22 @@ def process_relationship(serializer, include_tree)
127129
process_relationships(serializer, include_tree)
128130
end
129131

130-
def resource_identifier_type_for(serializer)
131-
return serializer._type if serializer._type
132-
if ActiveModelSerializers.config.jsonapi_resource_type == :singular
133-
serializer.object.class.model_name.singular
134-
else
135-
serializer.object.class.model_name.plural
136-
end
137-
end
138-
139-
def resource_identifier_id_for(serializer)
140-
if serializer.respond_to?(:id)
141-
serializer.id
142-
else
143-
serializer.object.id
144-
end
145-
end
146-
147-
def resource_identifier_for(serializer)
148-
type = resource_identifier_type_for(serializer)
149-
id = resource_identifier_id_for(serializer)
150-
151-
{ id: id.to_s, type: type }
152-
end
153-
154132
def attributes_for(serializer, fields)
155133
serializer.attributes(fields).except(:id)
156134
end
157135

158136
def resource_object_for(serializer)
159137
resource_object = cache_check(serializer) do
160-
resource_object = resource_identifier_for(serializer)
138+
resource_object = JsonApi::ResourceIdentifier.new(serializer).as_json
161139

162140
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
163141
attributes = attributes_for(serializer, requested_fields)
164142
resource_object[:attributes] = attributes if attributes.any?
165143
resource_object
166144
end
167145

168-
relationships = relationships_for(serializer)
146+
requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
147+
relationships = relationships_for(serializer, requested_associations)
169148
resource_object[:relationships] = relationships if relationships.any?
170149

171150
links = links_for(serializer)
@@ -174,24 +153,15 @@ def resource_object_for(serializer)
174153
resource_object
175154
end
176155

177-
def relationship_value_for(serializer, options = {})
178-
if serializer.respond_to?(:each)
179-
serializer.map { |s| resource_identifier_for(s) }
180-
else
181-
if options[:virtual_value]
182-
options[:virtual_value]
183-
elsif serializer && serializer.object
184-
resource_identifier_for(serializer)
185-
end
186-
end
187-
end
188-
189-
def relationships_for(serializer)
190-
resource_type = resource_identifier_type_for(serializer)
191-
requested_associations = fieldset.fields_for(resource_type) || '*'
156+
def relationships_for(serializer, requested_associations)
192157
include_tree = IncludeTree.from_include_args(requested_associations)
193158
serializer.associations(include_tree).each_with_object({}) do |association, hash|
194-
hash[association.key] = { data: relationship_value_for(association.serializer, association.options) }
159+
hash[association.key] = JsonApi::Association.new(serializer,
160+
association.serializer,
161+
association.options,
162+
association.links,
163+
association.meta)
164+
.as_json
195165
end
196166
end
197167

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module ActiveModel
2+
class Serializer
3+
module Adapter
4+
class JsonApi
5+
class Association
6+
def initialize(parent_serializer, serializer, options, links, meta)
7+
@object = parent_serializer.object
8+
@scope = parent_serializer.scope
9+
10+
@options = options
11+
@data = data_for(serializer, options)
12+
@links = links
13+
.map { |key, value| { key => Link.new(parent_serializer, value).as_json } }
14+
.reduce({}, :merge)
15+
@meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
16+
end
17+
18+
def as_json
19+
hash = {}
20+
hash[:data] = @data if @options[:include_data]
21+
hash[:links] = @links if @links.any?
22+
hash[:meta] = @meta if @meta
23+
24+
hash
25+
end
26+
27+
protected
28+
29+
attr_reader :object, :scope
30+
31+
private
32+
33+
def data_for(serializer, options)
34+
if serializer.respond_to?(:each)
35+
serializer.map { |s| ResourceIdentifier.new(s).as_json }
36+
else
37+
if options[:virtual_value]
38+
options[:virtual_value]
39+
elsif serializer && serializer.object
40+
ResourceIdentifier.new(serializer).as_json
41+
end
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end
48+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
module ActiveModel
2+
class Serializer
3+
module Adapter
4+
class JsonApi
5+
class ResourceIdentifier
6+
def initialize(serializer)
7+
@id = id_for(serializer)
8+
@type = type_for(serializer)
9+
end
10+
11+
def as_json
12+
{ id: @id.to_s, type: @type }
13+
end
14+
15+
protected
16+
17+
attr_reader :object, :scope
18+
19+
private
20+
21+
def type_for(serializer)
22+
return serializer._type if serializer._type
23+
if ActiveModelSerializers.config.jsonapi_resource_type == :singular
24+
serializer.object.class.model_name.singular
25+
else
26+
serializer.object.class.model_name.plural
27+
end
28+
end
29+
30+
def id_for(serializer)
31+
if serializer.respond_to?(:id)
32+
serializer.id
33+
else
34+
serializer.object.id
35+
end
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end

lib/active_model/serializer/association.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Serializer
99
# @example
1010
# Association.new(:comments, CommentSummarySerializer)
1111
#
12-
Association = Struct.new(:name, :serializer, :options) do
12+
Association = Struct.new(:name, :serializer, :options, :links, :meta) do
1313
# @return [Symbol]
1414
#
1515
def key

lib/active_model/serializer/reflection.rb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,38 @@ class Serializer
3434
# So you can inspect reflections in your Adapters.
3535
#
3636
class Reflection < Field
37+
def initialize(*)
38+
super
39+
@_links = {}
40+
@_include_data = true
41+
end
42+
43+
def link(name, value = nil, &block)
44+
@_links[name] = block || value
45+
nil
46+
end
47+
48+
def meta(value = nil, &block)
49+
@_meta = block || value
50+
nil
51+
end
52+
53+
def include_data(value = true)
54+
@_include_data = value
55+
nil
56+
end
57+
58+
def value(serializer)
59+
@object = serializer.object
60+
@scope = serializer.scope
61+
62+
if block
63+
instance_eval(&block)
64+
else
65+
serializer.read_attribute_for_serialization(name)
66+
end
67+
end
68+
3769
# Build association. This method is used internally to
3870
# build serializer's association by its reflection.
3971
#
@@ -59,6 +91,7 @@ def build_association(subject, parent_serializer_options)
5991
association_value = value(subject)
6092
reflection_options = options.dup
6193
serializer_class = subject.class.serializer_for(association_value, reflection_options)
94+
reflection_options[:include_data] = _include_data
6295

6396
if serializer_class
6497
begin
@@ -73,9 +106,13 @@ def build_association(subject, parent_serializer_options)
73106
reflection_options[:virtual_value] = association_value
74107
end
75108

76-
Association.new(name, serializer, reflection_options)
109+
Association.new(name, serializer, reflection_options, _links, _meta)
77110
end
78111

112+
protected
113+
114+
attr_accessor :object, :scope, :_links, :_meta, :_include_data
115+
79116
private
80117

81118
def serializer_options(subject, parent_serializer_options, reflection_options)

test/adapter/json_api/links_test.rb

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class JsonApi
77
class LinksTest < ActiveSupport::TestCase
88
LinkAuthor = Class.new(::Model)
99
class LinkAuthorSerializer < ActiveModel::Serializer
10+
type 'author'
11+
1012
link :self do
1113
href "//example.com/link_author/#{object.id}"
1214
meta stuff: 'value'
@@ -17,11 +19,23 @@ class LinkAuthorSerializer < ActiveModel::Serializer
1719
link :yet_another do
1820
"//example.com/resource/#{object.id}"
1921
end
22+
23+
has_many :posts do
24+
link :self do
25+
href '//example.com/link_author/relationships/posts'
26+
meta stuff: 'value'
27+
end
28+
link :related do
29+
href '//example.com/link_author/posts'
30+
meta count: object.posts.count
31+
end
32+
include_data false
33+
end
2034
end
2135

2236
def setup
2337
@post = Post.new(id: 1337, comments: [], author: nil)
24-
@author = LinkAuthor.new(id: 1337)
38+
@author = LinkAuthor.new(id: 1337, posts: [@post])
2539
end
2640

2741
def test_toplevel_links
@@ -61,6 +75,23 @@ def test_resource_links
6175
}
6276
assert_equal(expected, hash[:data][:links])
6377
end
78+
79+
def test_relationship_links
80+
hash = serializable(@author, adapter: :json_api).serializable_hash
81+
expected = {
82+
links: {
83+
self: {
84+
href: '//example.com/link_author/relationships/posts',
85+
meta: { stuff: 'value' }
86+
},
87+
related: {
88+
href: '//example.com/link_author/posts',
89+
meta: { count: 1 }
90+
}
91+
}
92+
}
93+
assert_equal(expected, hash[:data][:relationships][:posts])
94+
end
6495
end
6596
end
6697
end

test/serializers/associations_test.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ def test_has_many_and_has_one
3232

3333
case key
3434
when :posts
35-
assert_equal({}, options)
35+
assert_equal({ include_data: true }, options)
3636
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
3737
when :bio
38-
assert_equal({}, options)
38+
assert_equal({ include_data: true }, options)
3939
assert_nil serializer
4040
when :roles
41-
assert_equal({}, options)
41+
assert_equal({ include_data: true }, options)
4242
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
4343
else
4444
flunk "Unknown association: #{key}"
@@ -80,7 +80,7 @@ def test_belongs_to
8080
flunk "Unknown association: #{key}"
8181
end
8282

83-
assert_equal({}, association.options)
83+
assert_equal({ include_data: true }, association.options)
8484
end
8585
end
8686

0 commit comments

Comments
 (0)