active_model_serializers 0.9.0 → 0.10.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +679 -9
  3. data/MIT-LICENSE +3 -2
  4. data/README.md +195 -753
  5. data/lib/action_controller/serialization.rb +45 -49
  6. data/lib/active_model/serializable_resource.rb +13 -0
  7. data/lib/active_model/serializer.rb +369 -212
  8. data/lib/active_model/serializer/adapter.rb +26 -0
  9. data/lib/active_model/serializer/adapter/attributes.rb +17 -0
  10. data/lib/active_model/serializer/adapter/base.rb +20 -0
  11. data/lib/active_model/serializer/adapter/json.rb +17 -0
  12. data/lib/active_model/serializer/adapter/json_api.rb +17 -0
  13. data/lib/active_model/serializer/adapter/null.rb +17 -0
  14. data/lib/active_model/serializer/array_serializer.rb +14 -0
  15. data/lib/active_model/serializer/association.rb +73 -0
  16. data/lib/active_model/serializer/attribute.rb +27 -0
  17. data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
  18. data/lib/active_model/serializer/collection_serializer.rb +90 -0
  19. data/lib/active_model/serializer/concerns/caching.rb +305 -0
  20. data/lib/active_model/serializer/error_serializer.rb +16 -0
  21. data/lib/active_model/serializer/errors_serializer.rb +34 -0
  22. data/lib/active_model/serializer/field.rb +92 -0
  23. data/lib/active_model/serializer/fieldset.rb +33 -0
  24. data/lib/active_model/serializer/has_many_reflection.rb +12 -0
  25. data/lib/active_model/serializer/has_one_reflection.rb +9 -0
  26. data/lib/active_model/serializer/lazy_association.rb +99 -0
  27. data/lib/active_model/serializer/link.rb +23 -0
  28. data/lib/active_model/serializer/lint.rb +152 -0
  29. data/lib/active_model/serializer/null.rb +19 -0
  30. data/lib/active_model/serializer/reflection.rb +212 -0
  31. data/lib/active_model/serializer/version.rb +3 -1
  32. data/lib/active_model_serializers.rb +60 -17
  33. data/lib/active_model_serializers/adapter.rb +100 -0
  34. data/lib/active_model_serializers/adapter/attributes.rb +36 -0
  35. data/lib/active_model_serializers/adapter/base.rb +85 -0
  36. data/lib/active_model_serializers/adapter/json.rb +23 -0
  37. data/lib/active_model_serializers/adapter/json_api.rb +535 -0
  38. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
  39. data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
  40. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
  41. data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
  42. data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
  43. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
  44. data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
  45. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
  46. data/lib/active_model_serializers/adapter/null.rb +11 -0
  47. data/lib/active_model_serializers/callbacks.rb +57 -0
  48. data/lib/active_model_serializers/deprecate.rb +56 -0
  49. data/lib/active_model_serializers/deserialization.rb +17 -0
  50. data/lib/active_model_serializers/json_pointer.rb +16 -0
  51. data/lib/active_model_serializers/logging.rb +124 -0
  52. data/lib/active_model_serializers/lookup_chain.rb +82 -0
  53. data/lib/active_model_serializers/model.rb +132 -0
  54. data/lib/active_model_serializers/model/caching.rb +25 -0
  55. data/lib/active_model_serializers/railtie.rb +52 -0
  56. data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
  57. data/lib/active_model_serializers/serializable_resource.rb +84 -0
  58. data/lib/active_model_serializers/serialization_context.rb +41 -0
  59. data/lib/active_model_serializers/test.rb +9 -0
  60. data/lib/active_model_serializers/test/schema.rb +140 -0
  61. data/lib/active_model_serializers/test/serializer.rb +127 -0
  62. data/lib/generators/rails/USAGE +6 -0
  63. data/lib/{active_model/serializer/generators → generators/rails}/resource_override.rb +3 -4
  64. data/lib/{active_model/serializer/generators/serializer → generators/rails}/serializer_generator.rb +6 -5
  65. data/lib/{active_model/serializer/generators/serializer/templates/serializer.rb → generators/rails/templates/serializer.rb.erb} +0 -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 +315 -99
  71. data/CONTRIBUTING.md +0 -20
  72. data/DESIGN.textile +0 -586
  73. data/lib/action_controller/serialization_test_case.rb +0 -79
  74. data/lib/active_model/array_serializer.rb +0 -65
  75. data/lib/active_model/default_serializer.rb +0 -32
  76. data/lib/active_model/serializable.rb +0 -40
  77. data/lib/active_model/serializer/associations.rb +0 -102
  78. data/lib/active_model/serializer/config.rb +0 -31
  79. data/lib/active_model/serializer/generators/serializer/USAGE +0 -9
  80. data/lib/active_model/serializer/generators/serializer/scaffold_controller_generator.rb +0 -14
  81. data/lib/active_model/serializer/generators/serializer/templates/controller.rb +0 -93
  82. data/lib/active_model/serializer/railtie.rb +0 -10
  83. data/lib/active_model/serializer_support.rb +0 -5
  84. data/test/fixtures/active_record.rb +0 -92
  85. data/test/fixtures/poro.rb +0 -75
  86. data/test/integration/action_controller/serialization_test.rb +0 -287
  87. data/test/integration/action_controller/serialization_test_case_test.rb +0 -61
  88. data/test/integration/active_record/active_record_test.rb +0 -77
  89. data/test/integration/generators/resource_generator_test.rb +0 -26
  90. data/test/integration/generators/scaffold_controller_generator_test.rb +0 -64
  91. data/test/integration/generators/serializer_generator_test.rb +0 -41
  92. data/test/test_app.rb +0 -11
  93. data/test/test_helper.rb +0 -24
  94. data/test/unit/active_model/array_serializer/except_test.rb +0 -18
  95. data/test/unit/active_model/array_serializer/key_format_test.rb +0 -18
  96. data/test/unit/active_model/array_serializer/meta_test.rb +0 -53
  97. data/test/unit/active_model/array_serializer/only_test.rb +0 -18
  98. data/test/unit/active_model/array_serializer/root_test.rb +0 -102
  99. data/test/unit/active_model/array_serializer/scope_test.rb +0 -24
  100. data/test/unit/active_model/array_serializer/serialization_test.rb +0 -199
  101. data/test/unit/active_model/default_serializer_test.rb +0 -13
  102. data/test/unit/active_model/serializer/associations/build_serializer_test.rb +0 -21
  103. data/test/unit/active_model/serializer/associations_test.rb +0 -19
  104. data/test/unit/active_model/serializer/attributes_test.rb +0 -41
  105. data/test/unit/active_model/serializer/config_test.rb +0 -88
  106. data/test/unit/active_model/serializer/filter_test.rb +0 -69
  107. data/test/unit/active_model/serializer/has_many_test.rb +0 -230
  108. data/test/unit/active_model/serializer/has_one_test.rb +0 -207
  109. data/test/unit/active_model/serializer/key_format_test.rb +0 -25
  110. data/test/unit/active_model/serializer/meta_test.rb +0 -39
  111. data/test/unit/active_model/serializer/options_test.rb +0 -15
  112. data/test/unit/active_model/serializer/root_test.rb +0 -117
  113. data/test/unit/active_model/serializer/scope_test.rb +0 -49
@@ -1,79 +1,75 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/class/attribute'
4
+ require 'active_model_serializers/serialization_context'
2
5
 
3
6
  module ActionController
4
- # Action Controller Serialization
5
- #
6
- # Overrides render :json to check if the given object implements +active_model_serializer+
7
- # as a method. If so, use the returned serializer instead of calling +to_json+ on the object.
8
- #
9
- # This module also provides a serialization_scope method that allows you to configure the
10
- # +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+
11
- # to the current user:
12
- #
13
- # class ApplicationController < ActionController::Base
14
- # serialization_scope :current_user
15
- # end
16
- #
17
- # If you need more complex scope rules, you can simply override the serialization_scope:
18
- #
19
- # class ApplicationController < ActionController::Base
20
- # private
21
- #
22
- # def serialization_scope
23
- # current_user
24
- # end
25
- # end
26
- #
27
7
  module Serialization
28
8
  extend ActiveSupport::Concern
29
9
 
30
10
  include ActionController::Renderers
31
11
 
32
- class << self
33
- attr_accessor :enabled
12
+ module ClassMethods
13
+ def serialization_scope(scope)
14
+ self._serialization_scope = scope
15
+ end
34
16
  end
35
- self.enabled = true
36
17
 
37
18
  included do
38
19
  class_attribute :_serialization_scope
39
20
  self._serialization_scope = :current_user
40
- end
41
21
 
42
- module ClassMethods
43
- def serialization_scope(scope)
44
- self._serialization_scope = scope
45
- end
22
+ attr_writer :namespace_for_serializer
46
23
  end
47
24
 
48
- def _render_option_json(resource, options)
49
- serializer = build_json_serializer(resource, options)
25
+ def namespace_for_serializer
26
+ @namespace_for_serializer ||= namespace_for_class(self.class) unless namespace_for_class(self.class) == Object
27
+ end
50
28
 
51
- if serializer
52
- super(serializer, options)
29
+ def namespace_for_class(klass)
30
+ if Module.method_defined?(:module_parent)
31
+ klass.module_parent
53
32
  else
54
- super
33
+ klass.parent
55
34
  end
56
35
  end
57
36
 
58
- private
37
+ def serialization_scope
38
+ return unless _serialization_scope && respond_to?(_serialization_scope, true)
59
39
 
60
- def default_serializer_options
61
- {}
40
+ send(_serialization_scope)
62
41
  end
63
42
 
64
- def serialization_scope
65
- _serialization_scope = self.class._serialization_scope
66
- send(_serialization_scope) if _serialization_scope && respond_to?(_serialization_scope, true)
67
- end
43
+ def get_serializer(resource, options = {})
44
+ unless use_adapter?
45
+ warn 'ActionController::Serialization#use_adapter? has been removed. '\
46
+ "Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
47
+ options[:adapter] = false
48
+ end
49
+
50
+ options.fetch(:namespace) { options[:namespace] = namespace_for_serializer }
68
51
 
69
- def build_json_serializer(resource, options = {})
70
- options = default_serializer_options.merge(options)
52
+ serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)
53
+ serializable_resource.serialization_scope ||= options.fetch(:scope) { serialization_scope }
54
+ serializable_resource.serialization_scope_name = options.fetch(:scope_name) { _serialization_scope }
55
+ # For compatibility with the JSON renderer: `json.to_json(options) if json.is_a?(String)`.
56
+ # Otherwise, since `serializable_resource` is not a string, the renderer would call
57
+ # `to_json` on a String and given odd results, such as `"".to_json #=> '""'`
58
+ serializable_resource.adapter.is_a?(String) ? serializable_resource.adapter : serializable_resource
59
+ end
71
60
 
72
- if serializer = options.fetch(:serializer, ActiveModel::Serializer.serializer_for(resource))
73
- options[:scope] = serialization_scope unless options.has_key?(:scope)
74
- options[:resource_name] = controller_name if resource.respond_to?(:to_ary)
61
+ # Deprecated
62
+ def use_adapter?
63
+ true
64
+ end
75
65
 
76
- serializer.new(resource, options)
66
+ [:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
67
+ define_method renderer_method do |resource, options|
68
+ options.fetch(:serialization_context) do
69
+ options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options)
70
+ end
71
+ serializable_resource = get_serializer(resource, options)
72
+ super(serializable_resource, options)
77
73
  end
78
74
  end
79
75
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module ActiveModel
6
+ class SerializableResource
7
+ class << self
8
+ extend ActiveModelSerializers::Deprecate
9
+
10
+ delegate_and_deprecate :new, ActiveModelSerializers::SerializableResource
11
+ end
12
+ end
13
+ end
@@ -1,268 +1,425 @@
1
- require 'active_model/array_serializer'
2
- require 'active_model/serializable'
3
- require 'active_model/serializer/associations'
4
- require 'active_model/serializer/config'
5
-
6
- require 'thread'
7
-
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.
8
14
  module ActiveModel
9
15
  class Serializer
10
- include Serializable
11
-
12
- @mutex = Mutex.new
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]) }
51
+ end
52
+ end
13
53
 
54
+ # @see ActiveModelSerializers::Adapter.lookup
55
+ # Deprecated
56
+ def self.adapter
57
+ ActiveModelSerializers::Adapter.lookup(config.adapter)
58
+ end
14
59
  class << self
15
- def inherited(base)
16
- base._root = _root
17
- base._attributes = (_attributes || []).dup
18
- base._associations = (_associations || {}).dup
19
- end
60
+ extend ActiveModelSerializers::Deprecate
61
+ deprecate :adapter, 'ActiveModelSerializers::Adapter.configured_adapter'
62
+ end
20
63
 
21
- def setup
22
- @mutex.synchronize do
23
- yield CONFIG
24
- end
25
- end
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
26
71
 
27
- EMBED_IN_ROOT_OPTIONS = [
28
- :include,
29
- :embed_in_root,
30
- :embed_in_root_key,
31
- :embed_namespace
32
- ].freeze
33
-
34
- def embed(type, options={})
35
- CONFIG.embed = type
36
- if EMBED_IN_ROOT_OPTIONS.any? { |opt| options[opt].present? }
37
- CONFIG.embed_in_root = true
38
- end
39
- if options[:embed_in_root_key].present?
40
- CONFIG.embed_in_root_key = options[:embed_in_root_key]
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
77
+
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
41
99
  end
42
- ActiveSupport::Deprecation.warn <<-WARN
43
- ** Notice: embed is deprecated. **
44
- The use of .embed method on a Serializer will be soon removed, as this should have a global scope and not a class scope.
45
- Please use the global .setup method instead:
46
- ActiveModel::Serializer.setup do |config|
47
- config.embed = :#{type}
48
- config.embed_in_root = #{CONFIG.embed_in_root || false}
49
- end
50
- WARN
51
100
  end
101
+ end
52
102
 
53
- def format_keys(format)
54
- @key_format = format
55
- end
56
- attr_reader :key_format
57
-
58
- if RUBY_VERSION >= '2.0'
59
- def serializer_for(resource)
60
- if resource.respond_to?(:to_ary)
61
- if Object.constants.include?(:ArraySerializer)
62
- ::ArraySerializer
63
- else
64
- ArraySerializer
65
- end
66
- else
67
- begin
68
- Object.const_get "#{resource.class.name}Serializer"
69
- rescue NameError
70
- nil
71
- end
72
- end
73
- end
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)
74
109
  else
75
- def serializer_for(resource)
76
- if resource.respond_to?(:to_ary)
77
- if Object.constants.include?(:ArraySerializer)
78
- ::ArraySerializer
79
- else
80
- ArraySerializer
81
- end
82
- else
83
- "#{resource.class.name}Serializer".safe_constantize
84
- end
85
- end
110
+ ActiveModelSerializers.default_include_directive
86
111
  end
112
+ end
87
113
 
88
- attr_accessor :_root, :_attributes, :_associations
89
- alias root _root=
90
- alias root= _root=
114
+ # @api private
115
+ def self.serialization_adapter_instance
116
+ @serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
117
+ end
91
118
 
92
- def root_name
93
- name.demodulize.underscore.sub(/_serializer$/, '') if name
94
- end
119
+ # Preferred interface is ActiveModelSerializers.config
120
+ # BEGIN DEFAULT CONFIGURATION
121
+ config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
122
+ config.serializer_lookup_enabled = true
95
123
 
96
- def attributes(*attrs)
97
- @_attributes.concat attrs
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
98
129
 
99
- attrs.each do |attr|
100
- define_method attr do
101
- object.read_attribute_for_serialization attr
102
- end unless method_defined?(attr)
103
- end
104
- 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
105
135
 
106
- def has_one(*attrs)
107
- associate(Association::HasOne, *attrs)
108
- 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
169
+
170
+ config.schema_path = 'test/support/schemas'
171
+ # END DEFAULT CONFIGURATION
172
+
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
184
+ end
109
185
 
110
- def has_many(*attrs)
111
- associate(Association::HasMany, *attrs)
112
- end
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
113
192
 
114
- private
193
+ # @return [Array<Symbol>] Key names of declared attributes
194
+ # @see Serializer::attribute
195
+ def self._attributes
196
+ _attributes_data.keys
197
+ end
115
198
 
116
- def associate(klass, *attrs)
117
- options = attrs.extract_options!
199
+ # BEGIN SERIALIZER MACROS
118
200
 
119
- attrs.each do |attr|
120
- define_method attr do
121
- object.send attr
122
- end unless method_defined?(attr)
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
123
206
 
124
- @_associations[attr] = klass.new(attr, options)
125
- end
207
+ attrs.each do |attr|
208
+ attribute(attr)
126
209
  end
127
210
  end
128
211
 
129
- def initialize(object, options={})
130
- @object = object
131
- @scope = options[:scope]
132
- @root = options.fetch(:root, self.class._root)
133
- @meta_key = options[:meta_key] || :meta
134
- @meta = options[@meta_key]
135
- @wrap_in_array = options[:_wrap_in_array]
136
- @only = options[:only] ? Array(options[:only]) : nil
137
- @except = options[:except] ? Array(options[:except]) : nil
138
- @key_format = options[:key_format]
139
- @context = options[:context]
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)
140
227
  end
141
- attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context
142
228
 
143
- def json_key
144
- key = if root == true || root.nil?
145
- self.class.root_name
146
- else
147
- root
148
- end
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))
238
+ end
149
239
 
150
- key_format == :lower_camel && key.present? ? key.camelize(:lower) : key
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))
151
249
  end
152
250
 
153
- def attributes
154
- filter(self.class._attributes.dup).each_with_object({}) do |name, hash|
155
- hash[name] = send(name)
156
- 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))
157
260
  end
158
261
 
159
- def associations
160
- associations = self.class._associations
161
- included_associations = filter(associations.keys)
162
- associations.each_with_object({}) do |(name, association), hash|
163
- if included_associations.include? name
164
- if association.embed_ids?
165
- ids = serialize_ids association
166
- if association.embed_namespace?
167
- hash = hash[association.embed_namespace] ||= {}
168
- hash[association.key] = ids
169
- else
170
- hash[association.key] = ids
171
- end
172
- elsif association.embed_objects?
173
- if association.embed_namespace?
174
- hash = hash[association.embed_namespace] ||= {}
175
- end
176
- hash[association.embedded_key] = serialize association
177
- end
178
- end
179
- 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
270
+ 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 })
180
288
  end
181
289
 
182
- def filter(keys)
183
- if @only
184
- keys & @only
185
- elsif @except
186
- keys - @except
187
- else
188
- keys
189
- 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
190
300
  end
191
301
 
192
- def embedded_in_root_associations
193
- associations = self.class._associations
194
- included_associations = filter(associations.keys)
195
- associations.each_with_object({}) do |(name, association), hash|
196
- if included_associations.include? name
197
- if association.embed_in_root?
198
- if association.embed_in_root_key?
199
- hash = hash[association.embed_in_root_key] ||= {}
200
- end
201
- association_serializer = build_serializer(association)
202
- hash.merge!(association_serializer.embedded_in_root_associations) {|key, oldval, newval| [newval, oldval].flatten }
203
-
204
- serialized_data = association_serializer.serializable_object
205
- key = association.root_key
206
- if hash.has_key?(key)
207
- hash[key].concat(serialized_data).uniq!
208
- else
209
- hash[key] = serialized_data
210
- end
211
- end
212
- end
213
- 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
214
308
  end
215
309
 
216
- def build_serializer(association)
217
- object = send(association.name)
218
- association.build_serializer(object, scope: scope)
310
+ # END SERIALIZER MACROS
311
+
312
+ attr_accessor :object, :root, :scope
313
+
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]
322
+
323
+ return if !(scope_name = instance_options[:scope_name]) || respond_to?(scope_name)
324
+
325
+ define_singleton_method scope_name, -> { scope }
219
326
  end
220
327
 
221
- def serialize(association)
222
- build_serializer(association).serializable_object
328
+ def success?
329
+ true
223
330
  end
224
331
 
225
- def serialize_ids(association)
226
- associated_data = send(association.name)
227
- if associated_data.respond_to?(:to_ary)
228
- associated_data.map { |elem| elem.read_attribute_for_serialization(association.embed_key) }
229
- else
230
- associated_data.read_attribute_for_serialization(association.embed_key) if associated_data
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)
231
340
  end
232
341
  end
233
342
 
234
- def key_format
235
- @key_format || self.class.key_format || CONFIG.key_format
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
349
+
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)
354
+
355
+ association = reflection.build_association(self, instance_options, include_slice)
356
+ y.yield association
357
+ end
358
+ end
236
359
  end
237
360
 
238
- def format_key(key)
239
- if key_format == :lower_camel
240
- key.to_s.camelize(:lower)
241
- else
242
- key
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)
243
369
  end
370
+ resource = attributes_hash(adapter_options, options, adapter_instance)
371
+ relationships = associations_hash(adapter_options, options, adapter_instance)
372
+ resource.merge(relationships)
244
373
  end
374
+ alias to_hash serializable_hash
375
+ alias to_h serializable_hash
245
376
 
246
- def convert_keys(hash)
247
- Hash[hash.map do |k,v|
248
- key = if k.is_a?(Symbol)
249
- format_key(k).to_sym
250
- else
251
- format_key(k)
377
+ # @see #serializable_hash
378
+ def as_json(adapter_opts = nil)
379
+ serializable_hash(adapter_opts)
380
+ end
381
+
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'
252
389
  end
390
+ end
391
+
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
398
+ end
253
399
 
254
- [key ,v]
255
- end]
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
256
409
  end
257
410
 
258
- def serializable_object(options={})
259
- return @wrap_in_array ? [] : nil if @object.nil?
260
- hash = attributes
261
- hash.merge! associations
262
- hash = convert_keys(hash) if key_format.present?
263
- @wrap_in_array ? [hash] : hash
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
264
419
  end
265
- alias_method :serializable_hash, :serializable_object
266
- end
267
420
 
421
+ protected
422
+
423
+ attr_accessor :instance_options, :instance_reflections
424
+ end
268
425
  end