active_model_serializers_custom 0.10.90

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 (215) 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 +109 -0
  6. data/.simplecov +110 -0
  7. data/.travis.yml +63 -0
  8. data/CHANGELOG.md +727 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/CONTRIBUTING.md +105 -0
  11. data/Gemfile +74 -0
  12. data/MIT-LICENSE +22 -0
  13. data/README.md +305 -0
  14. data/Rakefile +76 -0
  15. data/active_model_serializers.gemspec +64 -0
  16. data/appveyor.yml +28 -0
  17. data/bin/bench +171 -0
  18. data/bin/bench_regression +316 -0
  19. data/bin/rubocop +38 -0
  20. data/bin/serve_benchmark +39 -0
  21. data/docs/README.md +41 -0
  22. data/docs/STYLE.md +58 -0
  23. data/docs/general/adapters.md +269 -0
  24. data/docs/general/caching.md +58 -0
  25. data/docs/general/configuration_options.md +185 -0
  26. data/docs/general/deserialization.md +100 -0
  27. data/docs/general/fields.md +31 -0
  28. data/docs/general/getting_started.md +133 -0
  29. data/docs/general/instrumentation.md +40 -0
  30. data/docs/general/key_transforms.md +40 -0
  31. data/docs/general/logging.md +21 -0
  32. data/docs/general/rendering.md +293 -0
  33. data/docs/general/serializers.md +495 -0
  34. data/docs/how-open-source-maintained.jpg +0 -0
  35. data/docs/howto/add_pagination_links.md +138 -0
  36. data/docs/howto/add_relationship_links.md +140 -0
  37. data/docs/howto/add_root_key.md +62 -0
  38. data/docs/howto/grape_integration.md +42 -0
  39. data/docs/howto/outside_controller_use.md +66 -0
  40. data/docs/howto/passing_arbitrary_options.md +27 -0
  41. data/docs/howto/serialize_poro.md +73 -0
  42. data/docs/howto/test.md +154 -0
  43. data/docs/howto/upgrade_from_0_8_to_0_10.md +265 -0
  44. data/docs/integrations/ember-and-json-api.md +147 -0
  45. data/docs/integrations/grape.md +19 -0
  46. data/docs/jsonapi/errors.md +56 -0
  47. data/docs/jsonapi/schema.md +151 -0
  48. data/docs/jsonapi/schema/schema.json +366 -0
  49. data/docs/rfcs/0000-namespace.md +106 -0
  50. data/docs/rfcs/template.md +15 -0
  51. data/lib/action_controller/serialization.rb +76 -0
  52. data/lib/active_model/serializable_resource.rb +13 -0
  53. data/lib/active_model/serializer.rb +418 -0
  54. data/lib/active_model/serializer/adapter.rb +26 -0
  55. data/lib/active_model/serializer/adapter/attributes.rb +17 -0
  56. data/lib/active_model/serializer/adapter/base.rb +20 -0
  57. data/lib/active_model/serializer/adapter/json.rb +17 -0
  58. data/lib/active_model/serializer/adapter/json_api.rb +17 -0
  59. data/lib/active_model/serializer/adapter/null.rb +17 -0
  60. data/lib/active_model/serializer/array_serializer.rb +14 -0
  61. data/lib/active_model/serializer/association.rb +91 -0
  62. data/lib/active_model/serializer/attribute.rb +27 -0
  63. data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
  64. data/lib/active_model/serializer/collection_serializer.rb +90 -0
  65. data/lib/active_model/serializer/concerns/caching.rb +304 -0
  66. data/lib/active_model/serializer/error_serializer.rb +16 -0
  67. data/lib/active_model/serializer/errors_serializer.rb +34 -0
  68. data/lib/active_model/serializer/field.rb +92 -0
  69. data/lib/active_model/serializer/fieldset.rb +33 -0
  70. data/lib/active_model/serializer/has_many_reflection.rb +12 -0
  71. data/lib/active_model/serializer/has_one_reflection.rb +9 -0
  72. data/lib/active_model/serializer/lazy_association.rb +99 -0
  73. data/lib/active_model/serializer/link.rb +23 -0
  74. data/lib/active_model/serializer/lint.rb +152 -0
  75. data/lib/active_model/serializer/null.rb +19 -0
  76. data/lib/active_model/serializer/reflection.rb +212 -0
  77. data/lib/active_model/serializer/version.rb +7 -0
  78. data/lib/active_model_serializers.rb +63 -0
  79. data/lib/active_model_serializers/adapter.rb +100 -0
  80. data/lib/active_model_serializers/adapter/attributes.rb +15 -0
  81. data/lib/active_model_serializers/adapter/base.rb +85 -0
  82. data/lib/active_model_serializers/adapter/json.rb +23 -0
  83. data/lib/active_model_serializers/adapter/json_api.rb +535 -0
  84. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
  85. data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
  86. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
  87. data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
  88. data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
  89. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
  90. data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
  91. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
  92. data/lib/active_model_serializers/adapter/null.rb +11 -0
  93. data/lib/active_model_serializers/callbacks.rb +57 -0
  94. data/lib/active_model_serializers/deprecate.rb +56 -0
  95. data/lib/active_model_serializers/deserialization.rb +17 -0
  96. data/lib/active_model_serializers/json_pointer.rb +16 -0
  97. data/lib/active_model_serializers/logging.rb +124 -0
  98. data/lib/active_model_serializers/lookup_chain.rb +82 -0
  99. data/lib/active_model_serializers/model.rb +132 -0
  100. data/lib/active_model_serializers/railtie.rb +52 -0
  101. data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
  102. data/lib/active_model_serializers/serializable_resource.rb +84 -0
  103. data/lib/active_model_serializers/serialization_context.rb +41 -0
  104. data/lib/active_model_serializers/test.rb +9 -0
  105. data/lib/active_model_serializers/test/schema.rb +140 -0
  106. data/lib/active_model_serializers/test/serializer.rb +127 -0
  107. data/lib/generators/rails/USAGE +6 -0
  108. data/lib/generators/rails/resource_override.rb +12 -0
  109. data/lib/generators/rails/serializer_generator.rb +38 -0
  110. data/lib/generators/rails/templates/serializer.rb.erb +8 -0
  111. data/lib/grape/active_model_serializers.rb +18 -0
  112. data/lib/grape/formatters/active_model_serializers.rb +34 -0
  113. data/lib/grape/helpers/active_model_serializers.rb +19 -0
  114. data/lib/tasks/rubocop.rake +55 -0
  115. data/test/action_controller/adapter_selector_test.rb +64 -0
  116. data/test/action_controller/explicit_serializer_test.rb +137 -0
  117. data/test/action_controller/json/include_test.rb +248 -0
  118. data/test/action_controller/json_api/deserialization_test.rb +114 -0
  119. data/test/action_controller/json_api/errors_test.rb +42 -0
  120. data/test/action_controller/json_api/fields_test.rb +68 -0
  121. data/test/action_controller/json_api/linked_test.rb +204 -0
  122. data/test/action_controller/json_api/pagination_test.rb +126 -0
  123. data/test/action_controller/json_api/transform_test.rb +191 -0
  124. data/test/action_controller/lookup_proc_test.rb +51 -0
  125. data/test/action_controller/namespace_lookup_test.rb +239 -0
  126. data/test/action_controller/serialization_scope_name_test.rb +237 -0
  127. data/test/action_controller/serialization_test.rb +480 -0
  128. data/test/active_model_serializers/adapter_for_test.rb +210 -0
  129. data/test/active_model_serializers/json_pointer_test.rb +24 -0
  130. data/test/active_model_serializers/logging_test.rb +79 -0
  131. data/test/active_model_serializers/model_test.rb +144 -0
  132. data/test/active_model_serializers/railtie_test_isolated.rb +70 -0
  133. data/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb +163 -0
  134. data/test/active_model_serializers/serialization_context_test_isolated.rb +73 -0
  135. data/test/active_model_serializers/test/schema_test.rb +133 -0
  136. data/test/active_model_serializers/test/serializer_test.rb +64 -0
  137. data/test/active_record_test.rb +11 -0
  138. data/test/adapter/attributes_test.rb +42 -0
  139. data/test/adapter/deprecation_test.rb +102 -0
  140. data/test/adapter/json/belongs_to_test.rb +47 -0
  141. data/test/adapter/json/collection_test.rb +106 -0
  142. data/test/adapter/json/has_many_test.rb +55 -0
  143. data/test/adapter/json/transform_test.rb +95 -0
  144. data/test/adapter/json_api/belongs_to_test.rb +157 -0
  145. data/test/adapter/json_api/collection_test.rb +98 -0
  146. data/test/adapter/json_api/errors_test.rb +78 -0
  147. data/test/adapter/json_api/fields_test.rb +98 -0
  148. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +98 -0
  149. data/test/adapter/json_api/has_many_test.rb +175 -0
  150. data/test/adapter/json_api/has_one_test.rb +82 -0
  151. data/test/adapter/json_api/include_data_if_sideloaded_test.rb +215 -0
  152. data/test/adapter/json_api/json_api_test.rb +35 -0
  153. data/test/adapter/json_api/linked_test.rb +415 -0
  154. data/test/adapter/json_api/links_test.rb +112 -0
  155. data/test/adapter/json_api/pagination_links_test.rb +208 -0
  156. data/test/adapter/json_api/parse_test.rb +139 -0
  157. data/test/adapter/json_api/relationship_test.rb +399 -0
  158. data/test/adapter/json_api/resource_meta_test.rb +102 -0
  159. data/test/adapter/json_api/toplevel_jsonapi_test.rb +84 -0
  160. data/test/adapter/json_api/transform_test.rb +514 -0
  161. data/test/adapter/json_api/type_test.rb +195 -0
  162. data/test/adapter/json_test.rb +48 -0
  163. data/test/adapter/null_test.rb +24 -0
  164. data/test/adapter/polymorphic_test.rb +220 -0
  165. data/test/adapter_test.rb +69 -0
  166. data/test/array_serializer_test.rb +24 -0
  167. data/test/benchmark/app.rb +67 -0
  168. data/test/benchmark/benchmarking_support.rb +69 -0
  169. data/test/benchmark/bm_active_record.rb +83 -0
  170. data/test/benchmark/bm_adapter.rb +40 -0
  171. data/test/benchmark/bm_caching.rb +121 -0
  172. data/test/benchmark/bm_lookup_chain.rb +85 -0
  173. data/test/benchmark/bm_transform.rb +47 -0
  174. data/test/benchmark/config.ru +3 -0
  175. data/test/benchmark/controllers.rb +85 -0
  176. data/test/benchmark/fixtures.rb +221 -0
  177. data/test/cache_test.rb +717 -0
  178. data/test/collection_serializer_test.rb +129 -0
  179. data/test/fixtures/active_record.rb +115 -0
  180. data/test/fixtures/poro.rb +227 -0
  181. data/test/generators/scaffold_controller_generator_test.rb +26 -0
  182. data/test/generators/serializer_generator_test.rb +77 -0
  183. data/test/grape_test.rb +198 -0
  184. data/test/lint_test.rb +51 -0
  185. data/test/logger_test.rb +22 -0
  186. data/test/poro_test.rb +11 -0
  187. data/test/serializable_resource_test.rb +81 -0
  188. data/test/serializers/association_macros_test.rb +39 -0
  189. data/test/serializers/associations_test.rb +520 -0
  190. data/test/serializers/attribute_test.rb +155 -0
  191. data/test/serializers/attributes_test.rb +54 -0
  192. data/test/serializers/caching_configuration_test_isolated.rb +172 -0
  193. data/test/serializers/configuration_test.rb +34 -0
  194. data/test/serializers/fieldset_test.rb +16 -0
  195. data/test/serializers/meta_test.rb +204 -0
  196. data/test/serializers/options_test.rb +34 -0
  197. data/test/serializers/read_attribute_for_serialization_test.rb +81 -0
  198. data/test/serializers/reflection_test.rb +481 -0
  199. data/test/serializers/root_test.rb +23 -0
  200. data/test/serializers/serialization_test.rb +57 -0
  201. data/test/serializers/serializer_for_test.rb +138 -0
  202. data/test/serializers/serializer_for_with_namespace_test.rb +90 -0
  203. data/test/support/custom_schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  204. data/test/support/isolated_unit.rb +86 -0
  205. data/test/support/rails5_shims.rb +55 -0
  206. data/test/support/rails_app.rb +40 -0
  207. data/test/support/schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  208. data/test/support/schemas/active_model_serializers/test/schema_test/my/show.json +6 -0
  209. data/test/support/schemas/custom/show.json +7 -0
  210. data/test/support/schemas/hyper_schema.json +93 -0
  211. data/test/support/schemas/render_using_json_api.json +43 -0
  212. data/test/support/schemas/simple_json_pointers.json +10 -0
  213. data/test/support/serialization_testing.rb +81 -0
  214. data/test/test_helper.rb +72 -0
  215. metadata +622 -0
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './benchmarking_support'
4
+ require_relative './app'
5
+
6
+ time = 10
7
+ disable_gc = true
8
+ ActiveModelSerializers.config.key_transform = :unaltered
9
+
10
+ module AmsBench
11
+ module Api
12
+ module V1
13
+ class PrimaryResourceSerializer < ActiveModel::Serializer
14
+ attributes :title, :body
15
+
16
+ has_many :has_many_relationships
17
+ end
18
+
19
+ class HasManyRelationshipSerializer < ActiveModel::Serializer
20
+ attribute :body
21
+ end
22
+ end
23
+ end
24
+ class PrimaryResourceSerializer < ActiveModel::Serializer
25
+ attributes :title, :body
26
+
27
+ has_many :has_many_relationships
28
+
29
+ class HasManyRelationshipSerializer < ActiveModel::Serializer
30
+ attribute :body
31
+ end
32
+ end
33
+ end
34
+
35
+ resource = PrimaryResource.new(
36
+ id: 1,
37
+ title: 'title',
38
+ body: 'body',
39
+ has_many_relationships: [
40
+ HasManyRelationship.new(id: 1, body: 'body1'),
41
+ HasManyRelationship.new(id: 2, body: 'body1')
42
+ ]
43
+ )
44
+
45
+ serialization = lambda do
46
+ ActiveModelSerializers::SerializableResource.new(resource, serializer: AmsBench::PrimaryResourceSerializer).as_json
47
+ ActiveModelSerializers::SerializableResource.new(resource, namespace: AmsBench::Api::V1).as_json
48
+ ActiveModelSerializers::SerializableResource.new(resource).as_json
49
+ end
50
+
51
+ def clear_cache
52
+ AmsBench::PrimaryResourceSerializer.serializers_cache.clear
53
+ AmsBench::Api::V1::PrimaryResourceSerializer.serializers_cache.clear
54
+ ActiveModel::Serializer.serializers_cache.clear
55
+ end
56
+
57
+ configurable = lambda do
58
+ clear_cache
59
+ Benchmark.ams('Configurable Lookup Chain', time: time, disable_gc: disable_gc, &serialization)
60
+ end
61
+
62
+ old = lambda do
63
+ clear_cache
64
+ module ActiveModel
65
+ class Serializer
66
+ def self.serializer_lookup_chain_for(klass, namespace = nil)
67
+ chain = []
68
+
69
+ resource_class_name = klass.name.demodulize
70
+ resource_namespace = klass.name.deconstantize
71
+ serializer_class_name = "#{resource_class_name}Serializer"
72
+
73
+ chain.push("#{namespace}::#{serializer_class_name}") if namespace
74
+ chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
75
+ chain.push("#{resource_namespace}::#{serializer_class_name}")
76
+ chain
77
+ end
78
+ end
79
+ end
80
+
81
+ Benchmark.ams('Old Lookup Chain (v0.10)', time: time, disable_gc: disable_gc, &serialization)
82
+ end
83
+
84
+ configurable.call
85
+ old.call
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './benchmarking_support'
4
+ require_relative './app'
5
+
6
+ time = 10
7
+ disable_gc = true
8
+ ActiveModelSerializers.config.key_transform = :unaltered
9
+ has_many_relationships = (0..50).map do |i|
10
+ HasManyRelationship.new(id: i, body: 'ZOMG A HAS MANY RELATIONSHIP')
11
+ end
12
+ has_one_relationship = HasOneRelationship.new(
13
+ id: 42,
14
+ first_name: 'Joao',
15
+ last_name: 'Moura'
16
+ )
17
+ primary_resource = PrimaryResource.new(
18
+ id: 1337,
19
+ title: 'New PrimaryResource',
20
+ virtual_attribute: nil,
21
+ body: 'Body',
22
+ has_many_relationships: has_many_relationships,
23
+ has_one_relationship: has_one_relationship
24
+ )
25
+ serializer = PrimaryResourceSerializer.new(primary_resource)
26
+ adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
27
+ serialization = adapter.as_json
28
+
29
+ Benchmark.ams('camel', time: time, disable_gc: disable_gc) do
30
+ CaseTransform.camel(serialization)
31
+ end
32
+
33
+ Benchmark.ams('camel_lower', time: time, disable_gc: disable_gc) do
34
+ CaseTransform.camel_lower(serialization)
35
+ end
36
+
37
+ Benchmark.ams('dash', time: time, disable_gc: disable_gc) do
38
+ CaseTransform.dash(serialization)
39
+ end
40
+
41
+ Benchmark.ams('unaltered', time: time, disable_gc: disable_gc) do
42
+ CaseTransform.unaltered(serialization)
43
+ end
44
+
45
+ Benchmark.ams('underscore', time: time, disable_gc: disable_gc) do
46
+ CaseTransform.underscore(serialization)
47
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path(['..', 'app'].join(File::SEPARATOR), __FILE__)
2
+
3
+ run Rails.application
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PrimaryResourceController < ActionController::Base
4
+ PRIMARY_RESOURCE =
5
+ begin
6
+ if ENV['BENCH_STRESS']
7
+ has_many_relationships = (0..50).map do |i|
8
+ HasManyRelationship.new(id: i, body: 'ZOMG A HAS MANY RELATIONSHIP')
9
+ end
10
+ else
11
+ has_many_relationships = [HasManyRelationship.new(id: 1, body: 'ZOMG A HAS MANY RELATIONSHIP')]
12
+ end
13
+ has_one_relationship = HasOneRelationship.new(id: 42, first_name: 'Joao', last_name: 'Moura')
14
+ PrimaryResource.new(id: 1337, title: 'New PrimaryResource', virtual_attribute: nil, body: 'Body', has_many_relationships: has_many_relationships, has_one_relationship: has_one_relationship)
15
+ end
16
+
17
+ def render_with_caching_serializer
18
+ toggle_cache_status
19
+ render json: PRIMARY_RESOURCE, serializer: CachingPrimaryResourceSerializer, adapter: :json, meta: { caching: perform_caching }
20
+ end
21
+
22
+ def render_with_fragment_caching_serializer
23
+ toggle_cache_status
24
+ render json: PRIMARY_RESOURCE, serializer: FragmentCachingPrimaryResourceSerializer, adapter: :json, meta: { caching: perform_caching }
25
+ end
26
+
27
+ def render_with_non_caching_serializer
28
+ toggle_cache_status
29
+ render json: PRIMARY_RESOURCE, adapter: :json, meta: { caching: perform_caching }
30
+ end
31
+
32
+ def render_cache_status
33
+ toggle_cache_status
34
+ # Uncomment to debug
35
+ # STDERR.puts cache_store.class
36
+ # STDERR.puts cache_dependencies
37
+ # ActiveSupport::Cache::Store.logger.debug [ActiveModelSerializers.config.cache_store, ActiveModelSerializers.config.perform_caching, CachingPrimaryResourceSerializer._cache, perform_caching, params].inspect
38
+ render json: { caching: perform_caching, meta: { cache_log: cache_messages, cache_status: cache_status } }.to_json
39
+ end
40
+
41
+ def clear
42
+ ActionController::Base.cache_store.clear
43
+ # Test caching is on
44
+ # Uncomment to turn on logger; possible performance issue
45
+ # logger = BenchmarkLogger.new
46
+ # ActiveSupport::Cache::Store.logger = logger # seems to be the best way
47
+ #
48
+ # the below is used in some rails tests but isn't available/working in all versions, so far as I can tell
49
+ # https://github.com/rails/rails/pull/15943
50
+ # ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
51
+ # logger.debug ActiveSupport::Notifications::Event.new(*args)
52
+ # end
53
+ render json: 'ok'.to_json
54
+ end
55
+
56
+ private
57
+
58
+ def cache_status
59
+ {
60
+ controller: perform_caching,
61
+ app: Rails.configuration.action_controller.perform_caching,
62
+ serializers: Rails.configuration.serializers.each_with_object({}) { |serializer, data| data[serializer.name] = serializer._cache.present? }
63
+ }
64
+ end
65
+
66
+ def cache_messages
67
+ ActiveSupport::Cache::Store.logger.is_a?(BenchmarkLogger) && ActiveSupport::Cache::Store.logger.messages.split("\n")
68
+ end
69
+
70
+ def toggle_cache_status
71
+ case params[:on]
72
+ when 'on'.freeze then self.perform_caching = true
73
+ when 'off'.freeze then self.perform_caching = false
74
+ else nil # no-op
75
+ end
76
+ end
77
+ end
78
+
79
+ Rails.application.routes.draw do
80
+ get '/status(/:on)' => 'primary_resource#render_cache_status'
81
+ get '/clear' => 'primary_resource#clear'
82
+ get '/caching(/:on)' => 'primary_resource#render_with_caching_serializer'
83
+ get '/fragment_caching(/:on)' => 'primary_resource#render_with_fragment_caching_serializer'
84
+ get '/non_caching(/:on)' => 'primary_resource#render_with_non_caching_serializer'
85
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.configuration.serializers = []
4
+ class HasOneRelationshipSerializer < ActiveModel::Serializer
5
+ attributes :id, :first_name, :last_name
6
+
7
+ has_many :primary_resources, embed: :ids
8
+ has_one :bio
9
+ end
10
+ Rails.configuration.serializers << HasOneRelationshipSerializer
11
+
12
+ class VirtualAttributeSerializer < ActiveModel::Serializer
13
+ attributes :id, :name
14
+ end
15
+ Rails.configuration.serializers << VirtualAttributeSerializer
16
+
17
+ class HasManyRelationshipSerializer < ActiveModel::Serializer
18
+ attributes :id, :body
19
+
20
+ belongs_to :primary_resource
21
+ belongs_to :has_one_relationship
22
+ end
23
+ Rails.configuration.serializers << HasManyRelationshipSerializer
24
+
25
+ class PrimaryResourceSerializer < ActiveModel::Serializer
26
+ attributes :id, :title, :body
27
+
28
+ has_many :has_many_relationships, serializer: HasManyRelationshipSerializer
29
+ belongs_to :virtual_attribute, serializer: VirtualAttributeSerializer
30
+ belongs_to :has_one_relationship, serializer: HasOneRelationshipSerializer
31
+
32
+ link(:primary_resource_has_one_relationships) { 'https://example.com/primary_resource_has_one_relationships' }
33
+
34
+ meta do
35
+ {
36
+ rating: 5,
37
+ favorite_count: 10
38
+ }
39
+ end
40
+
41
+ def virtual_attribute
42
+ VirtualAttribute.new(id: 999, name: 'Free-Range Virtual Attribute')
43
+ end
44
+ end
45
+ Rails.configuration.serializers << PrimaryResourceSerializer
46
+
47
+ class CachingHasOneRelationshipSerializer < HasOneRelationshipSerializer
48
+ cache key: 'writer', skip_digest: true
49
+ end
50
+ Rails.configuration.serializers << CachingHasOneRelationshipSerializer
51
+
52
+ class CachingHasManyRelationshipSerializer < HasManyRelationshipSerializer
53
+ cache expires_in: 1.day, skip_digest: true
54
+ end
55
+ Rails.configuration.serializers << CachingHasManyRelationshipSerializer
56
+
57
+ # see https://github.com/rails-api/active_model_serializers/pull/1690/commits/68715b8f99bc29677e8a47bb3f305f23c077024b#r60344532
58
+ class CachingPrimaryResourceSerializer < ActiveModel::Serializer
59
+ cache key: 'primary_resource', expires_in: 0.1, skip_digest: true
60
+
61
+ attributes :id, :title, :body
62
+
63
+ belongs_to :virtual_attribute, serializer: VirtualAttributeSerializer
64
+ belongs_to :has_one_relationship, serializer: CachingHasOneRelationshipSerializer
65
+ has_many :has_many_relationships, serializer: CachingHasManyRelationshipSerializer
66
+
67
+ link(:primary_resource_has_one_relationships) { 'https://example.com/primary_resource_has_one_relationships' }
68
+
69
+ meta do
70
+ {
71
+ rating: 5,
72
+ favorite_count: 10
73
+ }
74
+ end
75
+
76
+ def virtual_attribute
77
+ VirtualAttribute.new(id: 999, name: 'Free-Range Virtual Attribute')
78
+ end
79
+ end
80
+ Rails.configuration.serializers << CachingPrimaryResourceSerializer
81
+
82
+ class FragmentCachingHasOneRelationshipSerializer < HasOneRelationshipSerializer
83
+ cache key: 'writer', only: [:first_name, :last_name], skip_digest: true
84
+ end
85
+ Rails.configuration.serializers << FragmentCachingHasOneRelationshipSerializer
86
+
87
+ class FragmentCachingHasManyRelationshipSerializer < HasManyRelationshipSerializer
88
+ cache expires_in: 1.day, except: [:body], skip_digest: true
89
+ end
90
+ Rails.configuration.serializers << CachingHasManyRelationshipSerializer
91
+
92
+ # see https://github.com/rails-api/active_model_serializers/pull/1690/commits/68715b8f99bc29677e8a47bb3f305f23c077024b#r60344532
93
+ class FragmentCachingPrimaryResourceSerializer < ActiveModel::Serializer
94
+ cache key: 'primary_resource', expires_in: 0.1, skip_digest: true
95
+
96
+ attributes :id, :title, :body
97
+
98
+ belongs_to :virtual_attribute, serializer: VirtualAttributeSerializer
99
+ belongs_to :has_one_relationship, serializer: FragmentCachingHasOneRelationshipSerializer
100
+ has_many :has_many_relationships, serializer: FragmentCachingHasManyRelationshipSerializer
101
+
102
+ link(:primary_resource_has_one_relationships) { 'https://example.com/primary_resource_has_one_relationships' }
103
+
104
+ meta do
105
+ {
106
+ rating: 5,
107
+ favorite_count: 10
108
+ }
109
+ end
110
+
111
+ def virtual_attribute
112
+ VirtualAttribute.new(id: 999, name: 'Free-Range Virtual Attribute')
113
+ end
114
+ end
115
+ Rails.configuration.serializers << FragmentCachingPrimaryResourceSerializer
116
+
117
+ if ENV['ENABLE_ACTIVE_RECORD'] == 'true'
118
+ require 'active_record'
119
+
120
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
121
+ ActiveRecord::Schema.define do
122
+ self.verbose = false
123
+
124
+ create_table :virtual_attributes, force: true do |t|
125
+ t.string :name
126
+ t.timestamps null: false
127
+ end
128
+ create_table :has_one_relationships, force: true do |t|
129
+ t.string :first_name
130
+ t.string :last_name
131
+ t.timestamps null: false
132
+ end
133
+ create_table :primary_resources, force: true do |t|
134
+ t.string :title
135
+ t.text :body
136
+ t.references :has_one_relationship
137
+ t.references :virtual_attribute
138
+ t.timestamps null: false
139
+ end
140
+ create_table :has_many_relationships, force: true do |t|
141
+ t.text :body
142
+ t.references :has_one_relationship
143
+ t.references :primary_resource
144
+ t.timestamps null: false
145
+ end
146
+ end
147
+
148
+ class HasManyRelationship < ActiveRecord::Base
149
+ belongs_to :has_one_relationship
150
+ belongs_to :primary_resource
151
+ end
152
+
153
+ class HasOneRelationship < ActiveRecord::Base
154
+ has_many :primary_resources
155
+ has_many :has_many_relationships
156
+ end
157
+
158
+ class PrimaryResource < ActiveRecord::Base
159
+ has_many :has_many_relationships
160
+ belongs_to :has_one_relationship
161
+ belongs_to :virtual_attribute
162
+ end
163
+
164
+ class VirtualAttribute < ActiveRecord::Base
165
+ has_many :primary_resources
166
+ end
167
+ else
168
+ # ActiveModelSerializers::Model is a convenient
169
+ # serializable class to inherit from when making
170
+ # serializable non-activerecord objects.
171
+ class BenchmarkModel
172
+ include ActiveModel::Model
173
+ include ActiveModel::Serializers::JSON
174
+
175
+ attr_reader :attributes
176
+
177
+ def initialize(attributes = {})
178
+ @attributes = attributes
179
+ super
180
+ end
181
+
182
+ # Defaults to the downcased model name.
183
+ def id
184
+ attributes.fetch(:id) { self.class.name.downcase }
185
+ end
186
+
187
+ # Defaults to the downcased model name and updated_at
188
+ def cache_key
189
+ attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}" }
190
+ end
191
+
192
+ # Defaults to the time the serializer file was modified.
193
+ def updated_at
194
+ @updated_at ||= attributes.fetch(:updated_at) { File.mtime(__FILE__) }
195
+ end
196
+
197
+ def read_attribute_for_serialization(key)
198
+ if key == :id || key == 'id'
199
+ attributes.fetch(key) { id }
200
+ else
201
+ attributes[key]
202
+ end
203
+ end
204
+ end
205
+
206
+ class HasManyRelationship < BenchmarkModel
207
+ attr_accessor :id, :body
208
+ end
209
+
210
+ class HasOneRelationship < BenchmarkModel
211
+ attr_accessor :id, :first_name, :last_name, :primary_resources
212
+ end
213
+
214
+ class PrimaryResource < BenchmarkModel
215
+ attr_accessor :id, :title, :body, :has_many_relationships, :virtual_attribute, :has_one_relationship
216
+ end
217
+
218
+ class VirtualAttribute < BenchmarkModel
219
+ attr_accessor :id, :name
220
+ end
221
+ end
@@ -0,0 +1,717 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'tmpdir'
5
+ require 'tempfile'
6
+
7
+ module ActiveModelSerializers
8
+ class CacheTest < ActiveSupport::TestCase
9
+ class Article < ::Model
10
+ attributes :title
11
+ # To confirm error is raised when cache_key is not set and cache_key option not passed to cache
12
+ undef_method :cache_key
13
+ end
14
+ class ArticleSerializer < ActiveModel::Serializer
15
+ cache only: [:place], skip_digest: true
16
+ attributes :title
17
+ end
18
+
19
+ class Author < ::Model
20
+ attributes :id, :name
21
+ associations :posts, :bio, :roles
22
+ end
23
+ # Instead of a primitive cache key (i.e. a string), this class
24
+ # returns a list of objects that require to be expanded themselves.
25
+ class AuthorWithExpandableCacheElements < Author
26
+ # For the test purposes it's important that #to_s for HasCacheKey differs
27
+ # between instances, hence not a Struct.
28
+ class HasCacheKey
29
+ attr_reader :cache_key
30
+ def initialize(cache_key)
31
+ @cache_key = cache_key
32
+ end
33
+
34
+ def to_s
35
+ "HasCacheKey##{object_id}"
36
+ end
37
+ end
38
+
39
+ def cache_key
40
+ [
41
+ HasCacheKey.new(name),
42
+ HasCacheKey.new(id)
43
+ ]
44
+ end
45
+ end
46
+ class UncachedAuthor < Author
47
+ # To confirm cache_key is set using updated_at and cache_key option passed to cache
48
+ undef_method :cache_key
49
+ end
50
+ class AuthorSerializer < ActiveModel::Serializer
51
+ cache key: 'writer', skip_digest: true
52
+ attributes :id, :name
53
+
54
+ has_many :posts
55
+ has_many :roles
56
+ has_one :bio
57
+ end
58
+ class AuthorSerializerWithCache < ActiveModel::Serializer
59
+ cache
60
+
61
+ attributes :name
62
+ end
63
+
64
+ class Blog < ::Model
65
+ attributes :name
66
+ associations :writer
67
+ end
68
+ class BlogSerializer < ActiveModel::Serializer
69
+ cache key: 'blog'
70
+ attributes :id, :name
71
+
72
+ belongs_to :writer
73
+ end
74
+
75
+ class Comment < ::Model
76
+ attributes :id, :body
77
+ associations :post, :author
78
+
79
+ # Uses a custom non-time-based cache key
80
+ def cache_key
81
+ "comment/#{id}"
82
+ end
83
+ end
84
+ class CommentSerializer < ActiveModel::Serializer
85
+ cache expires_in: 1.day, skip_digest: true
86
+ attributes :id, :body
87
+ belongs_to :post
88
+ belongs_to :author
89
+ end
90
+
91
+ class Post < ::Model
92
+ attributes :id, :title, :body
93
+ associations :author, :comments, :blog
94
+ end
95
+ class PostSerializer < ActiveModel::Serializer
96
+ cache key: 'post', expires_in: 0.1, skip_digest: true
97
+ attributes :id, :title, :body
98
+
99
+ has_many :comments
100
+ belongs_to :blog
101
+ belongs_to :author
102
+ end
103
+
104
+ class Role < ::Model
105
+ attributes :name, :description, :special_attribute
106
+ associations :author
107
+ end
108
+ class RoleSerializer < ActiveModel::Serializer
109
+ cache only: [:name, :slug], skip_digest: true
110
+ attributes :id, :name, :description
111
+ attribute :friendly_id, key: :slug
112
+ belongs_to :author
113
+
114
+ def friendly_id
115
+ "#{object.name}-#{object.id}"
116
+ end
117
+ end
118
+ class InheritedRoleSerializer < RoleSerializer
119
+ cache key: 'inherited_role', only: [:name, :special_attribute]
120
+ attribute :special_attribute
121
+ end
122
+
123
+ setup do
124
+ cache_store.clear
125
+ @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
126
+ @post = Post.new(id: 'post', title: 'New Post', body: 'Body')
127
+ @bio = Bio.new(id: 1, content: 'AMS Contributor')
128
+ @author = Author.new(id: 'author', name: 'Joao M. D. Moura')
129
+ @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author)
130
+ @role = Role.new(name: 'Great Author')
131
+ @location = Location.new(lat: '-23.550520', lng: '-46.633309')
132
+ @place = Place.new(name: 'Amazing Place')
133
+ @author.posts = [@post]
134
+ @author.roles = [@role]
135
+ @role.author = @author
136
+ @author.bio = @bio
137
+ @bio.author = @author
138
+ @post.comments = [@comment]
139
+ @post.author = @author
140
+ @comment.post = @post
141
+ @comment.author = @author
142
+ @post.blog = @blog
143
+ @location.place = @place
144
+
145
+ @location_serializer = LocationSerializer.new(@location)
146
+ @bio_serializer = BioSerializer.new(@bio)
147
+ @role_serializer = RoleSerializer.new(@role)
148
+ @post_serializer = PostSerializer.new(@post)
149
+ @author_serializer = AuthorSerializer.new(@author)
150
+ @comment_serializer = CommentSerializer.new(@comment)
151
+ @blog_serializer = BlogSerializer.new(@blog)
152
+ end
153
+
154
+ def test_expiring_of_cache_at_update_of_record
155
+ original_cache_versioning = :none
156
+
157
+ if ARModels::Author.respond_to?(:cache_versioning)
158
+ original_cache_versioning = ARModels::Author.cache_versioning
159
+ ARModels::Author.cache_versioning = true
160
+ end
161
+
162
+ author = ARModels::Author.create(name: 'Foo')
163
+ author_json = AuthorSerializerWithCache.new(author).as_json
164
+
165
+ assert_equal 'Foo', author_json[:name]
166
+
167
+ author.update_attributes(name: 'Bar')
168
+ author_json = AuthorSerializerWithCache.new(author).as_json
169
+
170
+ expected = 'Bar'
171
+ actual = author_json[:name]
172
+ if ENV['APPVEYOR'] && actual != expected
173
+ skip('Cache expiration tests sometimes fail on Appveyor. FIXME :)')
174
+ else
175
+ assert_equal expected, actual
176
+ end
177
+ ensure
178
+ ARModels::Author.cache_versioning = original_cache_versioning unless original_cache_versioning == :none
179
+ end
180
+
181
+ def test_cache_expiration_in_collection_on_update_of_record
182
+ original_cache_versioning = :none
183
+
184
+ if ARModels::Author.respond_to?(:cache_versioning)
185
+ original_cache_versioning = ARModels::Author.cache_versioning
186
+ ARModels::Author.cache_versioning = true
187
+ end
188
+
189
+ foo = 'Foo'
190
+ foo2 = 'Foo2'
191
+ author = ARModels::Author.create(name: foo)
192
+ author2 = ARModels::Author.create(name: foo2)
193
+ author_collection = [author, author, author2]
194
+
195
+ collection_json = render_object_with_cache(author_collection, each_serializer: AuthorSerializerWithCache)
196
+ actual = collection_json
197
+ expected = [{ name: foo }, { name: foo }, { name: foo2 }]
198
+ if ENV['APPVEYOR'] && actual != expected
199
+ skip('Cache expiration tests sometimes fail on Appveyor. FIXME :)')
200
+ else
201
+ assert_equal expected, actual
202
+ end
203
+
204
+ bar = 'Bar'
205
+ author.update!(name: bar)
206
+
207
+ collection_json = render_object_with_cache(author_collection, each_serializer: AuthorSerializerWithCache)
208
+ assert_equal [{ name: bar }, { name: bar }, { name: foo2 }], collection_json
209
+ ensure
210
+ ARModels::Author.cache_versioning = original_cache_versioning unless original_cache_versioning == :none
211
+ end
212
+
213
+ def test_explicit_cache_store
214
+ default_store = Class.new(ActiveModel::Serializer) do
215
+ cache
216
+ end
217
+ explicit_store = Class.new(ActiveModel::Serializer) do
218
+ cache cache_store: ActiveSupport::Cache::FileStore
219
+ end
220
+
221
+ assert ActiveSupport::Cache::MemoryStore, ActiveModelSerializers.config.cache_store
222
+ assert ActiveSupport::Cache::MemoryStore, default_store.cache_store
223
+ assert ActiveSupport::Cache::FileStore, explicit_store.cache_store
224
+ end
225
+
226
+ def test_inherited_cache_configuration
227
+ inherited_serializer = Class.new(PostSerializer)
228
+
229
+ assert_equal PostSerializer._cache_key, inherited_serializer._cache_key
230
+ assert_equal PostSerializer._cache_options, inherited_serializer._cache_options
231
+ end
232
+
233
+ def test_override_cache_configuration
234
+ inherited_serializer = Class.new(PostSerializer) do
235
+ cache key: 'new-key'
236
+ end
237
+
238
+ assert_equal PostSerializer._cache_key, 'post'
239
+ assert_equal inherited_serializer._cache_key, 'new-key'
240
+ end
241
+
242
+ def test_cache_definition
243
+ assert_equal(cache_store, @post_serializer.class._cache)
244
+ assert_equal(cache_store, @author_serializer.class._cache)
245
+ assert_equal(cache_store, @comment_serializer.class._cache)
246
+ end
247
+
248
+ def test_cache_key_definition
249
+ assert_equal('post', @post_serializer.class._cache_key)
250
+ assert_equal('writer', @author_serializer.class._cache_key)
251
+ assert_nil(@comment_serializer.class._cache_key)
252
+ end
253
+
254
+ def test_cache_key_interpolation_with_updated_at_when_cache_key_is_not_defined_on_object
255
+ uncached_author = UncachedAuthor.new(name: 'Joao M. D. Moura')
256
+ uncached_author_serializer = AuthorSerializer.new(uncached_author)
257
+
258
+ render_object_with_cache(uncached_author)
259
+ key = "#{uncached_author_serializer.class._cache_key}/#{uncached_author_serializer.object.id}-#{uncached_author_serializer.object.updated_at.strftime('%Y%m%d%H%M%S%9N')}"
260
+ key = "#{key}/#{adapter.cache_key}"
261
+ assert_equal(uncached_author_serializer.attributes.to_json, cache_store.fetch(key).to_json)
262
+ end
263
+
264
+ def test_cache_key_expansion
265
+ author = AuthorWithExpandableCacheElements.new(id: 10, name: 'hello')
266
+ same_author = AuthorWithExpandableCacheElements.new(id: 10, name: 'hello')
267
+ diff_author = AuthorWithExpandableCacheElements.new(id: 11, name: 'hello')
268
+
269
+ author_serializer = AuthorSerializer.new(author)
270
+ same_author_serializer = AuthorSerializer.new(same_author)
271
+ diff_author_serializer = AuthorSerializer.new(diff_author)
272
+ adapter = AuthorSerializer.serialization_adapter_instance
273
+
274
+ assert_equal(author_serializer.cache_key(adapter), same_author_serializer.cache_key(adapter))
275
+ refute_equal(author_serializer.cache_key(adapter), diff_author_serializer.cache_key(adapter))
276
+ end
277
+
278
+ def test_default_cache_key_fallback
279
+ render_object_with_cache(@comment)
280
+ key = "#{@comment.cache_key}/#{adapter.cache_key}"
281
+ assert_equal(@comment_serializer.attributes.to_json, cache_store.fetch(key).to_json)
282
+ end
283
+
284
+ def test_error_is_raised_if_cache_key_is_not_defined_on_object_or_passed_as_cache_option
285
+ article = Article.new(title: 'Must Read')
286
+ e = assert_raises ActiveModel::Serializer::UndefinedCacheKey do
287
+ render_object_with_cache(article)
288
+ end
289
+ assert_match(/ActiveModelSerializers::CacheTest::Article must define #cache_key, or the 'key:' option must be passed into 'ActiveModelSerializers::CacheTest::ArticleSerializer.cache'/, e.message)
290
+ end
291
+
292
+ def test_cache_options_definition
293
+ assert_equal({ expires_in: 0.1, skip_digest: true }, @post_serializer.class._cache_options)
294
+ assert_nil(@blog_serializer.class._cache_options)
295
+ assert_equal({ expires_in: 1.day, skip_digest: true }, @comment_serializer.class._cache_options)
296
+ end
297
+
298
+ def test_fragment_cache_definition
299
+ assert_equal([:name, :slug], @role_serializer.class._cache_only)
300
+ assert_equal([:content], @bio_serializer.class._cache_except)
301
+ end
302
+
303
+ def test_associations_separately_cache
304
+ cache_store.clear
305
+ assert_nil(cache_store.fetch(@post.cache_key))
306
+ assert_nil(cache_store.fetch(@comment.cache_key))
307
+
308
+ Timecop.freeze(Time.current) do
309
+ render_object_with_cache(@post)
310
+
311
+ key = "#{@post.cache_key}/#{adapter.cache_key}"
312
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
313
+ key = "#{@comment.cache_key}/#{adapter.cache_key}"
314
+ assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
315
+ end
316
+ end
317
+
318
+ def test_associations_cache_when_updated
319
+ Timecop.freeze(Time.current) do
320
+ # Generate a new Cache of Post object and each objects related to it.
321
+ render_object_with_cache(@post)
322
+
323
+ # Check if it cached the objects separately
324
+ key = "#{@post.cache_key}/#{adapter.cache_key}"
325
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
326
+ key = "#{@comment.cache_key}/#{adapter.cache_key}"
327
+ assert_equal(@comment_serializer.attributes, cache_store.fetch(key))
328
+
329
+ # Simulating update on comments relationship with Post
330
+ new_comment = Comment.new(id: 2567, body: 'ZOMG A NEW COMMENT')
331
+ new_comment_serializer = CommentSerializer.new(new_comment)
332
+ @post.comments = [new_comment]
333
+
334
+ # Ask for the serialized object
335
+ render_object_with_cache(@post)
336
+
337
+ # Check if the the new comment was cached
338
+ key = "#{new_comment.cache_key}/#{adapter.cache_key}"
339
+ assert_equal(new_comment_serializer.attributes, cache_store.fetch(key))
340
+ key = "#{@post.cache_key}/#{adapter.cache_key}"
341
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
342
+ end
343
+ end
344
+
345
+ def test_fragment_fetch_with_virtual_associations
346
+ expected_result = {
347
+ id: @location.id,
348
+ lat: @location.lat,
349
+ lng: @location.lng,
350
+ address: 'Nowhere'
351
+ }
352
+
353
+ hash = render_object_with_cache(@location)
354
+
355
+ assert_equal(hash, expected_result)
356
+ key = "#{@location.cache_key}/#{adapter.cache_key}"
357
+ assert_equal({ address: 'Nowhere' }, cache_store.fetch(key))
358
+ end
359
+
360
+ def test_fragment_cache_with_inheritance
361
+ inherited = render_object_with_cache(@role, serializer: InheritedRoleSerializer)
362
+ base = render_object_with_cache(@role)
363
+
364
+ assert_includes(inherited.keys, :special_attribute)
365
+ refute_includes(base.keys, :special_attribute)
366
+ end
367
+
368
+ def test_uses_adapter_in_cache_key
369
+ render_object_with_cache(@post)
370
+ key = "#{@post.cache_key}/#{adapter.class.to_s.demodulize.underscore}"
371
+ assert_equal(@post_serializer.attributes, cache_store.fetch(key))
372
+ end
373
+
374
+ # Based on original failing test by @kevintyll
375
+ # rubocop:disable Metrics/AbcSize
376
+ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attributes
377
+ Object.const_set(:Alert, Class.new(ActiveModelSerializers::Model) do
378
+ attributes :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
379
+ end)
380
+ Object.const_set(:UncachedAlertSerializer, Class.new(ActiveModel::Serializer) do
381
+ attributes :id, :status, :resource, :started_at, :ended_at, :updated_at, :created_at
382
+ end)
383
+ Object.const_set(:AlertSerializer, Class.new(UncachedAlertSerializer) do
384
+ cache
385
+ end)
386
+
387
+ alert = Alert.new(
388
+ id: 1,
389
+ status: 'fail',
390
+ resource: 'resource-1',
391
+ started_at: Time.new(2016, 3, 31, 21, 36, 35, 0),
392
+ ended_at: nil,
393
+ updated_at: Time.new(2016, 3, 31, 21, 27, 35, 0),
394
+ created_at: Time.new(2016, 3, 31, 21, 37, 35, 0)
395
+ )
396
+
397
+ expected_fetch_attributes = {
398
+ id: 1,
399
+ status: 'fail',
400
+ resource: 'resource-1',
401
+ started_at: alert.started_at,
402
+ ended_at: nil,
403
+ updated_at: alert.updated_at,
404
+ created_at: alert.created_at
405
+ }.with_indifferent_access
406
+ expected_cached_jsonapi_attributes = {
407
+ id: '1',
408
+ type: 'alerts',
409
+ attributes: {
410
+ status: 'fail',
411
+ resource: 'resource-1',
412
+ started_at: alert.started_at,
413
+ ended_at: nil,
414
+ updated_at: alert.updated_at,
415
+ created_at: alert.created_at
416
+ }
417
+ }.with_indifferent_access
418
+
419
+ # Assert attributes are serialized correctly
420
+ serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :attributes)
421
+ attributes_serialization = serializable_alert.as_json.with_indifferent_access
422
+ assert_equal expected_fetch_attributes, alert.attributes
423
+ assert_equal alert.attributes, attributes_serialization
424
+ attributes_cache_key = serializable_alert.adapter.serializer.cache_key(serializable_alert.adapter)
425
+ assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key).with_indifferent_access
426
+
427
+ serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :json_api)
428
+ jsonapi_cache_key = serializable_alert.adapter.serializer.cache_key(serializable_alert.adapter)
429
+ # Assert cache keys differ
430
+ refute_equal attributes_cache_key, jsonapi_cache_key
431
+ # Assert (cached) serializations differ
432
+ jsonapi_serialization = serializable_alert.as_json
433
+ assert_equal alert.status, jsonapi_serialization.fetch(:data).fetch(:attributes).fetch(:status)
434
+ serializable_alert = serializable(alert, serializer: UncachedAlertSerializer, adapter: :json_api)
435
+ assert_equal serializable_alert.as_json, jsonapi_serialization
436
+
437
+ cached_serialization = cache_store.fetch(jsonapi_cache_key).with_indifferent_access
438
+ assert_equal expected_cached_jsonapi_attributes, cached_serialization
439
+ ensure
440
+ Object.send(:remove_const, :Alert)
441
+ Object.send(:remove_const, :AlertSerializer)
442
+ Object.send(:remove_const, :UncachedAlertSerializer)
443
+ end
444
+ # rubocop:enable Metrics/AbcSize
445
+
446
+ def test_uses_file_digest_in_cache_key
447
+ render_object_with_cache(@blog)
448
+ file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read)
449
+ key = "#{@blog.cache_key}/#{adapter.cache_key}/#{file_digest}"
450
+ assert_equal(@blog_serializer.attributes, cache_store.fetch(key))
451
+ end
452
+
453
+ def test_cache_digest_definition
454
+ file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read)
455
+ assert_equal(file_digest, @post_serializer.class._cache_digest)
456
+ end
457
+
458
+ def test_object_cache_keys
459
+ serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment])
460
+ include_directive = JSONAPI::IncludeDirective.new('*', allow_wildcard: true)
461
+
462
+ actual = ActiveModel::Serializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_directive)
463
+
464
+ assert_equal 3, actual.size
465
+ expected_key = "comment/1/#{serializable.adapter.cache_key}"
466
+ assert actual.any? { |key| key == expected_key }, "actual '#{actual}' should include #{expected_key}"
467
+ expected_key = %r{post/post-\d+}
468
+ assert actual.any? { |key| key =~ expected_key }, "actual '#{actual}' should match '#{expected_key}'"
469
+ expected_key = %r{author/author-\d+}
470
+ assert actual.any? { |key| key =~ expected_key }, "actual '#{actual}' should match '#{expected_key}'"
471
+ end
472
+
473
+ # rubocop:disable Metrics/AbcSize
474
+ def test_fetch_attributes_from_cache
475
+ serializers = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
476
+
477
+ Timecop.freeze(Time.current) do
478
+ render_object_with_cache(@comment)
479
+
480
+ options = {}
481
+ adapter_options = {}
482
+ adapter_instance = ActiveModelSerializers::Adapter::Attributes.new(serializers, adapter_options)
483
+ serializers.serializable_hash(adapter_options, options, adapter_instance)
484
+ cached_attributes = options.fetch(:cached_attributes).with_indifferent_access
485
+
486
+ include_directive = ActiveModelSerializers.default_include_directive
487
+ manual_cached_attributes = ActiveModel::Serializer.cache_read_multi(serializers, adapter_instance, include_directive).with_indifferent_access
488
+ assert_equal manual_cached_attributes, cached_attributes
489
+
490
+ assert_equal cached_attributes["#{@comment.cache_key}/#{adapter_instance.cache_key}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
491
+ assert_equal cached_attributes["#{@comment.post.cache_key}/#{adapter_instance.cache_key}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
492
+
493
+ writer = @comment.post.blog.writer
494
+ writer_cache_key = writer.cache_key
495
+ assert_equal cached_attributes["#{writer_cache_key}/#{adapter_instance.cache_key}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
496
+ end
497
+ end
498
+ # rubocop:enable Metrics/AbcSize
499
+
500
+ def test_cache_read_multi_with_fragment_cache_enabled
501
+ post_serializer = Class.new(ActiveModel::Serializer) do
502
+ cache except: [:body]
503
+ end
504
+
505
+ serializers = ActiveModel::Serializer::CollectionSerializer.new([@post, @post], serializer: post_serializer)
506
+
507
+ Timecop.freeze(Time.current) do
508
+ # Warming up.
509
+ options = {}
510
+ adapter_options = {}
511
+ adapter_instance = ActiveModelSerializers::Adapter::Attributes.new(serializers, adapter_options)
512
+ serializers.serializable_hash(adapter_options, options, adapter_instance)
513
+
514
+ # Should find something with read_multi now
515
+ options = {}
516
+ serializers.serializable_hash(adapter_options, options, adapter_instance)
517
+ cached_attributes = options.fetch(:cached_attributes)
518
+
519
+ include_directive = ActiveModelSerializers.default_include_directive
520
+ manual_cached_attributes = ActiveModel::Serializer.cache_read_multi(serializers, adapter_instance, include_directive)
521
+
522
+ refute_equal 0, cached_attributes.size
523
+ refute_equal 0, manual_cached_attributes.size
524
+ assert_equal manual_cached_attributes, cached_attributes
525
+ end
526
+ end
527
+
528
+ def test_serializer_file_path_on_nix
529
+ path = '/Users/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
530
+ caller_line = "#{path}:1:in `<top (required)>'"
531
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
532
+ end
533
+
534
+ def test_serializer_file_path_on_windows
535
+ path = 'c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb'
536
+ caller_line = "#{path}:1:in `<top (required)>'"
537
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
538
+ end
539
+
540
+ def test_serializer_file_path_with_space
541
+ path = '/Users/git/ember js/ember-crm-backend/app/serializers/lead_serializer.rb'
542
+ caller_line = "#{path}:1:in `<top (required)>'"
543
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
544
+ end
545
+
546
+ def test_serializer_file_path_with_submatch
547
+ # The submatch in the path ensures we're using a correctly greedy regexp.
548
+ path = '/Users/git/ember js/ember:123:in x/app/serializers/lead_serializer.rb'
549
+ caller_line = "#{path}:1:in `<top (required)>'"
550
+ assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path
551
+ end
552
+
553
+ def test_digest_caller_file
554
+ contents = "puts 'AMS rocks'!"
555
+ dir = Dir.mktmpdir('space char')
556
+ file = Tempfile.new('some_ruby.rb', dir)
557
+ file.write(contents)
558
+ path = file.path
559
+ caller_line = "#{path}:1:in `<top (required)>'"
560
+ file.close
561
+ assert_equal ActiveModel::Serializer.digest_caller_file(caller_line), Digest::MD5.hexdigest(contents)
562
+ ensure
563
+ file.unlink
564
+ FileUtils.remove_entry dir
565
+ end
566
+
567
+ def test_warn_on_serializer_not_defined_in_file
568
+ called = false
569
+ serializer = Class.new(ActiveModel::Serializer)
570
+ assert_output(nil, /_cache_digest/) do
571
+ serializer.digest_caller_file('')
572
+ called = true
573
+ end
574
+ assert called
575
+ end
576
+
577
+ def test_cached_false_without_cache_store
578
+ cached_serializer = build_cached_serializer do |serializer|
579
+ serializer._cache = nil
580
+ end
581
+ refute cached_serializer.class.cache_enabled?
582
+ end
583
+
584
+ def test_cached_true_with_cache_store_and_without_cache_only_and_cache_except
585
+ cached_serializer = build_cached_serializer do |serializer|
586
+ serializer._cache = Object
587
+ end
588
+ assert cached_serializer.class.cache_enabled?
589
+ end
590
+
591
+ def test_cached_false_with_cache_store_and_with_cache_only
592
+ cached_serializer = build_cached_serializer do |serializer|
593
+ serializer._cache = Object
594
+ serializer._cache_only = [:name]
595
+ end
596
+ refute cached_serializer.class.cache_enabled?
597
+ end
598
+
599
+ def test_cached_false_with_cache_store_and_with_cache_except
600
+ cached_serializer = build_cached_serializer do |serializer|
601
+ serializer._cache = Object
602
+ serializer._cache_except = [:content]
603
+ end
604
+ refute cached_serializer.class.cache_enabled?
605
+ end
606
+
607
+ def test_fragment_cached_false_without_cache_store
608
+ cached_serializer = build_cached_serializer do |serializer|
609
+ serializer._cache = nil
610
+ serializer._cache_only = [:name]
611
+ end
612
+ refute cached_serializer.class.fragment_cache_enabled?
613
+ end
614
+
615
+ def test_fragment_cached_true_with_cache_store_and_cache_only
616
+ cached_serializer = build_cached_serializer do |serializer|
617
+ serializer._cache = Object
618
+ serializer._cache_only = [:name]
619
+ end
620
+ assert cached_serializer.class.fragment_cache_enabled?
621
+ end
622
+
623
+ def test_fragment_cached_true_with_cache_store_and_cache_except
624
+ cached_serializer = build_cached_serializer do |serializer|
625
+ serializer._cache = Object
626
+ serializer._cache_except = [:content]
627
+ end
628
+ assert cached_serializer.class.fragment_cache_enabled?
629
+ end
630
+
631
+ def test_fragment_cached_false_with_cache_store_and_cache_except_and_cache_only
632
+ cached_serializer = build_cached_serializer do |serializer|
633
+ serializer._cache = Object
634
+ serializer._cache_except = [:content]
635
+ serializer._cache_only = [:name]
636
+ end
637
+ refute cached_serializer.class.fragment_cache_enabled?
638
+ end
639
+
640
+ def test_fragment_fetch_with_virtual_attributes
641
+ author = Author.new(name: 'Joao M. D. Moura')
642
+ role = Role.new(name: 'Great Author', description: nil)
643
+ role.author = [author]
644
+ role_serializer = RoleSerializer.new(role)
645
+ adapter_instance = ActiveModelSerializers::Adapter.configured_adapter.new(role_serializer)
646
+ expected_result = {
647
+ id: role.id,
648
+ description: role.description,
649
+ slug: "#{role.name}-#{role.id}",
650
+ name: role.name
651
+ }
652
+ cache_store.clear
653
+
654
+ role_hash = role_serializer.fetch_attributes_fragment(adapter_instance)
655
+ assert_equal(role_hash, expected_result)
656
+
657
+ role.id = 'this has been updated'
658
+ role.name = 'this was cached'
659
+
660
+ role_hash = role_serializer.fetch_attributes_fragment(adapter_instance)
661
+ assert_equal(expected_result.merge(id: role.id), role_hash)
662
+ end
663
+
664
+ def test_fragment_fetch_with_except
665
+ adapter_instance = ActiveModelSerializers::Adapter.configured_adapter.new(@bio_serializer)
666
+ expected_result = {
667
+ id: @bio.id,
668
+ rating: nil,
669
+ content: @bio.content
670
+ }
671
+ cache_store.clear
672
+
673
+ bio_hash = @bio_serializer.fetch_attributes_fragment(adapter_instance)
674
+ assert_equal(expected_result, bio_hash)
675
+
676
+ @bio.content = 'this has been updated'
677
+ @bio.rating = 'this was cached'
678
+
679
+ bio_hash = @bio_serializer.fetch_attributes_fragment(adapter_instance)
680
+ assert_equal(expected_result.merge(content: @bio.content), bio_hash)
681
+ end
682
+
683
+ def test_fragment_fetch_with_namespaced_object
684
+ @spam = Spam::UnrelatedLink.new(id: 'spam-id-1')
685
+ @spam_serializer = Spam::UnrelatedLinkSerializer.new(@spam)
686
+ adapter_instance = ActiveModelSerializers::Adapter.configured_adapter.new(@spam_serializer)
687
+ @spam_hash = @spam_serializer.fetch_attributes_fragment(adapter_instance)
688
+ expected_result = {
689
+ id: @spam.id
690
+ }
691
+ assert_equal(@spam_hash, expected_result)
692
+ end
693
+
694
+ private
695
+
696
+ def cache_store
697
+ ActiveModelSerializers.config.cache_store
698
+ end
699
+
700
+ def build_cached_serializer
701
+ serializer = Class.new(ActiveModel::Serializer)
702
+ serializer._cache_key = nil
703
+ serializer._cache_options = nil
704
+ yield serializer if block_given?
705
+ serializer.new(Object)
706
+ end
707
+
708
+ def render_object_with_cache(obj, options = {})
709
+ @serializable_resource = serializable(obj, options)
710
+ @serializable_resource.serializable_hash
711
+ end
712
+
713
+ def adapter
714
+ @serializable_resource.adapter
715
+ end
716
+ end
717
+ end