active_model_serializers 0.8.3 → 0.10.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rubocop.yml +86 -0
  4. data/.rubocop_todo.yml +240 -0
  5. data/.simplecov +111 -0
  6. data/.travis.yml +33 -22
  7. data/CHANGELOG.md +358 -6
  8. data/CONTRIBUTING.md +220 -0
  9. data/Gemfile +46 -1
  10. data/{MIT-LICENSE.txt → LICENSE.txt} +3 -2
  11. data/README.md +81 -591
  12. data/Rakefile +68 -11
  13. data/active_model_serializers.gemspec +57 -23
  14. data/appveyor.yml +27 -0
  15. data/docs/ARCHITECTURE.md +120 -0
  16. data/docs/DESIGN.textile +8 -0
  17. data/docs/README.md +35 -0
  18. data/docs/general/adapters.md +162 -0
  19. data/docs/general/caching.md +52 -0
  20. data/docs/general/configuration_options.md +27 -0
  21. data/docs/general/getting_started.md +98 -0
  22. data/docs/general/instrumentation.md +40 -0
  23. data/docs/general/logging.md +14 -0
  24. data/docs/general/rendering.md +153 -0
  25. data/docs/general/serializers.md +207 -0
  26. data/docs/how-open-source-maintained.jpg +0 -0
  27. data/docs/howto/add_pagination_links.md +121 -0
  28. data/docs/howto/add_root_key.md +51 -0
  29. data/docs/howto/outside_controller_use.md +58 -0
  30. data/docs/howto/test.md +152 -0
  31. data/docs/integrations/ember-and-json-api.md +112 -0
  32. data/docs/integrations/grape.md +19 -0
  33. data/docs/jsonapi/schema/schema.json +366 -0
  34. data/docs/jsonapi/schema.md +140 -0
  35. data/lib/action_controller/serialization.rb +41 -37
  36. data/lib/active_model/serializable_resource.rb +72 -0
  37. data/lib/active_model/serializer/adapter/attributes.rb +66 -0
  38. data/lib/active_model/serializer/adapter/base.rb +58 -0
  39. data/lib/active_model/serializer/adapter/cached_serializer.rb +45 -0
  40. data/lib/active_model/serializer/adapter/fragment_cache.rb +111 -0
  41. data/lib/active_model/serializer/adapter/json/fragment_cache.rb +13 -0
  42. data/lib/active_model/serializer/adapter/json.rb +21 -0
  43. data/lib/active_model/serializer/adapter/json_api/deserialization.rb +207 -0
  44. data/lib/active_model/serializer/adapter/json_api/fragment_cache.rb +21 -0
  45. data/lib/active_model/serializer/adapter/json_api/link.rb +44 -0
  46. data/lib/active_model/serializer/adapter/json_api/pagination_links.rb +58 -0
  47. data/lib/active_model/serializer/adapter/json_api.rb +223 -0
  48. data/lib/active_model/serializer/adapter/null.rb +11 -0
  49. data/lib/active_model/serializer/adapter.rb +91 -0
  50. data/lib/active_model/serializer/array_serializer.rb +9 -0
  51. data/lib/active_model/serializer/association.rb +20 -0
  52. data/lib/active_model/serializer/associations.rb +87 -220
  53. data/lib/active_model/serializer/attribute.rb +25 -0
  54. data/lib/active_model/serializer/attributes.rb +82 -0
  55. data/lib/active_model/serializer/belongs_to_reflection.rb +10 -0
  56. data/lib/active_model/serializer/caching.rb +100 -0
  57. data/lib/active_model/serializer/collection_reflection.rb +7 -0
  58. data/lib/active_model/serializer/collection_serializer.rb +47 -0
  59. data/lib/active_model/serializer/configuration.rb +28 -0
  60. data/lib/active_model/serializer/field.rb +56 -0
  61. data/lib/active_model/serializer/fieldset.rb +31 -0
  62. data/lib/active_model/serializer/has_many_reflection.rb +10 -0
  63. data/lib/active_model/serializer/has_one_reflection.rb +10 -0
  64. data/lib/active_model/serializer/include_tree.rb +111 -0
  65. data/lib/active_model/serializer/links.rb +33 -0
  66. data/lib/active_model/serializer/lint.rb +142 -0
  67. data/lib/active_model/serializer/reflection.rb +91 -0
  68. data/lib/active_model/serializer/singular_reflection.rb +7 -0
  69. data/lib/active_model/serializer/type.rb +25 -0
  70. data/lib/active_model/{serializers → serializer}/version.rb +1 -1
  71. data/lib/active_model/serializer.rb +99 -479
  72. data/lib/active_model_serializers/callbacks.rb +55 -0
  73. data/lib/active_model_serializers/deserialization.rb +13 -0
  74. data/lib/active_model_serializers/logging.rb +119 -0
  75. data/lib/active_model_serializers/model.rb +39 -0
  76. data/lib/active_model_serializers/railtie.rb +38 -0
  77. data/lib/active_model_serializers/serialization_context.rb +10 -0
  78. data/lib/active_model_serializers/test/schema.rb +103 -0
  79. data/lib/active_model_serializers/test/serializer.rb +125 -0
  80. data/lib/active_model_serializers/test.rb +7 -0
  81. data/lib/active_model_serializers.rb +20 -92
  82. data/lib/generators/rails/USAGE +6 -0
  83. data/lib/generators/rails/resource_override.rb +10 -0
  84. data/lib/generators/rails/serializer_generator.rb +36 -0
  85. data/lib/generators/rails/templates/serializer.rb.erb +8 -0
  86. data/lib/grape/active_model_serializers.rb +14 -0
  87. data/lib/grape/formatters/active_model_serializers.rb +15 -0
  88. data/lib/grape/helpers/active_model_serializers.rb +16 -0
  89. data/test/action_controller/adapter_selector_test.rb +53 -0
  90. data/test/action_controller/explicit_serializer_test.rb +134 -0
  91. data/test/action_controller/json/include_test.rb +167 -0
  92. data/test/action_controller/json_api/deserialization_test.rb +59 -0
  93. data/test/action_controller/json_api/linked_test.rb +196 -0
  94. data/test/action_controller/json_api/pagination_test.rb +116 -0
  95. data/test/{serialization_scope_name_test.rb → action_controller/serialization_scope_name_test.rb} +11 -15
  96. data/test/action_controller/serialization_test.rb +435 -0
  97. data/test/active_model_serializers/logging_test.rb +77 -0
  98. data/test/active_model_serializers/model_test.rb +9 -0
  99. data/test/active_model_serializers/railtie_test_isolated.rb +57 -0
  100. data/test/active_model_serializers/serialization_context_test.rb +18 -0
  101. data/test/active_model_serializers/test/schema_test.rb +128 -0
  102. data/test/active_model_serializers/test/serializer_test.rb +63 -0
  103. data/test/active_record_test.rb +9 -0
  104. data/test/adapter/fragment_cache_test.rb +38 -0
  105. data/test/adapter/json/belongs_to_test.rb +47 -0
  106. data/test/adapter/json/collection_test.rb +92 -0
  107. data/test/adapter/json/has_many_test.rb +47 -0
  108. data/test/adapter/json_api/belongs_to_test.rb +157 -0
  109. data/test/adapter/json_api/collection_test.rb +97 -0
  110. data/test/adapter/json_api/fields_test.rb +89 -0
  111. data/test/adapter/json_api/has_many_embed_ids_test.rb +45 -0
  112. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +98 -0
  113. data/test/adapter/json_api/has_many_test.rb +145 -0
  114. data/test/adapter/json_api/has_one_test.rb +81 -0
  115. data/test/adapter/json_api/json_api_test.rb +37 -0
  116. data/test/adapter/json_api/linked_test.rb +394 -0
  117. data/test/adapter/json_api/links_test.rb +68 -0
  118. data/test/adapter/json_api/pagination_links_test.rb +115 -0
  119. data/test/adapter/json_api/parse_test.rb +139 -0
  120. data/test/adapter/json_api/resource_type_config_test.rb +71 -0
  121. data/test/adapter/json_api/toplevel_jsonapi_test.rb +84 -0
  122. data/test/adapter/json_test.rb +47 -0
  123. data/test/adapter/null_test.rb +25 -0
  124. data/test/adapter_test.rb +42 -0
  125. data/test/array_serializer_test.rb +36 -73
  126. data/test/collection_serializer_test.rb +100 -0
  127. data/test/fixtures/active_record.rb +56 -0
  128. data/test/fixtures/poro.rb +229 -0
  129. data/test/generators/scaffold_controller_generator_test.rb +24 -0
  130. data/test/generators/serializer_generator_test.rb +57 -0
  131. data/test/grape_test.rb +82 -0
  132. data/test/include_tree/from_include_args_test.rb +26 -0
  133. data/test/include_tree/from_string_test.rb +94 -0
  134. data/test/include_tree/include_args_to_hash_test.rb +64 -0
  135. data/test/lint_test.rb +40 -0
  136. data/test/logger_test.rb +18 -0
  137. data/test/poro_test.rb +9 -0
  138. data/test/serializable_resource_test.rb +27 -0
  139. data/test/serializers/adapter_for_test.rb +166 -0
  140. data/test/serializers/association_macros_test.rb +36 -0
  141. data/test/serializers/associations_test.rb +267 -0
  142. data/test/serializers/attribute_test.rb +123 -0
  143. data/test/serializers/attributes_test.rb +52 -0
  144. data/test/serializers/cache_test.rb +209 -0
  145. data/test/serializers/configuration_test.rb +32 -0
  146. data/test/serializers/fieldset_test.rb +14 -0
  147. data/test/serializers/meta_test.rb +130 -0
  148. data/test/serializers/options_test.rb +21 -0
  149. data/test/serializers/root_test.rb +21 -0
  150. data/test/serializers/serializer_for_test.rb +134 -0
  151. data/test/support/custom_schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  152. data/test/support/isolated_unit.rb +77 -0
  153. data/test/support/rails5_shims.rb +29 -0
  154. data/test/support/rails_app.rb +25 -0
  155. data/test/support/schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  156. data/test/support/schemas/active_model_serializers/test/schema_test/my/show.json +6 -0
  157. data/test/support/schemas/custom/show.json +7 -0
  158. data/test/support/schemas/hyper_schema.json +93 -0
  159. data/test/support/schemas/render_using_json_api.json +43 -0
  160. data/test/support/schemas/simple_json_pointers.json +10 -0
  161. data/test/support/serialization_testing.rb +53 -0
  162. data/test/support/simplecov.rb +6 -0
  163. data/test/support/stream_capture.rb +50 -0
  164. data/test/support/test_case.rb +19 -0
  165. data/test/test_helper.rb +55 -24
  166. metadata +358 -42
  167. data/DESIGN.textile +0 -586
  168. data/Gemfile.edge +0 -9
  169. data/bench/perf.rb +0 -43
  170. data/cruft.md +0 -19
  171. data/lib/active_model/array_serializer.rb +0 -104
  172. data/lib/active_record/serializer_override.rb +0 -16
  173. data/lib/generators/resource_override.rb +0 -13
  174. data/lib/generators/serializer/USAGE +0 -9
  175. data/lib/generators/serializer/serializer_generator.rb +0 -42
  176. data/lib/generators/serializer/templates/serializer.rb +0 -19
  177. data/test/association_test.rb +0 -592
  178. data/test/caching_test.rb +0 -96
  179. data/test/generators_test.rb +0 -85
  180. data/test/no_serialization_scope_test.rb +0 -34
  181. data/test/serialization_test.rb +0 -392
  182. data/test/serializer_support_test.rb +0 -51
  183. data/test/serializer_test.rb +0 -1465
  184. data/test/test_fakes.rb +0 -217
@@ -0,0 +1,72 @@
1
+ require 'set'
2
+ module ActiveModel
3
+ class SerializableResource
4
+ ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :meta, :meta_key, :links])
5
+ include ActiveModelSerializers::Logging
6
+
7
+ delegate :serializable_hash, :as_json, :to_json, to: :adapter
8
+ notify :serializable_hash, :render
9
+ notify :as_json, :render
10
+ notify :to_json, :render
11
+
12
+ # Primary interface to composing a resource with a serializer and adapter.
13
+ # @return the serializable_resource, ready for #as_json/#to_json/#serializable_hash.
14
+ def initialize(resource, options = {})
15
+ @resource = resource
16
+ @adapter_opts, @serializer_opts =
17
+ options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
18
+ end
19
+
20
+ def serialization_scope=(scope)
21
+ serializer_opts[:scope] = scope
22
+ end
23
+
24
+ def serialization_scope
25
+ serializer_opts[:scope]
26
+ end
27
+
28
+ def serialization_scope_name=(scope_name)
29
+ serializer_opts[:scope_name] = scope_name
30
+ end
31
+
32
+ def adapter
33
+ @adapter ||= ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)
34
+ end
35
+ alias_method :adapter_instance, :adapter
36
+
37
+ def serializer_instance
38
+ @serializer_instance ||= serializer.new(resource, serializer_opts)
39
+ end
40
+
41
+ # Get serializer either explicitly :serializer or implicitly from resource
42
+ # Remove :serializer key from serializer_opts
43
+ # Replace :serializer key with :each_serializer if present
44
+ def serializer
45
+ @serializer ||=
46
+ begin
47
+ @serializer = serializer_opts.delete(:serializer)
48
+ @serializer ||= ActiveModel::Serializer.serializer_for(resource)
49
+
50
+ if serializer_opts.key?(:each_serializer)
51
+ serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
52
+ end
53
+ @serializer
54
+ end
55
+ end
56
+ alias_method :serializer_class, :serializer
57
+
58
+ # True when no explicit adapter given, or explicit appear is truthy (non-nil)
59
+ # False when explicit adapter is falsy (nil or false)
60
+ def use_adapter?
61
+ !(adapter_opts.key?(:adapter) && !adapter_opts[:adapter])
62
+ end
63
+
64
+ def serializer?
65
+ use_adapter? && !!(serializer)
66
+ end
67
+
68
+ protected
69
+
70
+ attr_reader :resource, :adapter_opts, :serializer_opts
71
+ end
72
+ end
@@ -0,0 +1,66 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class Attributes < Base
5
+ def initialize(serializer, options = {})
6
+ super
7
+ @include_tree = IncludeTree.from_include_args(options[:include] || '*')
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
+ def fragment_cache(cached_hash, non_cached_hash)
21
+ Json::FragmentCache.new.fragment_cache(cached_hash, non_cached_hash)
22
+ end
23
+
24
+ private
25
+
26
+ def serializable_hash_for_collection(options)
27
+ serializer.map { |s| Attributes.new(s, instance_options).serializable_hash(options) }
28
+ end
29
+
30
+ def serializable_hash_for_single_resource(options)
31
+ resource = resource_object_for(options)
32
+ relationships = resource_relationships(options)
33
+ resource.merge!(relationships)
34
+ end
35
+
36
+ def resource_relationships(options)
37
+ relationships = {}
38
+ serializer.associations(@include_tree).each do |association|
39
+ relationships[association.key] = relationship_value_for(association, options)
40
+ end
41
+
42
+ relationships
43
+ end
44
+
45
+ def relationship_value_for(association, options)
46
+ return association.options[:virtual_value] if association.options[:virtual_value]
47
+ return unless association.serializer && association.serializer.object
48
+
49
+ opts = instance_options.merge(include: @include_tree[association.key])
50
+ Attributes.new(association.serializer, opts).serializable_hash(options)
51
+ end
52
+
53
+ # no-op: Attributes adapter does not include meta data, because it does not support root.
54
+ def include_meta(json)
55
+ json
56
+ end
57
+
58
+ def resource_object_for(options)
59
+ cache_check(serializer) do
60
+ serializer.attributes(options[:fields])
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,58 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class Base
5
+ # Automatically register adapters when subclassing
6
+ def self.inherited(subclass)
7
+ ActiveModel::Serializer::Adapter.register(subclass)
8
+ end
9
+
10
+ attr_reader :serializer, :instance_options
11
+
12
+ def initialize(serializer, options = {})
13
+ @serializer = serializer
14
+ @instance_options = options
15
+ end
16
+
17
+ def serializable_hash(_options = nil)
18
+ fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
19
+ end
20
+
21
+ def as_json(options = nil)
22
+ hash = serializable_hash(options)
23
+ include_meta(hash)
24
+ hash
25
+ end
26
+
27
+ def fragment_cache(*_args)
28
+ fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
29
+ end
30
+
31
+ def cache_check(serializer)
32
+ CachedSerializer.new(serializer).cache_check(self) do
33
+ yield
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def meta
40
+ instance_options.fetch(:meta, nil)
41
+ end
42
+
43
+ def meta_key
44
+ instance_options.fetch(:meta_key, 'meta'.freeze)
45
+ end
46
+
47
+ def root
48
+ serializer.json_key.to_sym if serializer.json_key
49
+ end
50
+
51
+ def include_meta(json)
52
+ json[meta_key] = meta if meta
53
+ json
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class CachedSerializer
5
+ def initialize(serializer)
6
+ @cached_serializer = serializer
7
+ @klass = @cached_serializer.class
8
+ end
9
+
10
+ def cache_check(adapter_instance)
11
+ if cached?
12
+ @klass._cache.fetch(cache_key, @klass._cache_options) do
13
+ yield
14
+ end
15
+ elsif fragment_cached?
16
+ FragmentCache.new(adapter_instance, @cached_serializer, adapter_instance.instance_options).fetch
17
+ else
18
+ yield
19
+ end
20
+ end
21
+
22
+ def cached?
23
+ @klass._cache && !@klass._cache_only && !@klass._cache_except
24
+ end
25
+
26
+ def fragment_cached?
27
+ @klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except
28
+ end
29
+
30
+ def cache_key
31
+ parts = []
32
+ parts << object_cache_key
33
+ parts << @klass._cache_digest unless @klass._cache_options && @klass._cache_options[:skip_digest]
34
+ parts.join('/')
35
+ end
36
+
37
+ def object_cache_key
38
+ object_time_safe = @cached_serializer.object.updated_at
39
+ object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
40
+ (@klass._cache_key) ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{object_time_safe}" : @cached_serializer.object.cache_key
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,111 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class FragmentCache
5
+ attr_reader :serializer
6
+
7
+ def initialize(adapter, serializer, options)
8
+ @instance_options = options
9
+ @adapter = adapter
10
+ @serializer = serializer
11
+ end
12
+
13
+ # TODO: Use Serializable::Resource
14
+ # TODO: call +constantize+ less
15
+ # 1. Create a CachedSerializer and NonCachedSerializer from the serializer class
16
+ # 2. Serialize the above two with the given adapter
17
+ # 3. Pass their serializations to the adapter +::fragment_cache+
18
+ def fetch
19
+ klass = serializer.class
20
+ # It will split the serializer into two, one that will be cached and one that will not
21
+ serializers = fragment_serializer(serializer.object.class.name, klass)
22
+
23
+ # Instantiate both serializers
24
+ cached_serializer = serializers[:cached].constantize.new(serializer.object)
25
+ non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)
26
+
27
+ cached_adapter = adapter.class.new(cached_serializer, instance_options)
28
+ non_cached_adapter = adapter.class.new(non_cached_serializer, instance_options)
29
+
30
+ # Get serializable hash from both
31
+ cached_hash = cached_adapter.serializable_hash
32
+ non_cached_hash = non_cached_adapter.serializable_hash
33
+
34
+ # Merge both results
35
+ adapter.fragment_cache(cached_hash, non_cached_hash)
36
+ end
37
+
38
+ protected
39
+
40
+ attr_reader :instance_options, :adapter
41
+
42
+ private
43
+
44
+ # Given a serializer class and a hash of its cached and non-cached serializers
45
+ # 1. Determine cached attributes from serializer class options
46
+ # 2. Add cached attributes to cached Serializer
47
+ # 3. Add non-cached attributes to non-cached Serializer
48
+ def cached_attributes(klass, serializers)
49
+ attributes = serializer.class._attributes
50
+ cached_attributes = (klass._cache_only) ? klass._cache_only : attributes.reject { |attr| klass._cache_except.include?(attr) }
51
+ non_cached_attributes = attributes - cached_attributes
52
+
53
+ cached_attributes.each do |attribute|
54
+ options = serializer.class._attributes_keys[attribute]
55
+ options ||= {}
56
+ # Add cached attributes to cached Serializer
57
+ serializers[:cached].constantize.attribute(attribute, options)
58
+ end
59
+
60
+ non_cached_attributes.each do |attribute|
61
+ options = serializer.class._attributes_keys[attribute]
62
+ options ||= {}
63
+ # Add non-cached attributes to non-cached Serializer
64
+ serializers[:non_cached].constantize.attribute(attribute, options)
65
+ end
66
+ end
67
+
68
+ # Given a resource name and its serializer's class
69
+ # 1. Dyanmically creates a CachedSerializer and NonCachedSerializer
70
+ # for a given class 'name'
71
+ # 2. Call
72
+ # CachedSerializer.cache(serializer._cache_options)
73
+ # CachedSerializer.fragmented(serializer)
74
+ # NontCachedSerializer.cache(serializer._cache_options)
75
+ # 3. Build a hash keyed to the +cached+ and +non_cached+ serializers
76
+ # 4. Call +cached_attributes+ on the serializer class and the above hash
77
+ # 5. Return the hash
78
+ #
79
+ # @example
80
+ # When +name+ is <tt>User::Admin</tt>
81
+ # creates the Serializer classes (if they don't exist).
82
+ # User_AdminCachedSerializer
83
+ # User_AdminNOnCachedSerializer
84
+ #
85
+ def fragment_serializer(name, klass)
86
+ cached = "#{to_valid_const_name(name)}CachedSerializer"
87
+ non_cached = "#{to_valid_const_name(name)}NonCachedSerializer"
88
+
89
+ Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
90
+ Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
91
+
92
+ klass._cache_options ||= {}
93
+ klass._cache_options[:key] = klass._cache_key if klass._cache_key
94
+
95
+ cached.constantize.cache(klass._cache_options)
96
+
97
+ cached.constantize.fragmented(serializer)
98
+ non_cached.constantize.fragmented(serializer)
99
+
100
+ serializers = { cached: cached, non_cached: non_cached }
101
+ cached_attributes(klass, serializers)
102
+ serializers
103
+ end
104
+
105
+ def to_valid_const_name(name)
106
+ name.gsub('::', '_')
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class Json
5
+ class FragmentCache
6
+ def fragment_cache(cached_hash, non_cached_hash)
7
+ non_cached_hash.merge cached_hash
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class Json < Base
5
+ extend ActiveSupport::Autoload
6
+ autoload :FragmentCache
7
+
8
+ def serializable_hash(options = nil)
9
+ options ||= {}
10
+ { root => Attributes.new(serializer, instance_options).serializable_hash(options) }
11
+ end
12
+
13
+ private
14
+
15
+ def fragment_cache(cached_hash, non_cached_hash)
16
+ ActiveModel::Serializer::Adapter::Json::FragmentCache.new.fragment_cache(cached_hash, non_cached_hash)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,207 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class JsonApi
5
+ # NOTE(Experimental):
6
+ # This is an experimental feature. Both the interface and internals could be subject
7
+ # to changes.
8
+ module Deserialization
9
+ InvalidDocument = Class.new(ArgumentError)
10
+
11
+ module_function
12
+
13
+ # Transform a JSON API document, containing a single data object,
14
+ # into a hash that is ready for ActiveRecord::Base.new() and such.
15
+ # Raises InvalidDocument if the payload is not properly formatted.
16
+ #
17
+ # @param [Hash|ActionController::Parameters] document
18
+ # @param [Hash] options
19
+ # only: Array of symbols of whitelisted fields.
20
+ # except: Array of symbols of blacklisted fields.
21
+ # keys: Hash of translated keys (e.g. :author => :user).
22
+ # polymorphic: Array of symbols of polymorphic fields.
23
+ # @return [Hash]
24
+ #
25
+ # @example
26
+ # document = {
27
+ # data: {
28
+ # id: 1,
29
+ # type: 'post',
30
+ # attributes: {
31
+ # title: 'Title 1',
32
+ # date: '2015-12-20'
33
+ # },
34
+ # associations: {
35
+ # author: {
36
+ # data: {
37
+ # type: 'user',
38
+ # id: 2
39
+ # }
40
+ # },
41
+ # second_author: {
42
+ # data: nil
43
+ # },
44
+ # comments: {
45
+ # data: [{
46
+ # type: 'comment',
47
+ # id: 3
48
+ # },{
49
+ # type: 'comment',
50
+ # id: 4
51
+ # }]
52
+ # }
53
+ # }
54
+ # }
55
+ # }
56
+ #
57
+ # parse(document) #=>
58
+ # # {
59
+ # # title: 'Title 1',
60
+ # # date: '2015-12-20',
61
+ # # author_id: 2,
62
+ # # second_author_id: nil
63
+ # # comment_ids: [3, 4]
64
+ # # }
65
+ #
66
+ # parse(document, only: [:title, :date, :author],
67
+ # keys: { date: :published_at },
68
+ # polymorphic: [:author]) #=>
69
+ # # {
70
+ # # title: 'Title 1',
71
+ # # published_at: '2015-12-20',
72
+ # # author_id: '2',
73
+ # # author_type: 'people'
74
+ # # }
75
+ #
76
+ def parse!(document, options = {})
77
+ parse(document, options) do |invalid_payload, reason|
78
+ fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"
79
+ end
80
+ end
81
+
82
+ # Same as parse!, but returns an empty hash instead of raising InvalidDocument
83
+ # on invalid payloads.
84
+ def parse(document, options = {})
85
+ document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)
86
+
87
+ validate_payload(document) do |invalid_document, reason|
88
+ yield invalid_document, reason if block_given?
89
+ return {}
90
+ end
91
+
92
+ primary_data = document['data']
93
+ attributes = primary_data['attributes'] || {}
94
+ attributes['id'] = primary_data['id'] if primary_data['id']
95
+ relationships = primary_data['relationships'] || {}
96
+
97
+ filter_fields(attributes, options)
98
+ filter_fields(relationships, options)
99
+
100
+ hash = {}
101
+ hash.merge!(parse_attributes(attributes, options))
102
+ hash.merge!(parse_relationships(relationships, options))
103
+
104
+ hash
105
+ end
106
+
107
+ # Checks whether a payload is compliant with the JSON API spec.
108
+ #
109
+ # @api private
110
+ # rubocop:disable Metrics/CyclomaticComplexity
111
+ def validate_payload(payload)
112
+ unless payload.is_a?(Hash)
113
+ yield payload, 'Expected hash'
114
+ return
115
+ end
116
+
117
+ primary_data = payload['data']
118
+ unless primary_data.is_a?(Hash)
119
+ yield payload, { data: 'Expected hash' }
120
+ return
121
+ end
122
+
123
+ attributes = primary_data['attributes'] || {}
124
+ unless attributes.is_a?(Hash)
125
+ yield payload, { data: { attributes: 'Expected hash or nil' } }
126
+ return
127
+ end
128
+
129
+ relationships = primary_data['relationships'] || {}
130
+ unless relationships.is_a?(Hash)
131
+ yield payload, { data: { relationships: 'Expected hash or nil' } }
132
+ return
133
+ end
134
+
135
+ relationships.each do |(key, value)|
136
+ unless value.is_a?(Hash) && value.key?('data')
137
+ yield payload, { data: { relationships: { key => 'Expected hash with :data key' } } }
138
+ end
139
+ end
140
+ end
141
+ # rubocop:enable Metrics/CyclomaticComplexity
142
+
143
+ # @api private
144
+ def filter_fields(fields, options)
145
+ if (only = options[:only])
146
+ fields.slice!(*Array(only).map(&:to_s))
147
+ elsif (except = options[:except])
148
+ fields.except!(*Array(except).map(&:to_s))
149
+ end
150
+ end
151
+
152
+ # @api private
153
+ def field_key(field, options)
154
+ (options[:keys] || {}).fetch(field.to_sym, field).to_sym
155
+ end
156
+
157
+ # @api private
158
+ def parse_attributes(attributes, options)
159
+ attributes
160
+ .map { |(k, v)| { field_key(k, options) => v } }
161
+ .reduce({}, :merge)
162
+ end
163
+
164
+ # Given an association name, and a relationship data attribute, build a hash
165
+ # mapping the corresponding ActiveRecord attribute to the corresponding value.
166
+ #
167
+ # @example
168
+ # parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },
169
+ # { 'id' => '2', 'type' => 'comments' }],
170
+ # {})
171
+ # # => { :comment_ids => ['1', '2'] }
172
+ # parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})
173
+ # # => { :author_id => '1' }
174
+ # parse_relationship(:author, nil, {})
175
+ # # => { :author_id => nil }
176
+ # @param [Symbol] assoc_name
177
+ # @param [Hash] assoc_data
178
+ # @param [Hash] options
179
+ # @return [Hash{Symbol, Object}]
180
+ #
181
+ # @api private
182
+ def parse_relationship(assoc_name, assoc_data, options)
183
+ prefix_key = field_key(assoc_name, options).to_s.singularize
184
+ hash =
185
+ if assoc_data.is_a?(Array)
186
+ { "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }
187
+ else
188
+ { "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }
189
+ end
190
+
191
+ polymorphic = (options[:polymorphic] || []).include?(assoc_name.to_sym)
192
+ hash.merge!("#{prefix_key}_type".to_sym => assoc_data['type']) if polymorphic
193
+
194
+ hash
195
+ end
196
+
197
+ # @api private
198
+ def parse_relationships(relationships, options)
199
+ relationships
200
+ .map { |(k, v)| parse_relationship(k, v['data'], options) }
201
+ .reduce({}, :merge)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class JsonApi
5
+ class FragmentCache
6
+ def fragment_cache(root, cached_hash, non_cached_hash)
7
+ hash = {}
8
+ core_cached = cached_hash.first
9
+ core_non_cached = non_cached_hash.first
10
+ no_root_cache = cached_hash.delete_if { |key, value| key == core_cached[0] }
11
+ no_root_non_cache = non_cached_hash.delete_if { |key, value| key == core_non_cached[0] }
12
+ cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
13
+ hash = (root) ? { root => cached_resource } : cached_resource
14
+
15
+ hash.deep_merge no_root_non_cache.deep_merge no_root_cache
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveModel
2
+ class Serializer
3
+ module Adapter
4
+ class JsonApi
5
+ class Link
6
+ def initialize(serializer, value)
7
+ @object = serializer.object
8
+ @scope = serializer.scope
9
+
10
+ # Use the return value of the block unless it is nil.
11
+ if value.respond_to?(:call)
12
+ @value = instance_eval(&value)
13
+ else
14
+ @value = value
15
+ end
16
+ end
17
+
18
+ def href(value)
19
+ @href = value
20
+ nil
21
+ end
22
+
23
+ def meta(value)
24
+ @meta = value
25
+ nil
26
+ end
27
+
28
+ def as_json
29
+ return @value if @value
30
+
31
+ hash = { href: @href }
32
+ hash.merge!(meta: @meta) if @meta
33
+
34
+ hash
35
+ end
36
+
37
+ protected
38
+
39
+ attr_reader :object, :scope
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end