active_model_serializers 0.10.5 → 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
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