active_model_serializers 0.8.0 → 0.10.12

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