agi_active_model_serializers 0.10.7

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