active_model_serializers 0.8.3 → 0.10.8

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