active_model_serializers 0.8.0 → 0.10.12

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 (98) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +702 -6
  3. data/{MIT-LICENSE.txt → MIT-LICENSE} +3 -2
  4. data/README.md +195 -536
  5. data/lib/action_controller/serialization.rb +53 -35
  6. data/lib/active_model/serializable_resource.rb +13 -0
  7. data/lib/active_model/serializer/adapter/attributes.rb +17 -0
  8. data/lib/active_model/serializer/adapter/base.rb +20 -0
  9. data/lib/active_model/serializer/adapter/json.rb +17 -0
  10. data/lib/active_model/serializer/adapter/json_api.rb +17 -0
  11. data/lib/active_model/serializer/adapter/null.rb +17 -0
  12. data/lib/active_model/serializer/adapter.rb +26 -0
  13. data/lib/active_model/serializer/array_serializer.rb +14 -0
  14. data/lib/active_model/serializer/association.rb +73 -0
  15. data/lib/active_model/serializer/attribute.rb +27 -0
  16. data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
  17. data/lib/active_model/serializer/collection_serializer.rb +90 -0
  18. data/lib/active_model/serializer/concerns/caching.rb +305 -0
  19. data/lib/active_model/serializer/error_serializer.rb +16 -0
  20. data/lib/active_model/serializer/errors_serializer.rb +34 -0
  21. data/lib/active_model/serializer/field.rb +92 -0
  22. data/lib/active_model/serializer/fieldset.rb +33 -0
  23. data/lib/active_model/serializer/has_many_reflection.rb +12 -0
  24. data/lib/active_model/serializer/has_one_reflection.rb +9 -0
  25. data/lib/active_model/serializer/lazy_association.rb +99 -0
  26. data/lib/active_model/serializer/link.rb +23 -0
  27. data/lib/active_model/serializer/lint.rb +152 -0
  28. data/lib/active_model/serializer/null.rb +19 -0
  29. data/lib/active_model/serializer/reflection.rb +212 -0
  30. data/lib/active_model/serializer/version.rb +7 -0
  31. data/lib/active_model/serializer.rb +352 -441
  32. data/lib/active_model_serializers/adapter/attributes.rb +36 -0
  33. data/lib/active_model_serializers/adapter/base.rb +85 -0
  34. data/lib/active_model_serializers/adapter/json.rb +23 -0
  35. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
  36. data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
  37. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
  38. data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
  39. data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
  40. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
  41. data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
  42. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
  43. data/lib/active_model_serializers/adapter/json_api.rb +535 -0
  44. data/lib/active_model_serializers/adapter/null.rb +11 -0
  45. data/lib/active_model_serializers/adapter.rb +100 -0
  46. data/lib/active_model_serializers/callbacks.rb +57 -0
  47. data/lib/active_model_serializers/deprecate.rb +56 -0
  48. data/lib/active_model_serializers/deserialization.rb +17 -0
  49. data/lib/active_model_serializers/json_pointer.rb +16 -0
  50. data/lib/active_model_serializers/logging.rb +124 -0
  51. data/lib/active_model_serializers/lookup_chain.rb +82 -0
  52. data/lib/active_model_serializers/model/caching.rb +25 -0
  53. data/lib/active_model_serializers/model.rb +132 -0
  54. data/lib/active_model_serializers/railtie.rb +52 -0
  55. data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
  56. data/lib/active_model_serializers/serializable_resource.rb +84 -0
  57. data/lib/active_model_serializers/serialization_context.rb +41 -0
  58. data/lib/active_model_serializers/test/schema.rb +140 -0
  59. data/lib/active_model_serializers/test/serializer.rb +127 -0
  60. data/lib/active_model_serializers/test.rb +9 -0
  61. data/lib/active_model_serializers.rb +49 -83
  62. data/lib/generators/rails/USAGE +6 -0
  63. data/lib/generators/rails/resource_override.rb +12 -0
  64. data/lib/generators/rails/serializer_generator.rb +38 -0
  65. data/lib/generators/rails/templates/serializer.rb.erb +8 -0
  66. data/lib/grape/active_model_serializers.rb +18 -0
  67. data/lib/grape/formatters/active_model_serializers.rb +34 -0
  68. data/lib/grape/helpers/active_model_serializers.rb +19 -0
  69. data/lib/tasks/rubocop.rake +55 -0
  70. metadata +291 -77
  71. data/.gitignore +0 -18
  72. data/.travis.yml +0 -29
  73. data/DESIGN.textile +0 -586
  74. data/Gemfile +0 -6
  75. data/Gemfile.edge +0 -9
  76. data/Rakefile +0 -18
  77. data/active_model_serializers.gemspec +0 -25
  78. data/bench/perf.rb +0 -43
  79. data/cruft.md +0 -19
  80. data/lib/active_model/array_serializer.rb +0 -104
  81. data/lib/active_model/serializer/associations.rb +0 -233
  82. data/lib/active_model/serializers/version.rb +0 -5
  83. data/lib/active_record/serializer_override.rb +0 -16
  84. data/lib/generators/resource_override.rb +0 -13
  85. data/lib/generators/serializer/USAGE +0 -9
  86. data/lib/generators/serializer/serializer_generator.rb +0 -42
  87. data/lib/generators/serializer/templates/serializer.rb +0 -19
  88. data/test/array_serializer_test.rb +0 -54
  89. data/test/association_test.rb +0 -592
  90. data/test/caching_test.rb +0 -96
  91. data/test/generators_test.rb +0 -85
  92. data/test/no_serialization_scope_test.rb +0 -34
  93. data/test/serialization_scope_name_test.rb +0 -67
  94. data/test/serialization_test.rb +0 -394
  95. data/test/serializer_support_test.rb +0 -51
  96. data/test/serializer_test.rb +0 -1452
  97. data/test/test_fakes.rb +0 -194
  98. data/test/test_helper.rb +0 -41
@@ -1,514 +1,425 @@
1
- require "active_support/core_ext/class/attribute"
2
- require "active_support/core_ext/module/anonymous"
3
- require 'active_support/dependencies'
4
- require 'active_support/descendants_tracker'
5
-
1
+ # frozen_string_literal: true
2
+
3
+ require 'jsonapi/include_directive'
4
+ require 'active_model/serializer/collection_serializer'
5
+ require 'active_model/serializer/array_serializer'
6
+ require 'active_model/serializer/error_serializer'
7
+ require 'active_model/serializer/errors_serializer'
8
+ require 'active_model/serializer/concerns/caching'
9
+ require 'active_model/serializer/fieldset'
10
+ require 'active_model/serializer/lint'
11
+
12
+ # ActiveModel::Serializer is an abstract class that is
13
+ # reified when subclassed to decorate a resource.
6
14
  module ActiveModel
7
- # Active Model Serializer
8
- #
9
- # Provides a basic serializer implementation that allows you to easily
10
- # control how a given object is going to be serialized. On initialization,
11
- # it expects two objects as arguments, a resource and options. For example,
12
- # one may do in a controller:
13
- #
14
- # PostSerializer.new(@post, :scope => current_user).to_json
15
- #
16
- # The object to be serialized is the +@post+ and the current user is passed
17
- # in for authorization purposes.
18
- #
19
- # We use the scope to check if a given attribute should be serialized or not.
20
- # For example, some attributes may only be returned if +current_user+ is the
21
- # author of the post:
22
- #
23
- # class PostSerializer < ActiveModel::Serializer
24
- # attributes :title, :body
25
- # has_many :comments
26
- #
27
- # private
28
- #
29
- # def attributes
30
- # hash = super
31
- # hash.merge!(:email => post.email) if author?
32
- # hash
33
- # end
34
- #
35
- # def author?
36
- # post.author == scope
37
- # end
38
- # end
39
- #
40
15
  class Serializer
41
- extend ActiveSupport::DescendantsTracker
42
-
43
- INCLUDE_METHODS = {}
44
- INSTRUMENT = { :serialize => :"serialize.serializer", :associations => :"associations.serializer" }
45
-
46
- class IncludeError < StandardError
47
- attr_reader :source, :association
48
-
49
- def initialize(source, association)
50
- @source, @association = source, association
51
- end
52
-
53
- def to_s
54
- "Cannot serialize #{association} when #{source} does not have a root!"
16
+ undef_method :select, :display # These IO methods, which are mixed into Kernel,
17
+ # sometimes conflict with attribute names. We don't need these IO methods.
18
+
19
+ # @see #serializable_hash for more details on these valid keys.
20
+ SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze
21
+ extend ActiveSupport::Autoload
22
+ eager_autoload do
23
+ autoload :Adapter
24
+ autoload :Null
25
+ autoload :Attribute
26
+ autoload :Link
27
+ autoload :Association
28
+ autoload :Reflection
29
+ autoload :BelongsToReflection
30
+ autoload :HasOneReflection
31
+ autoload :HasManyReflection
32
+ end
33
+ include ActiveSupport::Configurable
34
+ include Caching
35
+
36
+ # @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
37
+ # @return [ActiveModel::Serializer]
38
+ # Preferentially returns
39
+ # 1. resource.serializer_class
40
+ # 2. ArraySerializer when resource is a collection
41
+ # 3. options[:serializer]
42
+ # 4. lookup serializer when resource is a Class
43
+ def self.serializer_for(resource_or_class, options = {})
44
+ if resource_or_class.respond_to?(:serializer_class)
45
+ resource_or_class.serializer_class
46
+ elsif resource_or_class.respond_to?(:to_ary)
47
+ config.collection_serializer
48
+ else
49
+ resource_class = resource_or_class.class == Class ? resource_or_class : resource_or_class.class
50
+ options.fetch(:serializer) { get_serializer_for(resource_class, options[:namespace]) }
55
51
  end
56
52
  end
57
53
 
58
- class_attribute :_attributes
59
- self._attributes = {}
60
-
61
- class_attribute :_associations
62
- self._associations = {}
63
-
64
- class_attribute :_root
65
- class_attribute :_embed
66
- self._embed = :objects
67
- class_attribute :_root_embed
68
-
69
- class_attribute :cache
70
- class_attribute :perform_caching
71
-
54
+ # @see ActiveModelSerializers::Adapter.lookup
55
+ # Deprecated
56
+ def self.adapter
57
+ ActiveModelSerializers::Adapter.lookup(config.adapter)
58
+ end
72
59
  class << self
73
- # set perform caching like root
74
- def cached(value = true)
75
- self.perform_caching = value
76
- end
77
-
78
- # Define attributes to be used in the serialization.
79
- def attributes(*attrs)
80
-
81
- self._attributes = _attributes.dup
82
-
83
- attrs.each do |attr|
84
- if Hash === attr
85
- attr.each {|attr_real, key| attribute attr_real, :key => key }
86
- else
87
- attribute attr
88
- end
89
- end
90
- end
91
-
92
- def attribute(attr, options={})
93
- self._attributes = _attributes.merge(attr.is_a?(Hash) ? attr : {attr => options[:key] || attr.to_s.gsub(/\?$/, '').to_sym})
94
-
95
- attr = attr.keys[0] if attr.is_a? Hash
96
-
97
- unless method_defined?(attr)
98
- define_method attr do
99
- object.read_attribute_for_serialization(attr.to_sym)
100
- end
101
- end
102
-
103
- define_include_method attr
104
-
105
- # protect inheritance chains and open classes
106
- # if a serializer inherits from another OR
107
- # attributes are added later in a classes lifecycle
108
- # poison the cache
109
- define_method :_fast_attributes do
110
- raise NameError
111
- end
112
-
113
- end
114
-
115
- def associate(klass, attrs) #:nodoc:
116
- options = attrs.extract_options!
117
- self._associations = _associations.dup
118
-
119
- attrs.each do |attr|
120
- unless method_defined?(attr)
121
- define_method attr do
122
- object.send attr
123
- end
124
- end
125
-
126
- define_include_method attr
127
-
128
- self._associations[attr] = klass.refine(attr, options)
129
- end
130
- end
60
+ extend ActiveModelSerializers::Deprecate
61
+ deprecate :adapter, 'ActiveModelSerializers::Adapter.configured_adapter'
62
+ end
131
63
 
132
- def define_include_method(name)
133
- method = "include_#{name}?".to_sym
64
+ # @api private
65
+ def self.serializer_lookup_chain_for(klass, namespace = nil)
66
+ lookups = ActiveModelSerializers.config.serializer_lookup_chain
67
+ Array[*lookups].flat_map do |lookup|
68
+ lookup.call(klass, self, namespace)
69
+ end.compact
70
+ end
134
71
 
135
- INCLUDE_METHODS[name] = method
72
+ # Used to cache serializer name => serializer class
73
+ # when looked up by Serializer.get_serializer_for.
74
+ def self.serializers_cache
75
+ @serializers_cache ||= Concurrent::Map.new
76
+ end
136
77
 
137
- unless method_defined?(method)
138
- define_method method do
139
- true
140
- end
78
+ # @api private
79
+ # Find a serializer from a class and caches the lookup.
80
+ # Preferentially returns:
81
+ # 1. class name appended with "Serializer"
82
+ # 2. try again with superclass, if present
83
+ # 3. nil
84
+ def self.get_serializer_for(klass, namespace = nil)
85
+ return nil unless config.serializer_lookup_enabled
86
+
87
+ cache_key = ActiveSupport::Cache.expand_cache_key(klass, namespace)
88
+ serializers_cache.fetch_or_store(cache_key) do
89
+ # NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
90
+ lookup_chain = serializer_lookup_chain_for(klass, namespace)
91
+ serializer_class = lookup_chain.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
92
+
93
+ if serializer_class
94
+ serializer_class
95
+ elsif klass.superclass
96
+ get_serializer_for(klass.superclass, namespace)
97
+ else
98
+ nil # No serializer found
141
99
  end
142
100
  end
101
+ end
143
102
 
144
- # Defines an association in the object should be rendered.
145
- #
146
- # The serializer object should implement the association name
147
- # as a method which should return an array when invoked. If a method
148
- # with the association name does not exist, the association name is
149
- # dispatched to the serialized object.
150
- def has_many(*attrs)
151
- associate(Associations::HasMany, attrs)
152
- end
153
-
154
- # Defines an association in the object should be rendered.
155
- #
156
- # The serializer object should implement the association name
157
- # as a method which should return an object when invoked. If a method
158
- # with the association name does not exist, the association name is
159
- # dispatched to the serialized object.
160
- def has_one(*attrs)
161
- associate(Associations::HasOne, attrs)
103
+ # @api private
104
+ def self.include_directive_from_options(options)
105
+ if options[:include_directive]
106
+ options[:include_directive]
107
+ elsif options[:include]
108
+ JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
109
+ else
110
+ ActiveModelSerializers.default_include_directive
162
111
  end
112
+ end
163
113
 
164
- # Return a schema hash for the current serializer. This information
165
- # can be used to generate clients for the serialized output.
166
- #
167
- # The schema hash has two keys: +attributes+ and +associations+.
168
- #
169
- # The +attributes+ hash looks like this:
170
- #
171
- # { :name => :string, :age => :integer }
172
- #
173
- # The +associations+ hash looks like this:
174
- # { :posts => { :has_many => :posts } }
175
- #
176
- # If :key is used:
177
- #
178
- # class PostsSerializer < ActiveModel::Serializer
179
- # has_many :posts, :key => :my_posts
180
- # end
181
- #
182
- # the hash looks like this:
183
- #
184
- # { :my_posts => { :has_many => :posts }
185
- #
186
- # This information is extracted from the serializer's model class,
187
- # which is provided by +SerializerClass.model_class+.
188
- #
189
- # The schema method uses the +columns_hash+ and +reflect_on_association+
190
- # methods, provided by default by ActiveRecord. You can implement these
191
- # methods on your custom models if you want the serializer's schema method
192
- # to work.
193
- #
194
- # TODO: This is currently coupled to Active Record. We need to
195
- # figure out a way to decouple those two.
196
- def schema
197
- klass = model_class
198
- columns = klass.columns_hash
199
-
200
- attrs = {}
201
- _attributes.each do |name, key|
202
- if column = columns[name.to_s]
203
- attrs[key] = column.type
204
- else
205
- # Computed attribute (method on serializer or model). We cannot
206
- # infer the type, so we put nil, unless specified in the attribute declaration
207
- if name != key
208
- attrs[name] = key
209
- else
210
- attrs[key] = nil
211
- end
212
- end
213
- end
214
-
215
- associations = {}
216
- _associations.each do |attr, association_class|
217
- association = association_class.new(attr, self)
218
-
219
- if model_association = klass.reflect_on_association(association.name)
220
- # Real association.
221
- associations[association.key] = { model_association.macro => model_association.name }
222
- else
223
- # Computed association. We could infer has_many vs. has_one from
224
- # the association class, but that would make it different from
225
- # real associations, which read has_one vs. belongs_to from the
226
- # model.
227
- associations[association.key] = nil
228
- end
229
- end
114
+ # @api private
115
+ def self.serialization_adapter_instance
116
+ @serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
117
+ end
230
118
 
231
- { :attributes => attrs, :associations => associations }
232
- end
119
+ # Preferred interface is ActiveModelSerializers.config
120
+ # BEGIN DEFAULT CONFIGURATION
121
+ config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
122
+ config.serializer_lookup_enabled = true
233
123
 
234
- # The model class associated with this serializer.
235
- def model_class
236
- name.sub(/Serializer$/, '').constantize
237
- end
124
+ # @deprecated Use {#config.collection_serializer=} instead of this. Is
125
+ # compatibility layer for ArraySerializer.
126
+ def config.array_serializer=(collection_serializer)
127
+ self.collection_serializer = collection_serializer
128
+ end
238
129
 
239
- # Define how associations should be embedded.
240
- #
241
- # embed :objects # Embed associations as full objects
242
- # embed :ids # Embed only the association ids
243
- # embed :ids, :include => true # Embed the association ids and include objects in the root
244
- #
245
- def embed(type, options={})
246
- self._embed = type
247
- self._root_embed = true if options[:include]
248
- end
130
+ # @deprecated Use {#config.collection_serializer} instead of this. Is
131
+ # compatibility layer for ArraySerializer.
132
+ def config.array_serializer
133
+ collection_serializer
134
+ end
249
135
 
250
- # Defines the root used on serialization. If false, disables the root.
251
- def root(name)
252
- self._root = name
253
- end
254
- alias_method :root=, :root
255
-
256
- # Used internally to create a new serializer object based on controller
257
- # settings and options for a given resource. These settings are typically
258
- # set during the request lifecycle or by the controller class, and should
259
- # not be manually defined for this method.
260
- def build_json(controller, resource, options)
261
- default_options = controller.send(:default_serializer_options) || {}
262
- options = default_options.merge(options || {})
263
-
264
- serializer = options.delete(:serializer) ||
265
- (resource.respond_to?(:active_model_serializer) &&
266
- resource.active_model_serializer)
267
-
268
- return serializer unless serializer
269
-
270
- if resource.respond_to?(:to_ary)
271
- unless serializer <= ActiveModel::ArraySerializer
272
- raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " +
273
- "You may want to use the :each_serializer option instead.")
274
- end
275
-
276
- if options[:root] != false && serializer.root != false
277
- # the serializer for an Array is ActiveModel::ArraySerializer
278
- options[:root] ||= serializer.root || controller.controller_name
279
- end
280
- end
136
+ config.default_includes = '*'
137
+ config.adapter = :attributes
138
+ config.key_transform = nil
139
+ config.jsonapi_pagination_links_enabled = true
140
+ config.jsonapi_resource_type = :plural
141
+ config.jsonapi_namespace_separator = '-'.freeze
142
+ config.jsonapi_version = '1.0'
143
+ config.jsonapi_toplevel_meta = {}
144
+ # Make JSON API top-level jsonapi member opt-in
145
+ # ref: http://jsonapi.org/format/#document-top-level
146
+ config.jsonapi_include_toplevel_object = false
147
+ config.jsonapi_use_foreign_key_on_belongs_to_relationship = false
148
+ config.include_data_default = true
149
+
150
+ # For configuring how serializers are found.
151
+ # This should be an array of procs.
152
+ #
153
+ # The priority of the output is that the first item
154
+ # in the evaluated result array will take precedence
155
+ # over other possible serializer paths.
156
+ #
157
+ # i.e.: First match wins.
158
+ #
159
+ # @example output
160
+ # => [
161
+ # "CustomNamespace::ResourceSerializer",
162
+ # "ParentSerializer::ResourceSerializer",
163
+ # "ResourceNamespace::ResourceSerializer" ,
164
+ # "ResourceSerializer"]
165
+ #
166
+ # If CustomNamespace::ResourceSerializer exists, it will be used
167
+ # for serialization
168
+ config.serializer_lookup_chain = ActiveModelSerializers::LookupChain::DEFAULT.dup
281
169
 
282
- options[:scope] = controller.serialization_scope unless options.has_key?(:scope)
283
- options[:scope_name] = controller._serialization_scope
284
- options[:url_options] = controller.url_options
170
+ config.schema_path = 'test/support/schemas'
171
+ # END DEFAULT CONFIGURATION
285
172
 
286
- serializer.new(resource, options)
287
- end
173
+ with_options instance_writer: false, instance_reader: false do |serializer|
174
+ serializer.class_attribute :_attributes_data # @api private
175
+ self._attributes_data ||= {}
176
+ end
177
+ with_options instance_writer: false, instance_reader: true do |serializer|
178
+ serializer.class_attribute :_reflections
179
+ self._reflections ||= {}
180
+ serializer.class_attribute :_links # @api private
181
+ self._links ||= {}
182
+ serializer.class_attribute :_meta # @api private
183
+ serializer.class_attribute :_type # @api private
288
184
  end
289
185
 
290
- attr_reader :object, :options
291
-
292
- def initialize(object, options={})
293
- @object, @options = object, options
186
+ def self.inherited(base)
187
+ super
188
+ base._attributes_data = _attributes_data.dup
189
+ base._reflections = _reflections.dup
190
+ base._links = _links.dup
191
+ end
294
192
 
295
- scope_name = @options[:scope_name]
296
- if scope_name && !respond_to?(scope_name)
297
- self.class.class_eval do
298
- define_method scope_name, lambda { scope }
299
- end
300
- end
193
+ # @return [Array<Symbol>] Key names of declared attributes
194
+ # @see Serializer::attribute
195
+ def self._attributes
196
+ _attributes_data.keys
301
197
  end
302
198
 
303
- def root_name
304
- return false if self._root == false
199
+ # BEGIN SERIALIZER MACROS
305
200
 
306
- class_name = self.class.name.demodulize.underscore.sub(/_serializer$/, '').to_sym unless self.class.name.blank?
201
+ # @example
202
+ # class AdminAuthorSerializer < ActiveModel::Serializer
203
+ # attributes :id, :name, :recent_edits
204
+ def self.attributes(*attrs)
205
+ attrs = attrs.first if attrs.first.class == Array
307
206
 
308
- if self._root == true
309
- class_name
310
- else
311
- self._root || class_name
207
+ attrs.each do |attr|
208
+ attribute(attr)
312
209
  end
313
210
  end
314
211
 
315
- def url_options
316
- @options[:url_options] || {}
212
+ # @example
213
+ # class AdminAuthorSerializer < ActiveModel::Serializer
214
+ # attributes :id, :recent_edits
215
+ # attribute :name, key: :title
216
+ #
217
+ # attribute :full_name do
218
+ # "#{object.first_name} #{object.last_name}"
219
+ # end
220
+ #
221
+ # def recent_edits
222
+ # object.edits.last(5)
223
+ # end
224
+ def self.attribute(attr, options = {}, &block)
225
+ key = options.fetch(:key, attr)
226
+ _attributes_data[key] = Attribute.new(attr, options, block)
317
227
  end
318
228
 
319
- def meta_key
320
- @options[:meta_key].try(:to_sym) || :meta
229
+ # @param [Symbol] name of the association
230
+ # @param [Hash<Symbol => any>] options for the reflection
231
+ # @return [void]
232
+ #
233
+ # @example
234
+ # has_many :comments, serializer: CommentSummarySerializer
235
+ #
236
+ def self.has_many(name, options = {}, &block) # rubocop:disable Style/PredicateName
237
+ associate(HasManyReflection.new(name, options, block))
321
238
  end
322
239
 
323
- def include_meta(hash)
324
- hash[meta_key] = @options[:meta] if @options.has_key?(:meta)
240
+ # @param [Symbol] name of the association
241
+ # @param [Hash<Symbol => any>] options for the reflection
242
+ # @return [void]
243
+ #
244
+ # @example
245
+ # belongs_to :author, serializer: AuthorSerializer
246
+ #
247
+ def self.belongs_to(name, options = {}, &block)
248
+ associate(BelongsToReflection.new(name, options, block))
325
249
  end
326
250
 
327
- def to_json(*args)
328
- if perform_caching?
329
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do
330
- super
331
- end
332
- else
333
- super
334
- end
251
+ # @param [Symbol] name of the association
252
+ # @param [Hash<Symbol => any>] options for the reflection
253
+ # @return [void]
254
+ #
255
+ # @example
256
+ # has_one :author, serializer: AuthorSerializer
257
+ #
258
+ def self.has_one(name, options = {}, &block) # rubocop:disable Style/PredicateName
259
+ associate(HasOneReflection.new(name, options, block))
335
260
  end
336
261
 
337
- # Returns a json representation of the serializable
338
- # object including the root.
339
- def as_json(options={})
340
- if root = options.fetch(:root, @options.fetch(:root, root_name))
341
- @options[:hash] = hash = {}
342
- @options[:unique_values] = {}
343
-
344
- hash.merge!(root => serializable_hash)
345
- include_meta hash
346
- hash
347
- else
348
- serializable_hash
349
- end
262
+ # Add reflection and define {name} accessor.
263
+ # @param [ActiveModel::Serializer::Reflection] reflection
264
+ # @return [void]
265
+ #
266
+ # @api private
267
+ def self.associate(reflection)
268
+ key = reflection.options[:key] || reflection.name
269
+ self._reflections[key] = reflection
350
270
  end
351
-
352
- # Returns a hash representation of the serializable
353
- # object without the root.
354
- def serializable_hash
355
- if perform_caching?
356
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-hash']) do
357
- _serializable_hash
358
- end
359
- else
360
- _serializable_hash
361
- end
271
+ private_class_method :associate
272
+
273
+ # Define a link on a serializer.
274
+ # @example
275
+ # link(:self) { resource_url(object) }
276
+ # @example
277
+ # link(:self) { "http://example.com/resource/#{object.id}" }
278
+ # @example
279
+ # link :resource, "http://example.com/resource"
280
+ # @example
281
+ # link(:callback, if: :internal?), { "http://example.com/callback" }
282
+ #
283
+ def self.link(name, *args, &block)
284
+ options = args.extract_options!
285
+ # For compatibility with the use case of passing link directly as string argument
286
+ # without block, we are creating a wrapping block
287
+ _links[name] = Link.new(name, options, block || ->(_serializer) { args.first })
362
288
  end
363
289
 
364
- def include_associations!
365
- _associations.each_key do |name|
366
- include!(name) if include?(name)
367
- end
290
+ # Set the JSON API meta attribute of a serializer.
291
+ # @example
292
+ # class AdminAuthorSerializer < ActiveModel::Serializer
293
+ # meta { stuff: 'value' }
294
+ # @example
295
+ # meta do
296
+ # { comment_count: object.comments.count }
297
+ # end
298
+ def self.meta(value = nil, &block)
299
+ self._meta = block || value
368
300
  end
369
301
 
370
- def include?(name)
371
- return false if options.key?(:only) && !Array(options[:only]).include?(name)
372
- return false if options.key?(:except) && Array(options[:except]).include?(name)
373
- send INCLUDE_METHODS[name]
374
- end
375
-
376
- def include!(name, options={})
377
- # Make sure that if a special options[:hash] was passed in, we generate
378
- # a new unique values hash and don't clobber the original. If the hash
379
- # passed in is the same as the current options hash, use the current
380
- # unique values.
381
- #
382
- # TODO: Should passing in a Hash even be public API here?
383
- unique_values =
384
- if hash = options[:hash]
385
- if @options[:hash] == hash
386
- @options[:unique_values] ||= {}
387
- else
388
- {}
389
- end
390
- else
391
- hash = @options[:hash]
392
- @options[:unique_values] ||= {}
393
- end
302
+ # Set the JSON API type of a serializer.
303
+ # @example
304
+ # class AdminAuthorSerializer < ActiveModel::Serializer
305
+ # type 'authors'
306
+ def self.type(type)
307
+ self._type = type && type.to_s
308
+ end
394
309
 
395
- node = options[:node] ||= @node
396
- value = options[:value]
310
+ # END SERIALIZER MACROS
397
311
 
398
- if options[:include] == nil
399
- if @options.key?(:include)
400
- options[:include] = @options[:include].include?(name)
401
- elsif @options.include?(:exclude)
402
- options[:include] = !@options[:exclude].include?(name)
403
- end
404
- end
312
+ attr_accessor :object, :root, :scope
405
313
 
406
- association_class =
407
- if klass = _associations[name]
408
- klass
409
- elsif value.respond_to?(:to_ary)
410
- Associations::HasMany
411
- else
412
- Associations::HasOne
413
- end
314
+ # `scope_name` is set as :current_user by default in the controller.
315
+ # If the instance does not have a method named `scope_name`, it
316
+ # defines the method so that it calls the +scope+.
317
+ def initialize(object, options = {})
318
+ self.object = object
319
+ self.instance_options = options
320
+ self.root = instance_options[:root]
321
+ self.scope = instance_options[:scope]
414
322
 
415
- association = association_class.new(name, self, options)
323
+ return if !(scope_name = instance_options[:scope_name]) || respond_to?(scope_name)
416
324
 
417
- if association.embed_ids?
418
- node[association.key] = association.serialize_ids
325
+ define_singleton_method scope_name, -> { scope }
326
+ end
419
327
 
420
- if association.embed_in_root? && hash.nil?
421
- raise IncludeError.new(self.class, association.name)
422
- elsif association.embed_in_root? && association.embeddable?
423
- merge_association hash, association.root, association.serializables, unique_values
424
- end
425
- elsif association.embed_objects?
426
- node[association.key] = association.serialize
427
- end
328
+ def success?
329
+ true
428
330
  end
429
331
 
430
- # In some cases, an Array of associations is built by merging the associated
431
- # content for all of the children. For instance, if a Post has_many comments,
432
- # which has_many tags, the top-level :tags key will contain the merged list
433
- # of all tags for all comments of the post.
434
- #
435
- # In order to make this efficient, we store a :unique_values hash containing
436
- # a unique list of all of the objects that are already in the Array. This
437
- # avoids the need to scan through the Array looking for entries every time
438
- # we want to merge a new list of values.
439
- def merge_association(hash, key, serializables, unique_values)
440
- already_serialized = (unique_values[key] ||= {})
441
- serializable_hashes = (hash[key] ||= [])
442
-
443
- serializables.each do |serializable|
444
- unless already_serialized.include? serializable.object
445
- already_serialized[serializable.object] = true
446
- serializable_hashes << serializable.serializable_hash
447
- end
332
+ # Return the +attributes+ of +object+ as presented
333
+ # by the serializer.
334
+ def attributes(requested_attrs = nil, reload = false)
335
+ @attributes = nil if reload
336
+ @attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
337
+ next if attr.excluded?(self)
338
+ next unless requested_attrs.nil? || requested_attrs.include?(key)
339
+ hash[key] = attr.value(self)
448
340
  end
449
341
  end
450
342
 
451
- # Returns a hash representation of the serializable
452
- # object attributes.
453
- def attributes
454
- _fast_attributes
455
- rescue NameError
456
- method = "def _fast_attributes\n"
343
+ # @param [JSONAPI::IncludeDirective] include_directive (defaults to the
344
+ # +default_include_directive+ config value when not provided)
345
+ # @return [Enumerator<Association>]
346
+ def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
347
+ include_slice ||= include_directive
348
+ return Enumerator.new {} unless object
457
349
 
458
- method << " h = {}\n"
350
+ Enumerator.new do |y|
351
+ (self.instance_reflections ||= self.class._reflections.deep_dup).each do |key, reflection|
352
+ next if reflection.excluded?(self)
353
+ next unless include_directive.key?(key)
459
354
 
460
- _attributes.each do |name,key|
461
- method << " h[:\"#{key}\"] = read_attribute_for_serialization(:\"#{name}\") if include?(:\"#{name}\")\n"
355
+ association = reflection.build_association(self, instance_options, include_slice)
356
+ y.yield association
462
357
  end
463
- method << " h\nend"
464
-
465
- self.class.class_eval method
466
- _fast_attributes
358
+ end
467
359
  end
468
360
 
469
- # Returns options[:scope]
470
- def scope
471
- @options[:scope]
361
+ # @return [Hash] containing the attributes and first level
362
+ # associations, similar to how ActiveModel::Serializers::JSON is used
363
+ # in ActiveRecord::Base.
364
+ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
365
+ adapter_options ||= {}
366
+ options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
367
+ if (fieldset = adapter_options[:fieldset])
368
+ options[:fields] = fieldset.fields_for(json_key)
369
+ end
370
+ resource = attributes_hash(adapter_options, options, adapter_instance)
371
+ relationships = associations_hash(adapter_options, options, adapter_instance)
372
+ resource.merge(relationships)
472
373
  end
374
+ alias to_hash serializable_hash
375
+ alias to_h serializable_hash
473
376
 
474
- alias :read_attribute_for_serialization :send
475
-
476
- def _serializable_hash
477
- return nil if @object.nil?
478
- @node = attributes
479
- include_associations! if _embed
480
- @node
377
+ # @see #serializable_hash
378
+ def as_json(adapter_opts = nil)
379
+ serializable_hash(adapter_opts)
481
380
  end
482
381
 
483
- def perform_caching?
484
- perform_caching && cache && respond_to?(:cache_key)
382
+ # Used by adapter as resource root.
383
+ def json_key
384
+ root || _type ||
385
+ begin
386
+ object.class.model_name.to_s.underscore
387
+ rescue ArgumentError
388
+ 'anonymous_object'
389
+ end
485
390
  end
486
391
 
487
- def expand_cache_key(*args)
488
- ActiveSupport::Cache.expand_cache_key(args)
392
+ def read_attribute_for_serialization(attr)
393
+ if respond_to?(attr)
394
+ send(attr)
395
+ else
396
+ object.read_attribute_for_serialization(attr)
397
+ end
489
398
  end
490
399
 
491
- # Use ActiveSupport::Notifications to send events to external systems.
492
- # The event name is: name.class_name.serializer
493
- def instrument(name, payload = {}, &block)
494
- event_name = INSTRUMENT[name]
495
- ActiveSupport::Notifications.instrument(event_name, payload, &block)
400
+ # @api private
401
+ def attributes_hash(_adapter_options, options, adapter_instance)
402
+ if self.class.cache_enabled?
403
+ fetch_attributes(options[:fields], options[:cached_attributes] || {}, adapter_instance)
404
+ elsif self.class.fragment_cache_enabled?
405
+ fetch_attributes_fragment(adapter_instance, options[:cached_attributes] || {})
406
+ else
407
+ attributes(options[:fields], true)
408
+ end
496
409
  end
497
- end
498
-
499
- # DefaultSerializer
500
- #
501
- # Provides a constant interface for all items, particularly
502
- # for ArraySerializer.
503
- class DefaultSerializer
504
- attr_reader :object, :options
505
410
 
506
- def initialize(object, options={})
507
- @object, @options = object, options
411
+ # @api private
412
+ def associations_hash(adapter_options, options, adapter_instance)
413
+ include_directive = options.fetch(:include_directive)
414
+ include_slice = options[:include_slice]
415
+ associations(include_directive, include_slice).each_with_object({}) do |association, relationships|
416
+ adapter_opts = adapter_options.merge(include_directive: include_directive[association.key], adapter_instance: adapter_instance)
417
+ relationships[association.key] = association.serializable_hash(adapter_opts, adapter_instance)
418
+ end
508
419
  end
509
420
 
510
- def serializable_hash
511
- @object.as_json(@options)
512
- end
421
+ protected
422
+
423
+ attr_accessor :instance_options, :instance_reflections
513
424
  end
514
425
  end