active_model_serializers 0.10.5 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +16 -2
  4. data/README.md +2 -2
  5. data/Rakefile +1 -30
  6. data/active_model_serializers.gemspec +1 -1
  7. data/bin/rubocop +38 -0
  8. data/docs/general/adapters.md +25 -9
  9. data/docs/general/getting_started.md +1 -1
  10. data/docs/general/rendering.md +18 -4
  11. data/docs/general/serializers.md +21 -2
  12. data/docs/howto/add_pagination_links.md +1 -1
  13. data/docs/integrations/ember-and-json-api.md +3 -0
  14. data/lib/active_model/serializer.rb +252 -74
  15. data/lib/active_model/serializer/association.rb +51 -14
  16. data/lib/active_model/serializer/belongs_to_reflection.rb +5 -1
  17. data/lib/active_model/serializer/concerns/caching.rb +29 -21
  18. data/lib/active_model/serializer/has_many_reflection.rb +4 -1
  19. data/lib/active_model/serializer/has_one_reflection.rb +1 -1
  20. data/lib/active_model/serializer/lazy_association.rb +95 -0
  21. data/lib/active_model/serializer/reflection.rb +119 -75
  22. data/lib/active_model/serializer/version.rb +1 -1
  23. data/lib/active_model_serializers/adapter/json_api.rb +30 -17
  24. data/lib/active_model_serializers/adapter/json_api/relationship.rb +38 -9
  25. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +9 -0
  26. data/lib/active_model_serializers/model.rb +5 -4
  27. data/lib/tasks/rubocop.rake +53 -0
  28. data/test/action_controller/adapter_selector_test.rb +2 -2
  29. data/test/serializers/associations_test.rb +64 -31
  30. data/test/serializers/reflection_test.rb +427 -0
  31. metadata +9 -12
  32. data/lib/active_model/serializer/collection_reflection.rb +0 -7
  33. data/lib/active_model/serializer/concerns/associations.rb +0 -102
  34. data/lib/active_model/serializer/concerns/attributes.rb +0 -82
  35. data/lib/active_model/serializer/concerns/configuration.rb +0 -59
  36. data/lib/active_model/serializer/concerns/links.rb +0 -35
  37. data/lib/active_model/serializer/concerns/meta.rb +0 -29
  38. data/lib/active_model/serializer/concerns/type.rb +0 -25
  39. data/lib/active_model/serializer/singular_reflection.rb +0 -7
@@ -1,34 +1,71 @@
1
+ require 'active_model/serializer/lazy_association'
2
+
1
3
  module ActiveModel
2
4
  class Serializer
3
5
  # This class holds all information about serializer's association.
4
6
  #
5
- # @attr [Symbol] name
6
- # @attr [Hash{Symbol => Object}] options
7
- # @attr [block]
8
- #
9
- # @example
10
- # Association.new(:comments, { serializer: CommentSummarySerializer })
11
- #
12
- class Association < Field
7
+ # @api private
8
+ Association = Struct.new(:reflection, :association_options) do
9
+ attr_reader :lazy_association
10
+ delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
11
+
12
+ def initialize(*)
13
+ super
14
+ @lazy_association = LazyAssociation.new(reflection, association_options)
15
+ end
16
+
17
+ # @return [Symbol]
18
+ delegate :name, to: :reflection
19
+
13
20
  # @return [Symbol]
14
21
  def key
15
- options.fetch(:key, name)
22
+ reflection_options.fetch(:key, name)
16
23
  end
17
24
 
18
- # @return [ActiveModel::Serializer, nil]
19
- def serializer
20
- options[:serializer]
25
+ # @return [True,False]
26
+ def key?
27
+ reflection_options.key?(:key)
21
28
  end
22
29
 
23
30
  # @return [Hash]
24
31
  def links
25
- options.fetch(:links) || {}
32
+ reflection_options.fetch(:links) || {}
26
33
  end
27
34
 
28
35
  # @return [Hash, nil]
36
+ # This gets mutated, so cannot use the cached reflection_options
29
37
  def meta
30
- options[:meta]
38
+ reflection.options[:meta]
39
+ end
40
+
41
+ def belongs_to?
42
+ reflection.foreign_key_on == :self
31
43
  end
44
+
45
+ def polymorphic?
46
+ true == reflection_options[:polymorphic]
47
+ end
48
+
49
+ # @api private
50
+ def serializable_hash(adapter_options, adapter_instance)
51
+ association_serializer = lazy_association.serializer
52
+ return virtual_value if virtual_value
53
+ association_object = association_serializer && association_serializer.object
54
+ return unless association_object
55
+
56
+ serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
57
+
58
+ if polymorphic? && serialization
59
+ polymorphic_type = association_object.class.name.underscore
60
+ serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization }
61
+ end
62
+
63
+ serialization
64
+ end
65
+
66
+ private
67
+
68
+ delegate :reflection_options, to: :lazy_association
32
69
  end
33
70
  end
34
71
  end
@@ -1,7 +1,11 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
3
  # @api private
4
- class BelongsToReflection < SingularReflection
4
+ class BelongsToReflection < Reflection
5
+ # @api private
6
+ def foreign_key_on
7
+ :self
8
+ end
5
9
  end
6
10
  end
7
11
  end
@@ -40,9 +40,9 @@ module ActiveModel
40
40
 
41
41
  module ClassMethods
42
42
  def inherited(base)
43
- super
44
43
  caller_line = caller[1]
45
44
  base._cache_digest_file_path = caller_line
45
+ super
46
46
  end
47
47
 
48
48
  def _cache_digest
@@ -68,6 +68,18 @@ module ActiveModel
68
68
  _cache_options && _cache_options[:skip_digest]
69
69
  end
70
70
 
71
+ # @api private
72
+ # maps attribute value to explicit key name
73
+ # @see Serializer::attribute
74
+ # @see Serializer::fragmented_attributes
75
+ def _attributes_keys
76
+ _attributes_data
77
+ .each_with_object({}) do |(key, attr), hash|
78
+ next if key == attr.name
79
+ hash[attr.name] = { key: key }
80
+ end
81
+ end
82
+
71
83
  def fragmented_attributes
72
84
  cached = _cache_only ? _cache_only : _attributes - _cache_except
73
85
  cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
@@ -158,6 +170,7 @@ module ActiveModel
158
170
 
159
171
  # Read cache from cache_store
160
172
  # @return [Hash]
173
+ # Used in CollectionSerializer to set :cached_attributes
161
174
  def cache_read_multi(collection_serializer, adapter_instance, include_directive)
162
175
  return {} if ActiveModelSerializers.config.cache_store.blank?
163
176
 
@@ -180,12 +193,14 @@ module ActiveModel
180
193
  cache_keys << object_cache_key(serializer, adapter_instance)
181
194
 
182
195
  serializer.associations(include_directive).each do |association|
183
- if association.serializer.respond_to?(:each)
184
- association.serializer.each do |sub_serializer|
196
+ # TODO(BF): Process relationship without evaluating lazy_association
197
+ association_serializer = association.lazy_association.serializer
198
+ if association_serializer.respond_to?(:each)
199
+ association_serializer.each do |sub_serializer|
185
200
  cache_keys << object_cache_key(sub_serializer, adapter_instance)
186
201
  end
187
202
  else
188
- cache_keys << object_cache_key(association.serializer, adapter_instance)
203
+ cache_keys << object_cache_key(association_serializer, adapter_instance)
189
204
  end
190
205
  end
191
206
  end
@@ -203,23 +218,18 @@ module ActiveModel
203
218
 
204
219
  ### INSTANCE METHODS
205
220
  def fetch_attributes(fields, cached_attributes, adapter_instance)
206
- if serializer_class.cache_enabled?
207
- key = cache_key(adapter_instance)
208
- cached_attributes.fetch(key) do
209
- serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
210
- attributes(fields, true)
211
- end
221
+ key = cache_key(adapter_instance)
222
+ cached_attributes.fetch(key) do
223
+ fetch(adapter_instance, serializer_class._cache_options, key) do
224
+ attributes(fields, true)
212
225
  end
213
- elsif serializer_class.fragment_cache_enabled?
214
- fetch_attributes_fragment(adapter_instance, cached_attributes)
215
- else
216
- attributes(fields, true)
217
226
  end
218
227
  end
219
228
 
220
- def fetch(adapter_instance, cache_options = serializer_class._cache_options)
229
+ def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = nil)
221
230
  if serializer_class.cache_store
222
- serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do
231
+ key ||= cache_key(adapter_instance)
232
+ serializer_class.cache_store.fetch(key, cache_options) do
223
233
  yield
224
234
  end
225
235
  else
@@ -230,7 +240,6 @@ module ActiveModel
230
240
  # 1. Determine cached fields from serializer class options
231
241
  # 2. Get non_cached_fields and fetch cache_fields
232
242
  # 3. Merge the two hashes using adapter_instance#fragment_cache
233
- # rubocop:disable Metrics/AbcSize
234
243
  def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
235
244
  serializer_class._cache_options ||= {}
236
245
  serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
@@ -239,22 +248,21 @@ module ActiveModel
239
248
  non_cached_fields = fields[:non_cached].dup
240
249
  non_cached_hash = attributes(non_cached_fields, true)
241
250
  include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
242
- non_cached_hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
251
+ non_cached_hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
243
252
 
244
253
  cached_fields = fields[:cached].dup
245
254
  key = cache_key(adapter_instance)
246
255
  cached_hash =
247
256
  cached_attributes.fetch(key) do
248
- serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
257
+ fetch(adapter_instance, serializer_class._cache_options, key) do
249
258
  hash = attributes(cached_fields, true)
250
259
  include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
251
- hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
260
+ hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
252
261
  end
253
262
  end
254
263
  # Merge both results
255
264
  adapter_instance.fragment_cache(cached_hash, non_cached_hash)
256
265
  end
257
- # rubocop:enable Metrics/AbcSize
258
266
 
259
267
  def cache_key(adapter_instance)
260
268
  return @cache_key if defined?(@cache_key)
@@ -1,7 +1,10 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
3
  # @api private
4
- class HasManyReflection < CollectionReflection
4
+ class HasManyReflection < Reflection
5
+ def collection?
6
+ true
7
+ end
5
8
  end
6
9
  end
7
10
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
3
  # @api private
4
- class HasOneReflection < SingularReflection
4
+ class HasOneReflection < Reflection
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,95 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ # @api private
4
+ LazyAssociation = Struct.new(:reflection, :association_options) do
5
+ REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
6
+
7
+ delegate :collection?, to: :reflection
8
+
9
+ def reflection_options
10
+ @reflection_options ||= reflection.options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
11
+ end
12
+
13
+ def object
14
+ @object ||= reflection.value(
15
+ association_options.fetch(:parent_serializer),
16
+ association_options.fetch(:include_slice)
17
+ )
18
+ end
19
+ alias_method :eval_reflection_block, :object
20
+
21
+ def include_data?
22
+ eval_reflection_block if reflection.block
23
+ reflection.include_data?(
24
+ association_options.fetch(:include_slice)
25
+ )
26
+ end
27
+
28
+ # @return [ActiveModel::Serializer, nil]
29
+ def serializer
30
+ return @serializer if defined?(@serializer)
31
+ if serializer_class
32
+ serialize_object!(object)
33
+ elsif !object.nil? && !object.instance_of?(Object)
34
+ cached_result[:virtual_value] = object
35
+ end
36
+ @serializer = cached_result[:serializer]
37
+ end
38
+
39
+ def virtual_value
40
+ cached_result[:virtual_value] || reflection_options[:virtual_value]
41
+ end
42
+
43
+ def serializer_class
44
+ return @serializer_class if defined?(@serializer_class)
45
+ serializer_for_options = { namespace: namespace }
46
+ serializer_for_options[:serializer] = reflection_options[:serializer] if reflection_options.key?(:serializer)
47
+ @serializer_class = association_options.fetch(:parent_serializer).class.serializer_for(object, serializer_for_options)
48
+ end
49
+
50
+ private
51
+
52
+ def cached_result
53
+ @cached_result ||= {}
54
+ end
55
+
56
+ def serialize_object!(object)
57
+ if collection?
58
+ if (serializer = instantiate_collection_serializer(object)).nil?
59
+ # BUG: per #2027, JSON API resource relationships are only id and type, and hence either
60
+ # *require* a serializer or we need to be a little clever about figuring out the id/type.
61
+ # In either case, returning the raw virtual value will almost always be incorrect.
62
+ #
63
+ # Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
64
+ # with an object that is non-nil and has no defined serializer.
65
+ cached_result[:virtual_value] = object.try(:as_json) || object
66
+ else
67
+ cached_result[:serializer] = serializer
68
+ end
69
+ else
70
+ cached_result[:serializer] = instantiate_serializer(object)
71
+ end
72
+ end
73
+
74
+ def instantiate_serializer(object)
75
+ serializer_options = association_options.fetch(:parent_serializer_options).except(:serializer)
76
+ serializer_options[:serializer_context_class] = association_options.fetch(:parent_serializer).class
77
+ serializer = reflection_options.fetch(:serializer, nil)
78
+ serializer_options[:serializer] = serializer if serializer
79
+ serializer_class.new(object, serializer_options)
80
+ end
81
+
82
+ def instantiate_collection_serializer(object)
83
+ serializer = catch(:no_serializer) do
84
+ instantiate_serializer(object)
85
+ end
86
+ serializer
87
+ end
88
+
89
+ def namespace
90
+ reflection_options[:namespace] ||
91
+ association_options.fetch(:parent_serializer_options)[:namespace]
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,4 +1,5 @@
1
1
  require 'active_model/serializer/field'
2
+ require 'active_model/serializer/association'
2
3
 
3
4
  module ActiveModel
4
5
  class Serializer
@@ -8,12 +9,26 @@ module ActiveModel
8
9
  # @example
9
10
  # class PostSerializer < ActiveModel::Serializer
10
11
  # has_one :author, serializer: AuthorSerializer
12
+ # belongs_to :boss, type: :users, foreign_key: :boss_id
11
13
  # has_many :comments
12
14
  # has_many :comments, key: :last_comments do
13
15
  # object.comments.last(1)
14
16
  # end
15
17
  # has_many :secret_meta_data, if: :is_admin?
16
18
  #
19
+ # has_one :blog do |serializer|
20
+ # meta count: object.roles.count
21
+ # serializer.cached_blog
22
+ # end
23
+ #
24
+ # private
25
+ #
26
+ # def cached_blog
27
+ # cache_store.fetch("cached_blog:#{object.updated_at}") do
28
+ # Blog.find(object.blog_id)
29
+ # end
30
+ # end
31
+ #
17
32
  # def is_admin?
18
33
  # current_user.admin?
19
34
  # end
@@ -23,52 +38,118 @@ module ActiveModel
23
38
  # 1) as 'comments' and named 'comments'.
24
39
  # 2) as 'object.comments.last(1)' and named 'last_comments'.
25
40
  #
26
- # PostSerializer._reflections #=>
27
- # # [
28
- # # HasOneReflection.new(:author, serializer: AuthorSerializer),
29
- # # HasManyReflection.new(:comments)
30
- # # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
31
- # # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
32
- # # ]
41
+ # PostSerializer._reflections # =>
42
+ # # {
43
+ # # author: HasOneReflection.new(:author, serializer: AuthorSerializer),
44
+ # # comments: HasManyReflection.new(:comments)
45
+ # # last_comments: HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
46
+ # # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
47
+ # # }
33
48
  #
34
49
  # So you can inspect reflections in your Adapters.
35
- #
36
50
  class Reflection < Field
51
+ attr_reader :foreign_key, :type
52
+
37
53
  def initialize(*)
38
54
  super
39
- @_links = {}
40
- @_include_data = Serializer.config.include_data_default
41
- @_meta = nil
55
+ options[:links] = {}
56
+ options[:include_data_setting] = Serializer.config.include_data_default
57
+ options[:meta] = nil
58
+ @type = options.fetch(:type) do
59
+ class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
60
+ class_name.underscore.pluralize.to_sym
61
+ end
62
+ @foreign_key = options.fetch(:foreign_key) do
63
+ if collection?
64
+ "#{name.to_s.singularize}_ids".to_sym
65
+ else
66
+ "#{name}_id".to_sym
67
+ end
68
+ end
42
69
  end
43
70
 
44
- def link(name, value = nil, &block)
45
- @_links[name] = block || value
71
+ # @api public
72
+ # @example
73
+ # has_one :blog do
74
+ # include_data false
75
+ # link :self, 'a link'
76
+ # link :related, 'another link'
77
+ # link :self, '//example.com/link_author/relationships/bio'
78
+ # id = object.profile.id
79
+ # link :related do
80
+ # "//example.com/profiles/#{id}" if id != 123
81
+ # end
82
+ # link :related do
83
+ # ids = object.likes.map(&:id).join(',')
84
+ # href "//example.com/likes/#{ids}"
85
+ # meta ids: ids
86
+ # end
87
+ # end
88
+ def link(name, value = nil)
89
+ options[:links][name] = block_given? ? Proc.new : value
46
90
  :nil
47
91
  end
48
92
 
49
- def meta(value = nil, &block)
50
- @_meta = block || value
93
+ # @api public
94
+ # @example
95
+ # has_one :blog do
96
+ # include_data false
97
+ # meta(id: object.blog.id)
98
+ # meta liked: object.likes.any?
99
+ # link :self do
100
+ # href object.blog.id.to_s
101
+ # meta(id: object.blog.id)
102
+ # end
103
+ def meta(value = nil)
104
+ options[:meta] = block_given? ? Proc.new : value
51
105
  :nil
52
106
  end
53
107
 
108
+ # @api public
109
+ # @example
110
+ # has_one :blog do
111
+ # include_data false
112
+ # link :self, 'a link'
113
+ # link :related, 'another link'
114
+ # end
115
+ #
116
+ # has_one :blog do
117
+ # include_data false
118
+ # link :self, 'a link'
119
+ # link :related, 'another link'
120
+ # end
121
+ #
122
+ # belongs_to :reviewer do
123
+ # meta name: 'Dan Brown'
124
+ # include_data true
125
+ # end
126
+ #
127
+ # has_many :tags, serializer: TagSerializer do
128
+ # link :self, '//example.com/link_author/relationships/tags'
129
+ # include_data :if_sideloaded
130
+ # end
54
131
  def include_data(value = true)
55
- @_include_data = value
132
+ options[:include_data_setting] = value
56
133
  :nil
57
134
  end
58
135
 
136
+ def collection?
137
+ false
138
+ end
139
+
140
+ def include_data?(include_slice)
141
+ include_data_setting = options[:include_data_setting]
142
+ case include_data_setting
143
+ when :if_sideloaded then include_slice.key?(name)
144
+ when true then true
145
+ when false then false
146
+ else fail ArgumentError, "Unknown include_data_setting '#{include_data_setting.inspect}'"
147
+ end
148
+ end
149
+
59
150
  # @param serializer [ActiveModel::Serializer]
60
151
  # @yield [ActiveModel::Serializer]
61
152
  # @return [:nil, associated resource or resource collection]
62
- # @example
63
- # has_one :blog do |serializer|
64
- # serializer.cached_blog
65
- # end
66
- #
67
- # def cached_blog
68
- # cache_store.fetch("cached_blog:#{object.updated_at}") do
69
- # Blog.find(object.blog_id)
70
- # end
71
- # end
72
153
  def value(serializer, include_slice)
73
154
  @object = serializer.object
74
155
  @scope = serializer.scope
@@ -83,6 +164,11 @@ module ActiveModel
83
164
  end
84
165
  end
85
166
 
167
+ # @api private
168
+ def foreign_key_on
169
+ :related
170
+ end
171
+
86
172
  # Build association. This method is used internally to
87
173
  # build serializer's association by its reflection.
88
174
  #
@@ -103,61 +189,19 @@ module ActiveModel
103
189
  # comments_reflection.build_association(post_serializer, foo: 'bar')
104
190
  #
105
191
  # @api private
106
- #
107
192
  def build_association(parent_serializer, parent_serializer_options, include_slice = {})
108
- reflection_options = options.dup
109
-
110
- # Pass the parent's namespace onto the child serializer
111
- reflection_options[:namespace] ||= parent_serializer_options[:namespace]
112
-
113
- association_value = value(parent_serializer, include_slice)
114
- serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
115
- reflection_options[:include_data] = include_data?(include_slice)
116
- reflection_options[:links] = @_links
117
- reflection_options[:meta] = @_meta
118
-
119
- if serializer_class
120
- serializer = catch(:no_serializer) do
121
- serializer_class.new(
122
- association_value,
123
- serializer_options(parent_serializer, parent_serializer_options, reflection_options)
124
- )
125
- end
126
- if serializer.nil?
127
- reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
128
- else
129
- reflection_options[:serializer] = serializer
130
- end
131
- elsif !association_value.nil? && !association_value.instance_of?(Object)
132
- reflection_options[:virtual_value] = association_value
133
- end
134
-
135
- block = nil
136
- Association.new(name, reflection_options, block)
193
+ association_options = {
194
+ parent_serializer: parent_serializer,
195
+ parent_serializer_options: parent_serializer_options,
196
+ include_slice: include_slice
197
+ }
198
+ Association.new(self, association_options)
137
199
  end
138
200
 
139
201
  protected
140
202
 
203
+ # used in instance exec
141
204
  attr_accessor :object, :scope
142
-
143
- private
144
-
145
- def include_data?(include_slice)
146
- if @_include_data == :if_sideloaded
147
- include_slice.key?(name)
148
- else
149
- @_include_data
150
- end
151
- end
152
-
153
- def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
154
- serializer = reflection_options.fetch(:serializer, nil)
155
-
156
- serializer_options = parent_serializer_options.except(:serializer)
157
- serializer_options[:serializer] = serializer if serializer
158
- serializer_options[:serializer_context_class] = parent_serializer.class
159
- serializer_options
160
- end
161
205
  end
162
206
  end
163
207
  end