active_model_serializers 0.10.0.rc4 → 0.10.0.rc5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +29 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +19 -1
  6. data/.rubocop_todo.yml +30 -103
  7. data/.simplecov +0 -1
  8. data/.travis.yml +20 -8
  9. data/CHANGELOG.md +89 -5
  10. data/CONTRIBUTING.md +54 -179
  11. data/Gemfile +7 -2
  12. data/{LICENSE.txt → MIT-LICENSE} +0 -0
  13. data/README.md +27 -5
  14. data/Rakefile +44 -16
  15. data/active_model_serializers.gemspec +9 -1
  16. data/appveyor.yml +1 -0
  17. data/bin/bench +171 -0
  18. data/bin/bench_regression +316 -0
  19. data/bin/serve_benchmark +39 -0
  20. data/docs/ARCHITECTURE.md +13 -7
  21. data/docs/README.md +5 -1
  22. data/docs/STYLE.md +58 -0
  23. data/docs/general/adapters.md +99 -16
  24. data/docs/general/configuration_options.md +87 -14
  25. data/docs/general/deserialization.md +100 -0
  26. data/docs/general/getting_started.md +35 -0
  27. data/docs/general/instrumentation.md +1 -1
  28. data/docs/general/key_transforms.md +40 -0
  29. data/docs/general/rendering.md +115 -13
  30. data/docs/general/serializers.md +138 -6
  31. data/docs/howto/add_pagination_links.md +36 -18
  32. data/docs/howto/outside_controller_use.md +4 -4
  33. data/docs/howto/passing_arbitrary_options.md +27 -0
  34. data/docs/jsonapi/errors.md +56 -0
  35. data/docs/jsonapi/schema.md +29 -18
  36. data/docs/rfcs/0000-namespace.md +106 -0
  37. data/docs/rfcs/template.md +15 -0
  38. data/lib/action_controller/serialization.rb +10 -19
  39. data/lib/active_model/serializable_resource.rb +4 -65
  40. data/lib/active_model/serializer.rb +73 -18
  41. data/lib/active_model/serializer/adapter.rb +15 -82
  42. data/lib/active_model/serializer/adapter/attributes.rb +5 -56
  43. data/lib/active_model/serializer/adapter/base.rb +5 -47
  44. data/lib/active_model/serializer/adapter/json.rb +6 -12
  45. data/lib/active_model/serializer/adapter/json_api.rb +5 -213
  46. data/lib/active_model/serializer/adapter/null.rb +7 -3
  47. data/lib/active_model/serializer/array_serializer.rb +3 -3
  48. data/lib/active_model/serializer/association.rb +4 -5
  49. data/lib/active_model/serializer/attributes.rb +1 -1
  50. data/lib/active_model/serializer/caching.rb +56 -5
  51. data/lib/active_model/serializer/collection_serializer.rb +30 -13
  52. data/lib/active_model/serializer/configuration.rb +7 -0
  53. data/lib/active_model/serializer/error_serializer.rb +10 -0
  54. data/lib/active_model/serializer/errors_serializer.rb +27 -0
  55. data/lib/active_model/serializer/links.rb +4 -2
  56. data/lib/active_model/serializer/lint.rb +14 -0
  57. data/lib/active_model/serializer/meta.rb +29 -0
  58. data/lib/active_model/serializer/null.rb +17 -0
  59. data/lib/active_model/serializer/reflection.rb +57 -1
  60. data/lib/active_model/serializer/type.rb +1 -1
  61. data/lib/active_model/serializer/version.rb +1 -1
  62. data/lib/active_model_serializers.rb +17 -0
  63. data/lib/active_model_serializers/adapter.rb +92 -0
  64. data/lib/active_model_serializers/adapter/attributes.rb +94 -0
  65. data/lib/active_model_serializers/adapter/base.rb +90 -0
  66. data/lib/active_model_serializers/adapter/json.rb +11 -0
  67. data/lib/active_model_serializers/adapter/json_api.rb +513 -0
  68. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +213 -0
  69. data/lib/active_model_serializers/adapter/json_api/error.rb +96 -0
  70. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +49 -0
  71. data/lib/active_model_serializers/adapter/json_api/link.rb +83 -0
  72. data/lib/active_model_serializers/adapter/json_api/meta.rb +37 -0
  73. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +57 -0
  74. data/lib/active_model_serializers/adapter/json_api/relationship.rb +52 -0
  75. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +37 -0
  76. data/lib/active_model_serializers/adapter/null.rb +10 -0
  77. data/lib/active_model_serializers/cached_serializer.rb +87 -0
  78. data/lib/active_model_serializers/callbacks.rb +1 -1
  79. data/lib/active_model_serializers/deprecate.rb +55 -0
  80. data/lib/active_model_serializers/deserialization.rb +2 -2
  81. data/lib/active_model_serializers/fragment_cache.rb +118 -0
  82. data/lib/active_model_serializers/json_pointer.rb +14 -0
  83. data/lib/active_model_serializers/key_transform.rb +70 -0
  84. data/lib/active_model_serializers/logging.rb +4 -1
  85. data/lib/active_model_serializers/model.rb +11 -1
  86. data/lib/active_model_serializers/railtie.rb +9 -1
  87. data/lib/active_model_serializers/register_jsonapi_renderer.rb +64 -0
  88. data/lib/active_model_serializers/serializable_resource.rb +81 -0
  89. data/lib/active_model_serializers/serialization_context.rb +24 -2
  90. data/lib/active_model_serializers/test/schema.rb +2 -2
  91. data/lib/grape/formatters/active_model_serializers.rb +1 -1
  92. data/test/action_controller/adapter_selector_test.rb +1 -1
  93. data/test/action_controller/json_api/deserialization_test.rb +56 -3
  94. data/test/action_controller/json_api/errors_test.rb +41 -0
  95. data/test/action_controller/json_api/linked_test.rb +10 -9
  96. data/test/action_controller/json_api/pagination_test.rb +2 -2
  97. data/test/action_controller/json_api/transform_test.rb +180 -0
  98. data/test/action_controller/serialization_scope_name_test.rb +201 -35
  99. data/test/action_controller/serialization_test.rb +39 -7
  100. data/test/active_model_serializers/adapter_for_test.rb +208 -0
  101. data/test/active_model_serializers/cached_serializer_test.rb +80 -0
  102. data/test/active_model_serializers/fragment_cache_test.rb +34 -0
  103. data/test/active_model_serializers/json_pointer_test.rb +20 -0
  104. data/test/active_model_serializers/key_transform_test.rb +263 -0
  105. data/test/active_model_serializers/logging_test.rb +8 -8
  106. data/test/active_model_serializers/railtie_test_isolated.rb +6 -0
  107. data/test/active_model_serializers/serialization_context_test_isolated.rb +58 -0
  108. data/test/adapter/deprecation_test.rb +100 -0
  109. data/test/adapter/json/belongs_to_test.rb +32 -34
  110. data/test/adapter/json/collection_test.rb +73 -75
  111. data/test/adapter/json/has_many_test.rb +36 -38
  112. data/test/adapter/json/transform_test.rb +93 -0
  113. data/test/adapter/json_api/belongs_to_test.rb +127 -129
  114. data/test/adapter/json_api/collection_test.rb +80 -82
  115. data/test/adapter/json_api/errors_test.rb +78 -0
  116. data/test/adapter/json_api/fields_test.rb +68 -70
  117. data/test/adapter/json_api/has_many_embed_ids_test.rb +32 -34
  118. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +75 -77
  119. data/test/adapter/json_api/has_many_test.rb +121 -123
  120. data/test/adapter/json_api/has_one_test.rb +59 -61
  121. data/test/adapter/json_api/json_api_test.rb +28 -30
  122. data/test/adapter/json_api/linked_test.rb +319 -321
  123. data/test/adapter/json_api/links_test.rb +75 -50
  124. data/test/adapter/json_api/pagination_links_test.rb +115 -82
  125. data/test/adapter/json_api/parse_test.rb +114 -116
  126. data/test/adapter/json_api/relationship_test.rb +161 -0
  127. data/test/adapter/json_api/relationships_test.rb +199 -0
  128. data/test/adapter/json_api/resource_identifier_test.rb +85 -0
  129. data/test/adapter/json_api/resource_meta_test.rb +100 -0
  130. data/test/adapter/json_api/toplevel_jsonapi_test.rb +61 -63
  131. data/test/adapter/json_api/transform_test.rb +500 -0
  132. data/test/adapter/json_api/type_test.rb +61 -0
  133. data/test/adapter/json_test.rb +35 -37
  134. data/test/adapter/null_test.rb +13 -15
  135. data/test/adapter/polymorphic_test.rb +72 -0
  136. data/test/adapter_test.rb +27 -29
  137. data/test/array_serializer_test.rb +7 -8
  138. data/test/benchmark/app.rb +65 -0
  139. data/test/benchmark/benchmarking_support.rb +67 -0
  140. data/test/benchmark/bm_caching.rb +117 -0
  141. data/test/benchmark/bm_transform.rb +34 -0
  142. data/test/benchmark/config.ru +3 -0
  143. data/test/benchmark/controllers.rb +77 -0
  144. data/test/benchmark/fixtures.rb +167 -0
  145. data/test/cache_test.rb +388 -0
  146. data/test/collection_serializer_test.rb +10 -0
  147. data/test/fixtures/active_record.rb +12 -0
  148. data/test/fixtures/poro.rb +28 -3
  149. data/test/grape_test.rb +5 -5
  150. data/test/lint_test.rb +9 -0
  151. data/test/serializable_resource_test.rb +59 -3
  152. data/test/serializers/associations_test.rb +8 -8
  153. data/test/serializers/attribute_test.rb +7 -7
  154. data/test/serializers/caching_configuration_test_isolated.rb +170 -0
  155. data/test/serializers/meta_test.rb +74 -6
  156. data/test/serializers/read_attribute_for_serialization_test.rb +79 -0
  157. data/test/serializers/serialization_test.rb +55 -0
  158. data/test/support/isolated_unit.rb +3 -0
  159. data/test/support/rails5_shims.rb +26 -8
  160. data/test/support/rails_app.rb +38 -18
  161. data/test/support/serialization_testing.rb +5 -5
  162. data/test/test_helper.rb +6 -10
  163. metadata +132 -37
  164. data/docs/DESIGN.textile +7 -1
  165. data/lib/active_model/serializer/adapter/cached_serializer.rb +0 -45
  166. data/lib/active_model/serializer/adapter/fragment_cache.rb +0 -111
  167. data/lib/active_model/serializer/adapter/json/fragment_cache.rb +0 -13
  168. data/lib/active_model/serializer/adapter/json_api/deserialization.rb +0 -207
  169. data/lib/active_model/serializer/adapter/json_api/fragment_cache.rb +0 -21
  170. data/lib/active_model/serializer/adapter/json_api/link.rb +0 -44
  171. data/lib/active_model/serializer/adapter/json_api/pagination_links.rb +0 -58
  172. data/test/active_model_serializers/serialization_context_test.rb +0 -18
  173. data/test/adapter/fragment_cache_test.rb +0 -38
  174. data/test/adapter/json_api/resource_type_config_test.rb +0 -71
  175. data/test/serializers/adapter_for_test.rb +0 -166
  176. data/test/serializers/cache_test.rb +0 -209
  177. data/test/support/simplecov.rb +0 -6
  178. data/test/support/stream_capture.rb +0 -50
  179. data/test/support/test_case.rb +0 -19
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
- VERSION = '0.10.0.rc4'
3
+ VERSION = '0.10.0.rc5'.freeze
4
4
  end
5
5
  end
@@ -1,13 +1,21 @@
1
1
  require 'active_model'
2
2
  require 'active_support'
3
3
  require 'active_support/core_ext/object/with_options'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/json'
4
6
  module ActiveModelSerializers
5
7
  extend ActiveSupport::Autoload
6
8
  autoload :Model
9
+ autoload :CachedSerializer
10
+ autoload :FragmentCache
7
11
  autoload :Callbacks
8
12
  autoload :Deserialization
13
+ autoload :SerializableResource
9
14
  autoload :Logging
10
15
  autoload :Test
16
+ autoload :Adapter
17
+ autoload :JsonPointer
18
+ autoload :Deprecate
11
19
 
12
20
  class << self; attr_accessor :logger; end
13
21
  self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
@@ -16,6 +24,15 @@ module ActiveModelSerializers
16
24
  ActiveModel::Serializer.config
17
25
  end
18
26
 
27
+ # The file name and line number of the caller of the caller of this method.
28
+ def self.location_of_caller
29
+ caller[1] =~ /(.*?):(\d+).*?$/i
30
+ file = Regexp.last_match(1)
31
+ lineno = Regexp.last_match(2).to_i
32
+
33
+ [file, lineno]
34
+ end
35
+
19
36
  require 'active_model/serializer/version'
20
37
  require 'active_model/serializer'
21
38
  require 'active_model/serializable_resource'
@@ -0,0 +1,92 @@
1
+ module ActiveModelSerializers
2
+ module Adapter
3
+ UnknownAdapterError = Class.new(ArgumentError)
4
+ ADAPTER_MAP = {} # rubocop:disable Style/MutableConstant
5
+ private_constant :ADAPTER_MAP if defined?(private_constant)
6
+
7
+ class << self # All methods are class functions
8
+ def new(*args)
9
+ fail ArgumentError, 'Adapters inherit from Adapter::Base.' \
10
+ "Adapter.new called with args: '#{args.inspect}', from" \
11
+ "'caller[0]'."
12
+ end
13
+
14
+ def configured_adapter
15
+ lookup(ActiveModelSerializers.config.adapter)
16
+ end
17
+
18
+ def create(resource, options = {})
19
+ override = options.delete(:adapter)
20
+ klass = override ? adapter_class(override) : configured_adapter
21
+ klass.new(resource, options)
22
+ end
23
+
24
+ # @see ActiveModelSerializers::Adapter.lookup
25
+ def adapter_class(adapter)
26
+ ActiveModelSerializers::Adapter.lookup(adapter)
27
+ end
28
+
29
+ # @return [Hash<adapter_name, adapter_class>]
30
+ def adapter_map
31
+ ADAPTER_MAP
32
+ end
33
+
34
+ # @return [Array<Symbol>] list of adapter names
35
+ def adapters
36
+ adapter_map.keys.sort
37
+ end
38
+
39
+ # Adds an adapter 'klass' with 'name' to the 'adapter_map'
40
+ # Names are stringified and underscored
41
+ # @param name [Symbol, String, Class] name of the registered adapter
42
+ # @param klass [Class] adapter class itself, optional if name is the class
43
+ # @example
44
+ # AMS::Adapter.register(:my_adapter, MyAdapter)
45
+ # @note The registered name strips out 'ActiveModelSerializers::Adapter::'
46
+ # so that registering 'ActiveModelSerializers::Adapter::Json' and
47
+ # 'Json' will both register as 'json'.
48
+ def register(name, klass = name)
49
+ name = name.to_s.gsub(/\AActiveModelSerializers::Adapter::/, ''.freeze)
50
+ adapter_map[name.underscore] = klass
51
+ self
52
+ end
53
+
54
+ # @param adapter [String, Symbol, Class] name to fetch adapter by
55
+ # @return [ActiveModelSerializers::Adapter] subclass of Adapter
56
+ # @raise [UnknownAdapterError]
57
+ def lookup(adapter)
58
+ # 1. return if is a class
59
+ return adapter if adapter.is_a?(Class)
60
+ adapter_name = adapter.to_s.underscore
61
+ # 2. return if registered
62
+ adapter_map.fetch(adapter_name) do
63
+ # 3. try to find adapter class from environment
64
+ adapter_class = find_by_name(adapter_name)
65
+ register(adapter_name, adapter_class)
66
+ adapter_class
67
+ end
68
+ rescue NameError, ArgumentError => e
69
+ failure_message =
70
+ "NameError: #{e.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
71
+ raise UnknownAdapterError, failure_message, e.backtrace
72
+ end
73
+
74
+ # @api private
75
+ def find_by_name(adapter_name)
76
+ adapter_name = adapter_name.to_s.classify.tr('API', 'Api')
77
+ "ActiveModelSerializers::Adapter::#{adapter_name}".safe_constantize ||
78
+ "ActiveModelSerializers::Adapter::#{adapter_name.pluralize}".safe_constantize or # rubocop:disable Style/AndOr
79
+ fail UnknownAdapterError
80
+ end
81
+ private :find_by_name
82
+ end
83
+
84
+ # Gotta be at the bottom to use the code above it :(
85
+ extend ActiveSupport::Autoload
86
+ autoload :Base
87
+ autoload :Null
88
+ autoload :Attributes
89
+ autoload :Json
90
+ autoload :JsonApi
91
+ end
92
+ end
@@ -0,0 +1,94 @@
1
+ module ActiveModelSerializers
2
+ module Adapter
3
+ class Attributes < Base
4
+ def initialize(serializer, options = {})
5
+ super
6
+ @include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include] || '*')
7
+ @cached_attributes = options[:cache_attributes] || {}
8
+ end
9
+
10
+ def serializable_hash(options = nil)
11
+ options ||= {}
12
+
13
+ if serializer.respond_to?(:each)
14
+ serializable_hash_for_collection(options)
15
+ else
16
+ serializable_hash_for_single_resource(options)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def serializable_hash_for_collection(options)
23
+ cache_attributes
24
+
25
+ serializer.map { |s| Attributes.new(s, instance_options).serializable_hash(options) }
26
+ end
27
+
28
+ # Read cache from cache_store
29
+ # @return [Hash]
30
+ def cache_read_multi
31
+ return {} if ActiveModelSerializers.config.cache_store.blank?
32
+
33
+ keys = CachedSerializer.object_cache_keys(serializer, self, @include_tree)
34
+
35
+ return {} if keys.blank?
36
+
37
+ ActiveModelSerializers.config.cache_store.read_multi(*keys)
38
+ end
39
+
40
+ # Set @cached_attributes
41
+ def cache_attributes
42
+ return if @cached_attributes.present?
43
+
44
+ @cached_attributes = cache_read_multi
45
+ end
46
+
47
+ # Get attributes from @cached_attributes
48
+ # @return [Hash] cached attributes
49
+ def cached_attributes(cached_serializer)
50
+ return yield unless cached_serializer.cached?
51
+
52
+ @cached_attributes.fetch(cached_serializer.cache_key(self)) { yield }
53
+ end
54
+
55
+ def serializable_hash_for_single_resource(options)
56
+ resource = resource_object_for(options)
57
+ relationships = resource_relationships(options)
58
+ resource.merge(relationships)
59
+ end
60
+
61
+ def resource_relationships(options)
62
+ relationships = {}
63
+ serializer.associations(@include_tree).each do |association|
64
+ relationships[association.key] = relationship_value_for(association, options)
65
+ end
66
+
67
+ relationships
68
+ end
69
+
70
+ def relationship_value_for(association, options)
71
+ return association.options[:virtual_value] if association.options[:virtual_value]
72
+ return unless association.serializer && association.serializer.object
73
+
74
+ opts = instance_options.merge(include: @include_tree[association.key])
75
+ Attributes.new(association.serializer, opts).serializable_hash(options)
76
+ end
77
+
78
+ # no-op: Attributes adapter does not include meta data, because it does not support root.
79
+ def include_meta(json)
80
+ json
81
+ end
82
+
83
+ def resource_object_for(options)
84
+ cached_serializer = CachedSerializer.new(serializer)
85
+
86
+ cached_attributes(cached_serializer) do
87
+ cached_serializer.cache_check(self) do
88
+ serializer.attributes(options[:fields])
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,90 @@
1
+ require 'active_model_serializers/key_transform'
2
+
3
+ module ActiveModelSerializers
4
+ module Adapter
5
+ class Base
6
+ # Automatically register adapters when subclassing
7
+ def self.inherited(subclass)
8
+ ActiveModelSerializers::Adapter.register(subclass)
9
+ end
10
+
11
+ attr_reader :serializer, :instance_options
12
+
13
+ def initialize(serializer, options = {})
14
+ @serializer = serializer
15
+ @instance_options = options
16
+ end
17
+
18
+ def cached_name
19
+ @cached_name ||= self.class.name.demodulize.underscore
20
+ end
21
+
22
+ def serializable_hash(_options = nil)
23
+ fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
24
+ end
25
+
26
+ def as_json(options = nil)
27
+ hash = serializable_hash(options)
28
+ include_meta(hash)
29
+ hash
30
+ end
31
+
32
+ def fragment_cache(cached_hash, non_cached_hash)
33
+ non_cached_hash.merge cached_hash
34
+ end
35
+
36
+ def cache_check(serializer)
37
+ CachedSerializer.new(serializer).cache_check(self) do
38
+ yield
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def meta
45
+ instance_options.fetch(:meta, nil)
46
+ end
47
+
48
+ def meta_key
49
+ instance_options.fetch(:meta_key, 'meta'.freeze)
50
+ end
51
+
52
+ def root
53
+ serializer.json_key.to_sym if serializer.json_key
54
+ end
55
+
56
+ def include_meta(json)
57
+ json[meta_key] = meta unless meta.blank?
58
+ json
59
+ end
60
+
61
+ class << self
62
+ # Sets the default transform for the adapter.
63
+ #
64
+ # @return [Symbol] the default transform for the adapter
65
+ def default_key_transform
66
+ :unaltered
67
+ end
68
+
69
+ # Determines the transform to use in order of precedence:
70
+ # adapter option, global config, adapter default.
71
+ #
72
+ # @param options [Object]
73
+ # @return [Symbol] the transform to use
74
+ def transform(options)
75
+ return options[:key_transform] if options && options[:key_transform]
76
+ ActiveModelSerializers.config.key_transform || default_key_transform
77
+ end
78
+
79
+ # Transforms the casing of the supplied value.
80
+ #
81
+ # @param value [Object] the value to be transformed
82
+ # @param options [Object] serializable resource options
83
+ # @return [Symbol] the default transform for the adapter
84
+ def transform_key_casing!(value, options)
85
+ KeyTransform.send(transform(options), value)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveModelSerializers
2
+ module Adapter
3
+ class Json < Base
4
+ def serializable_hash(options = nil)
5
+ options ||= {}
6
+ serialized_hash = { root => Attributes.new(serializer, instance_options).serializable_hash(options) }
7
+ self.class.transform_key_casing!(serialized_hash, options)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,513 @@
1
+ # {http://jsonapi.org/format/ JSON API specification}
2
+ # rubocop:disable Style/AsciiComments
3
+ # TODO: implement!
4
+ # ☐ https://github.com/rails-api/active_model_serializers/issues/1235
5
+ # TODO: use uri_template in link generation?
6
+ # ☐ https://github.com/rails-api/active_model_serializers/pull/1282#discussion_r42528812
7
+ # see gem https://github.com/hannesg/uri_template
8
+ # spec http://tools.ietf.org/html/rfc6570
9
+ # impl https://developer.github.com/v3/#schema https://api.github.com/
10
+ # TODO: validate against a JSON schema document?
11
+ # ☐ https://github.com/rails-api/active_model_serializers/issues/1162
12
+ # ☑ https://github.com/rails-api/active_model_serializers/pull/1270
13
+ # TODO: Routing
14
+ # ☐ https://github.com/rails-api/active_model_serializers/pull/1476
15
+ # TODO: Query Params
16
+ # ☑ `include` https://github.com/rails-api/active_model_serializers/pull/1131
17
+ # ☑ `fields` https://github.com/rails-api/active_model_serializers/pull/700
18
+ # ☑ `page[number]=3&page[size]=1` https://github.com/rails-api/active_model_serializers/pull/1041
19
+ # ☐ `filter`
20
+ # ☐ `sort`
21
+ module ActiveModelSerializers
22
+ module Adapter
23
+ class JsonApi < Base
24
+ extend ActiveSupport::Autoload
25
+ autoload :Jsonapi
26
+ autoload :ResourceIdentifier
27
+ autoload :Relationship
28
+ autoload :Link
29
+ autoload :PaginationLinks
30
+ autoload :Meta
31
+ autoload :Error
32
+ autoload :Deserialization
33
+
34
+ def initialize(serializer, options = {})
35
+ super
36
+ @include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include])
37
+ @fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
38
+ end
39
+
40
+ def self.default_key_transform
41
+ :dash
42
+ end
43
+
44
+ # {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
45
+ # {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
46
+ def serializable_hash(options = nil)
47
+ options ||= {}
48
+ document = if serializer.success?
49
+ success_document(options)
50
+ else
51
+ failure_document(options)
52
+ end
53
+ self.class.transform_key_casing!(document, options)
54
+ end
55
+
56
+ # {http://jsonapi.org/format/#document-top-level Primary data}
57
+ # definition:
58
+ # ☐ toplevel_data (required)
59
+ # ☐ toplevel_included
60
+ # ☑ toplevel_meta
61
+ # ☑ toplevel_links
62
+ # ☑ toplevel_jsonapi
63
+ # structure:
64
+ # {
65
+ # data: toplevel_data,
66
+ # included: toplevel_included,
67
+ # meta: toplevel_meta,
68
+ # links: toplevel_links,
69
+ # jsonapi: toplevel_jsonapi
70
+ # }.reject! {|_,v| v.nil? }
71
+ def success_document(options)
72
+ is_collection = serializer.respond_to?(:each)
73
+ serializers = is_collection ? serializer : [serializer]
74
+ primary_data, included = resource_objects_for(serializers, options)
75
+
76
+ hash = {}
77
+ # toplevel_data
78
+ # definition:
79
+ # oneOf
80
+ # resource
81
+ # array of unique items of type 'resource'
82
+ # null
83
+ #
84
+ # description:
85
+ # The document's "primary data" is a representation of the resource or collection of resources
86
+ # targeted by a request.
87
+ #
88
+ # Singular: the resource object.
89
+ #
90
+ # Collection: one of an array of resource objects, an array of resource identifier objects, or
91
+ # an empty array ([]), for requests that target resource collections.
92
+ #
93
+ # None: null if the request is one that might correspond to a single resource, but doesn't currently.
94
+ # structure:
95
+ # if serializable_resource.resource?
96
+ # resource
97
+ # elsif serializable_resource.collection?
98
+ # [
99
+ # resource,
100
+ # resource
101
+ # ]
102
+ # else
103
+ # nil
104
+ # end
105
+ hash[:data] = is_collection ? primary_data : primary_data[0]
106
+ # toplevel_included
107
+ # alias included
108
+ # definition:
109
+ # array of unique items of type 'resource'
110
+ #
111
+ # description:
112
+ # To reduce the number of HTTP requests, servers **MAY** allow
113
+ # responses that include related resources along with the requested primary
114
+ # resources. Such responses are called "compound documents".
115
+ # structure:
116
+ # [
117
+ # resource,
118
+ # resource
119
+ # ]
120
+ hash[:included] = included if included.any?
121
+
122
+ Jsonapi.add!(hash)
123
+
124
+ if instance_options[:links]
125
+ hash[:links] ||= {}
126
+ hash[:links].update(instance_options[:links])
127
+ end
128
+
129
+ if is_collection && serializer.paginated?
130
+ hash[:links] ||= {}
131
+ hash[:links].update(pagination_links_for(serializer, options))
132
+ end
133
+
134
+ hash
135
+ end
136
+
137
+ # {http://jsonapi.org/format/#errors JSON API Errors}
138
+ # TODO: look into caching
139
+ # definition:
140
+ # ☑ toplevel_errors array (required)
141
+ # ☐ toplevel_meta
142
+ # ☐ toplevel_jsonapi
143
+ # structure:
144
+ # {
145
+ # errors: toplevel_errors,
146
+ # meta: toplevel_meta,
147
+ # jsonapi: toplevel_jsonapi
148
+ # }.reject! {|_,v| v.nil? }
149
+ # prs:
150
+ # https://github.com/rails-api/active_model_serializers/pull/1004
151
+ def failure_document(options)
152
+ hash = {}
153
+ # PR Please :)
154
+ # Jsonapi.add!(hash)
155
+
156
+ # toplevel_errors
157
+ # definition:
158
+ # array of unique items of type 'error'
159
+ # structure:
160
+ # [
161
+ # error,
162
+ # error
163
+ # ]
164
+ if serializer.respond_to?(:each)
165
+ hash[:errors] = serializer.flat_map do |error_serializer|
166
+ Error.resource_errors(error_serializer, options)
167
+ end
168
+ else
169
+ hash[:errors] = Error.resource_errors(serializer, options)
170
+ end
171
+ hash
172
+ end
173
+
174
+ def fragment_cache(cached_hash, non_cached_hash)
175
+ root = false if instance_options.include?(:include)
176
+ core_cached = cached_hash.first
177
+ core_non_cached = non_cached_hash.first
178
+ no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
179
+ no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
180
+ cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
181
+ hash = root ? { root => cached_resource } : cached_resource
182
+
183
+ hash.deep_merge no_root_non_cache.deep_merge no_root_cache
184
+ end
185
+
186
+ protected
187
+
188
+ attr_reader :fieldset
189
+
190
+ private
191
+
192
+ # {http://jsonapi.org/format/#document-resource-objects Primary data}
193
+ # resource
194
+ # definition:
195
+ # JSON Object
196
+ #
197
+ # properties:
198
+ # type (required) : String
199
+ # id (required) : String
200
+ # attributes
201
+ # relationships
202
+ # links
203
+ # meta
204
+ #
205
+ # description:
206
+ # "Resource objects" appear in a JSON API document to represent resources
207
+ # structure:
208
+ # {
209
+ # type: 'admin--some-user',
210
+ # id: '1336',
211
+ # attributes: attributes,
212
+ # relationships: relationships,
213
+ # links: links,
214
+ # meta: meta,
215
+ # }.reject! {|_,v| v.nil? }
216
+ # prs:
217
+ # type
218
+ # https://github.com/rails-api/active_model_serializers/pull/1122
219
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1213
220
+ # https://github.com/rails-api/active_model_serializers/pull/1216
221
+ # https://github.com/rails-api/active_model_serializers/pull/1029
222
+ # links
223
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1246
224
+ # [x] url helpers https://github.com/rails-api/active_model_serializers/issues/1269
225
+ # meta
226
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1340
227
+ def resource_objects_for(serializers, options)
228
+ @primary = []
229
+ @included = []
230
+ @resource_identifiers = Set.new
231
+ serializers.each { |serializer| process_resource(serializer, true, options) }
232
+ serializers.each { |serializer| process_relationships(serializer, @include_tree, options) }
233
+
234
+ [@primary, @included]
235
+ end
236
+
237
+ def process_resource(serializer, primary, options)
238
+ resource_identifier = ResourceIdentifier.new(serializer, options).as_json
239
+ return false unless @resource_identifiers.add?(resource_identifier)
240
+
241
+ resource_object = resource_object_for(serializer, options)
242
+ if primary
243
+ @primary << resource_object
244
+ else
245
+ @included << resource_object
246
+ end
247
+
248
+ true
249
+ end
250
+
251
+ def process_relationships(serializer, include_tree, options)
252
+ serializer.associations(include_tree).each do |association|
253
+ process_relationship(association.serializer, include_tree[association.key], options)
254
+ end
255
+ end
256
+
257
+ def process_relationship(serializer, include_tree, options)
258
+ if serializer.respond_to?(:each)
259
+ serializer.each { |s| process_relationship(s, include_tree, options) }
260
+ return
261
+ end
262
+ return unless serializer && serializer.object
263
+ return unless process_resource(serializer, false, options)
264
+
265
+ process_relationships(serializer, include_tree, options)
266
+ end
267
+
268
+ # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
269
+ # attributes
270
+ # definition:
271
+ # JSON Object
272
+ #
273
+ # patternProperties:
274
+ # ^(?!relationships$|links$)\\w[-\\w_]*$
275
+ #
276
+ # description:
277
+ # Members of the attributes object ("attributes") represent information about the resource
278
+ # object in which it's defined.
279
+ # Attributes may contain any valid JSON value
280
+ # structure:
281
+ # {
282
+ # foo: 'bar'
283
+ # }
284
+ def attributes_for(serializer, fields)
285
+ serializer.attributes(fields).except(:id)
286
+ end
287
+
288
+ # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
289
+ def resource_object_for(serializer, options)
290
+ resource_object = cache_check(serializer) do
291
+ resource_object = ResourceIdentifier.new(serializer, options).as_json
292
+
293
+ requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
294
+ attributes = attributes_for(serializer, requested_fields)
295
+ resource_object[:attributes] = attributes if attributes.any?
296
+ resource_object
297
+ end
298
+
299
+ requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
300
+ relationships = relationships_for(serializer, requested_associations, options)
301
+ resource_object[:relationships] = relationships if relationships.any?
302
+
303
+ links = links_for(serializer)
304
+ # toplevel_links
305
+ # definition:
306
+ # allOf
307
+ # ☐ links
308
+ # ☐ pagination
309
+ #
310
+ # description:
311
+ # Link members related to the primary data.
312
+ # structure:
313
+ # links.merge!(pagination)
314
+ # prs:
315
+ # https://github.com/rails-api/active_model_serializers/pull/1247
316
+ # https://github.com/rails-api/active_model_serializers/pull/1018
317
+ resource_object[:links] = links if links.any?
318
+
319
+ # toplevel_meta
320
+ # alias meta
321
+ # definition:
322
+ # meta
323
+ # structure
324
+ # {
325
+ # :'git-ref' => 'abc123'
326
+ # }
327
+ meta = meta_for(serializer)
328
+ resource_object[:meta] = meta unless meta.blank?
329
+
330
+ resource_object
331
+ end
332
+
333
+ # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
334
+ # relationships
335
+ # definition:
336
+ # JSON Object
337
+ #
338
+ # patternProperties:
339
+ # ^\\w[-\\w_]*$"
340
+ #
341
+ # properties:
342
+ # data : relationshipsData
343
+ # links
344
+ # meta
345
+ #
346
+ # description:
347
+ #
348
+ # Members of the relationships object ("relationships") represent references from the
349
+ # resource object in which it's defined to other resource objects."
350
+ # structure:
351
+ # {
352
+ # links: links,
353
+ # meta: meta,
354
+ # data: relationshipsData
355
+ # }.reject! {|_,v| v.nil? }
356
+ #
357
+ # prs:
358
+ # links
359
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1454
360
+ # meta
361
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1454
362
+ # polymorphic
363
+ # [ ] https://github.com/rails-api/active_model_serializers/pull/1420
364
+ #
365
+ # relationshipsData
366
+ # definition:
367
+ # oneOf
368
+ # relationshipToOne
369
+ # relationshipToMany
370
+ #
371
+ # description:
372
+ # Member, whose value represents "resource linkage"
373
+ # structure:
374
+ # if has_one?
375
+ # relationshipToOne
376
+ # else
377
+ # relationshipToMany
378
+ # end
379
+ #
380
+ # definition:
381
+ # anyOf
382
+ # null
383
+ # linkage
384
+ #
385
+ # relationshipToOne
386
+ # description:
387
+ #
388
+ # References to other resource objects in a to-one ("relationship"). Relationships can be
389
+ # specified by including a member in a resource's links object.
390
+ #
391
+ # None: Describes an empty to-one relationship.
392
+ # structure:
393
+ # if has_related?
394
+ # linkage
395
+ # else
396
+ # nil
397
+ # end
398
+ #
399
+ # relationshipToMany
400
+ # definition:
401
+ # array of unique items of type 'linkage'
402
+ #
403
+ # description:
404
+ # An array of objects each containing "type" and "id" members for to-many relationships
405
+ # structure:
406
+ # [
407
+ # linkage,
408
+ # linkage
409
+ # ]
410
+ # prs:
411
+ # polymorphic
412
+ # [ ] https://github.com/rails-api/active_model_serializers/pull/1282
413
+ #
414
+ # linkage
415
+ # definition:
416
+ # type (required) : String
417
+ # id (required) : String
418
+ # meta
419
+ #
420
+ # description:
421
+ # The "type" and "id" to non-empty members.
422
+ # structure:
423
+ # {
424
+ # type: 'required-type',
425
+ # id: 'required-id',
426
+ # meta: meta
427
+ # }.reject! {|_,v| v.nil? }
428
+ def relationships_for(serializer, requested_associations, options)
429
+ include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations)
430
+ serializer.associations(include_tree).each_with_object({}) do |association, hash|
431
+ hash[association.key] = Relationship.new(
432
+ serializer,
433
+ association.serializer,
434
+ options,
435
+ options: association.options,
436
+ links: association.links,
437
+ meta: association.meta
438
+ ).as_json
439
+ end
440
+ end
441
+
442
+ # {http://jsonapi.org/format/#document-links Document Links}
443
+ # links
444
+ # definition:
445
+ # JSON Object
446
+ #
447
+ # properties:
448
+ # self : URI
449
+ # related : link
450
+ #
451
+ # description:
452
+ # A resource object **MAY** contain references to other resource objects ("relationships").
453
+ # Relationships may be to-one or to-many. Relationships can be specified by including a member
454
+ # in a resource's links object.
455
+ #
456
+ # A `self` member’s value is a URL for the relationship itself (a "relationship URL"). This
457
+ # URL allows the client to directly manipulate the relationship. For example, it would allow
458
+ # a client to remove an `author` from an `article` without deleting the people resource
459
+ # itself.
460
+ # structure:
461
+ # {
462
+ # self: 'http://example.com/etc',
463
+ # related: link
464
+ # }.reject! {|_,v| v.nil? }
465
+ def links_for(serializer)
466
+ serializer._links.each_with_object({}) do |(name, value), hash|
467
+ hash[name] = Link.new(serializer, value).as_json
468
+ end
469
+ end
470
+
471
+ # {http://jsonapi.org/format/#fetching-pagination Pagination Links}
472
+ # pagination
473
+ # definition:
474
+ # first : pageObject
475
+ # last : pageObject
476
+ # prev : pageObject
477
+ # next : pageObject
478
+ # structure:
479
+ # {
480
+ # first: pageObject,
481
+ # last: pageObject,
482
+ # prev: pageObject,
483
+ # next: pageObject
484
+ # }
485
+ #
486
+ # pageObject
487
+ # definition:
488
+ # oneOf
489
+ # URI
490
+ # null
491
+ #
492
+ # description:
493
+ # The <x> page of data
494
+ # structure:
495
+ # if has_page?
496
+ # 'http://example.com/some-page?page[number][x]'
497
+ # else
498
+ # nil
499
+ # end
500
+ # prs:
501
+ # https://github.com/rails-api/active_model_serializers/pull/1041
502
+ def pagination_links_for(serializer, options)
503
+ PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options)
504
+ end
505
+
506
+ # {http://jsonapi.org/format/#document-meta Docment Meta}
507
+ def meta_for(serializer)
508
+ Meta.new(serializer).as_json
509
+ end
510
+ end
511
+ end
512
+ end
513
+ # rubocop:enable Style/AsciiComments