active_model_serializers 0.9.0 → 0.10.12

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 (113) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +679 -9
  3. data/MIT-LICENSE +3 -2
  4. data/README.md +195 -753
  5. data/lib/action_controller/serialization.rb +45 -49
  6. data/lib/active_model/serializable_resource.rb +13 -0
  7. data/lib/active_model/serializer.rb +369 -212
  8. data/lib/active_model/serializer/adapter.rb +26 -0
  9. data/lib/active_model/serializer/adapter/attributes.rb +17 -0
  10. data/lib/active_model/serializer/adapter/base.rb +20 -0
  11. data/lib/active_model/serializer/adapter/json.rb +17 -0
  12. data/lib/active_model/serializer/adapter/json_api.rb +17 -0
  13. data/lib/active_model/serializer/adapter/null.rb +17 -0
  14. data/lib/active_model/serializer/array_serializer.rb +14 -0
  15. data/lib/active_model/serializer/association.rb +73 -0
  16. data/lib/active_model/serializer/attribute.rb +27 -0
  17. data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
  18. data/lib/active_model/serializer/collection_serializer.rb +90 -0
  19. data/lib/active_model/serializer/concerns/caching.rb +305 -0
  20. data/lib/active_model/serializer/error_serializer.rb +16 -0
  21. data/lib/active_model/serializer/errors_serializer.rb +34 -0
  22. data/lib/active_model/serializer/field.rb +92 -0
  23. data/lib/active_model/serializer/fieldset.rb +33 -0
  24. data/lib/active_model/serializer/has_many_reflection.rb +12 -0
  25. data/lib/active_model/serializer/has_one_reflection.rb +9 -0
  26. data/lib/active_model/serializer/lazy_association.rb +99 -0
  27. data/lib/active_model/serializer/link.rb +23 -0
  28. data/lib/active_model/serializer/lint.rb +152 -0
  29. data/lib/active_model/serializer/null.rb +19 -0
  30. data/lib/active_model/serializer/reflection.rb +212 -0
  31. data/lib/active_model/serializer/version.rb +3 -1
  32. data/lib/active_model_serializers.rb +60 -17
  33. data/lib/active_model_serializers/adapter.rb +100 -0
  34. data/lib/active_model_serializers/adapter/attributes.rb +36 -0
  35. data/lib/active_model_serializers/adapter/base.rb +85 -0
  36. data/lib/active_model_serializers/adapter/json.rb +23 -0
  37. data/lib/active_model_serializers/adapter/json_api.rb +535 -0
  38. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
  39. data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
  40. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
  41. data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
  42. data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
  43. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
  44. data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
  45. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
  46. data/lib/active_model_serializers/adapter/null.rb +11 -0
  47. data/lib/active_model_serializers/callbacks.rb +57 -0
  48. data/lib/active_model_serializers/deprecate.rb +56 -0
  49. data/lib/active_model_serializers/deserialization.rb +17 -0
  50. data/lib/active_model_serializers/json_pointer.rb +16 -0
  51. data/lib/active_model_serializers/logging.rb +124 -0
  52. data/lib/active_model_serializers/lookup_chain.rb +82 -0
  53. data/lib/active_model_serializers/model.rb +132 -0
  54. data/lib/active_model_serializers/model/caching.rb +25 -0
  55. data/lib/active_model_serializers/railtie.rb +52 -0
  56. data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
  57. data/lib/active_model_serializers/serializable_resource.rb +84 -0
  58. data/lib/active_model_serializers/serialization_context.rb +41 -0
  59. data/lib/active_model_serializers/test.rb +9 -0
  60. data/lib/active_model_serializers/test/schema.rb +140 -0
  61. data/lib/active_model_serializers/test/serializer.rb +127 -0
  62. data/lib/generators/rails/USAGE +6 -0
  63. data/lib/{active_model/serializer/generators → generators/rails}/resource_override.rb +3 -4
  64. data/lib/{active_model/serializer/generators/serializer → generators/rails}/serializer_generator.rb +6 -5
  65. data/lib/{active_model/serializer/generators/serializer/templates/serializer.rb → generators/rails/templates/serializer.rb.erb} +0 -0
  66. data/lib/grape/active_model_serializers.rb +18 -0
  67. data/lib/grape/formatters/active_model_serializers.rb +34 -0
  68. data/lib/grape/helpers/active_model_serializers.rb +19 -0
  69. data/lib/tasks/rubocop.rake +55 -0
  70. metadata +315 -99
  71. data/CONTRIBUTING.md +0 -20
  72. data/DESIGN.textile +0 -586
  73. data/lib/action_controller/serialization_test_case.rb +0 -79
  74. data/lib/active_model/array_serializer.rb +0 -65
  75. data/lib/active_model/default_serializer.rb +0 -32
  76. data/lib/active_model/serializable.rb +0 -40
  77. data/lib/active_model/serializer/associations.rb +0 -102
  78. data/lib/active_model/serializer/config.rb +0 -31
  79. data/lib/active_model/serializer/generators/serializer/USAGE +0 -9
  80. data/lib/active_model/serializer/generators/serializer/scaffold_controller_generator.rb +0 -14
  81. data/lib/active_model/serializer/generators/serializer/templates/controller.rb +0 -93
  82. data/lib/active_model/serializer/railtie.rb +0 -10
  83. data/lib/active_model/serializer_support.rb +0 -5
  84. data/test/fixtures/active_record.rb +0 -92
  85. data/test/fixtures/poro.rb +0 -75
  86. data/test/integration/action_controller/serialization_test.rb +0 -287
  87. data/test/integration/action_controller/serialization_test_case_test.rb +0 -61
  88. data/test/integration/active_record/active_record_test.rb +0 -77
  89. data/test/integration/generators/resource_generator_test.rb +0 -26
  90. data/test/integration/generators/scaffold_controller_generator_test.rb +0 -64
  91. data/test/integration/generators/serializer_generator_test.rb +0 -41
  92. data/test/test_app.rb +0 -11
  93. data/test/test_helper.rb +0 -24
  94. data/test/unit/active_model/array_serializer/except_test.rb +0 -18
  95. data/test/unit/active_model/array_serializer/key_format_test.rb +0 -18
  96. data/test/unit/active_model/array_serializer/meta_test.rb +0 -53
  97. data/test/unit/active_model/array_serializer/only_test.rb +0 -18
  98. data/test/unit/active_model/array_serializer/root_test.rb +0 -102
  99. data/test/unit/active_model/array_serializer/scope_test.rb +0 -24
  100. data/test/unit/active_model/array_serializer/serialization_test.rb +0 -199
  101. data/test/unit/active_model/default_serializer_test.rb +0 -13
  102. data/test/unit/active_model/serializer/associations/build_serializer_test.rb +0 -21
  103. data/test/unit/active_model/serializer/associations_test.rb +0 -19
  104. data/test/unit/active_model/serializer/attributes_test.rb +0 -41
  105. data/test/unit/active_model/serializer/config_test.rb +0 -88
  106. data/test/unit/active_model/serializer/filter_test.rb +0 -69
  107. data/test/unit/active_model/serializer/has_many_test.rb +0 -230
  108. data/test/unit/active_model/serializer/has_one_test.rb +0 -207
  109. data/test/unit/active_model/serializer/key_format_test.rb +0 -25
  110. data/test/unit/active_model/serializer/meta_test.rb +0 -39
  111. data/test/unit/active_model/serializer/options_test.rb +0 -15
  112. data/test/unit/active_model/serializer/root_test.rb +0 -117
  113. data/test/unit/active_model/serializer/scope_test.rb +0 -49
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model_serializers/adapter'
4
+ require 'active_model_serializers/deprecate'
5
+
6
+ module ActiveModel
7
+ class Serializer
8
+ # @deprecated Use ActiveModelSerializers::Adapter instead
9
+ module Adapter
10
+ class << self
11
+ extend ActiveModelSerializers::Deprecate
12
+
13
+ DEPRECATED_METHODS = [:create, :adapter_class, :adapter_map, :adapters, :register, :lookup].freeze
14
+ DEPRECATED_METHODS.each do |method|
15
+ delegate_and_deprecate method, ActiveModelSerializers::Adapter
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ require 'active_model/serializer/adapter/base'
23
+ require 'active_model/serializer/adapter/null'
24
+ require 'active_model/serializer/adapter/attributes'
25
+ require 'active_model/serializer/adapter/json'
26
+ require 'active_model/serializer/adapter/json_api'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ module Adapter
6
+ class Attributes < DelegateClass(ActiveModelSerializers::Adapter::Attributes)
7
+ def initialize(serializer, options = {})
8
+ super(ActiveModelSerializers::Adapter::Attributes.new(serializer, options))
9
+ end
10
+ class << self
11
+ extend ActiveModelSerializers::Deprecate
12
+ deprecate :new, 'ActiveModelSerializers::Adapter::Json.'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ module Adapter
6
+ class Base < DelegateClass(ActiveModelSerializers::Adapter::Base)
7
+ class << self
8
+ extend ActiveModelSerializers::Deprecate
9
+ deprecate :inherited, 'ActiveModelSerializers::Adapter::Base.'
10
+ end
11
+
12
+ # :nocov:
13
+ def initialize(serializer, options = {})
14
+ super(ActiveModelSerializers::Adapter::Base.new(serializer, options))
15
+ end
16
+ # :nocov:
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ module Adapter
6
+ class Json < DelegateClass(ActiveModelSerializers::Adapter::Json)
7
+ def initialize(serializer, options = {})
8
+ super(ActiveModelSerializers::Adapter::Json.new(serializer, options))
9
+ end
10
+ class << self
11
+ extend ActiveModelSerializers::Deprecate
12
+ deprecate :new, 'ActiveModelSerializers::Adapter::Json.new'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ module Adapter
6
+ class JsonApi < DelegateClass(ActiveModelSerializers::Adapter::JsonApi)
7
+ def initialize(serializer, options = {})
8
+ super(ActiveModelSerializers::Adapter::JsonApi.new(serializer, options))
9
+ end
10
+ class << self
11
+ extend ActiveModelSerializers::Deprecate
12
+ deprecate :new, 'ActiveModelSerializers::Adapter::JsonApi.new'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ module Adapter
6
+ class Null < DelegateClass(ActiveModelSerializers::Adapter::Null)
7
+ def initialize(serializer, options = {})
8
+ super(ActiveModelSerializers::Adapter::Null.new(serializer, options))
9
+ end
10
+ class << self
11
+ extend ActiveModelSerializers::Deprecate
12
+ deprecate :new, 'ActiveModelSerializers::Adapter::Null.new'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/serializer/collection_serializer'
4
+
5
+ module ActiveModel
6
+ class Serializer
7
+ class ArraySerializer < CollectionSerializer
8
+ class << self
9
+ extend ActiveModelSerializers::Deprecate
10
+ deprecate :new, 'ActiveModel::Serializer::CollectionSerializer.'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/serializer/lazy_association'
4
+
5
+ module ActiveModel
6
+ class Serializer
7
+ # This class holds all information about serializer's association.
8
+ #
9
+ # @api private
10
+ Association = Struct.new(:reflection, :association_options) do
11
+ attr_reader :lazy_association
12
+ delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
13
+
14
+ def initialize(*)
15
+ super
16
+ @lazy_association = LazyAssociation.new(reflection, association_options)
17
+ end
18
+
19
+ # @return [Symbol]
20
+ delegate :name, to: :reflection
21
+
22
+ # @return [Symbol]
23
+ def key
24
+ reflection_options.fetch(:key, name)
25
+ end
26
+
27
+ # @return [True,False]
28
+ def key?
29
+ reflection_options.key?(:key)
30
+ end
31
+
32
+ # @return [Hash]
33
+ def links
34
+ reflection_options.fetch(:links) || {}
35
+ end
36
+
37
+ # @return [Hash, nil]
38
+ # This gets mutated, so cannot use the cached reflection_options
39
+ def meta
40
+ reflection.options[:meta]
41
+ end
42
+
43
+ def belongs_to?
44
+ reflection.foreign_key_on == :self
45
+ end
46
+
47
+ def polymorphic?
48
+ true == reflection_options[:polymorphic]
49
+ end
50
+
51
+ # @api private
52
+ def serializable_hash(adapter_options, adapter_instance)
53
+ association_serializer = lazy_association.serializer
54
+ return virtual_value if virtual_value
55
+ association_object = association_serializer && association_serializer.object
56
+ return unless association_object
57
+
58
+ serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
59
+
60
+ if polymorphic? && serialization
61
+ polymorphic_type = association_object.class.name.underscore
62
+ serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization }
63
+ end
64
+
65
+ serialization
66
+ end
67
+
68
+ private
69
+
70
+ delegate :reflection_options, to: :lazy_association
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model/serializer/field'
4
+
5
+ module ActiveModel
6
+ class Serializer
7
+ # Holds all the meta-data about an attribute as it was specified in the
8
+ # ActiveModel::Serializer class.
9
+ #
10
+ # @example
11
+ # class PostSerializer < ActiveModel::Serializer
12
+ # attribute :content
13
+ # attribute :name, key: :title
14
+ # attribute :email, key: :author_email, if: :user_logged_in?
15
+ # attribute :preview do
16
+ # truncate(object.content)
17
+ # end
18
+ #
19
+ # def user_logged_in?
20
+ # current_user.logged_in?
21
+ # end
22
+ # end
23
+ #
24
+ class Attribute < Field
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ # @api private
6
+ class BelongsToReflection < Reflection
7
+ # @api private
8
+ def foreign_key_on
9
+ :self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ class CollectionSerializer
6
+ include Enumerable
7
+ delegate :each, to: :@serializers
8
+
9
+ attr_reader :object, :root
10
+
11
+ def initialize(resources, options = {})
12
+ @object = resources
13
+ @options = options
14
+ @root = options[:root]
15
+ @serializers = serializers_from_resources
16
+ end
17
+
18
+ def success?
19
+ true
20
+ end
21
+
22
+ # @api private
23
+ def serializable_hash(adapter_options, options, adapter_instance)
24
+ options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
25
+ options[:cached_attributes] ||= ActiveModel::Serializer.cache_read_multi(self, adapter_instance, options[:include_directive])
26
+ serializers.map do |serializer|
27
+ serializer.serializable_hash(adapter_options, options, adapter_instance)
28
+ end
29
+ end
30
+
31
+ # TODO: unify naming of root, json_key, and _type. Right now, a serializer's
32
+ # json_key comes from the root option or the object's model name, by default.
33
+ # But, if a dev defines a custom `json_key` method with an explicit value,
34
+ # we have no simple way to know that it is safe to call that instance method.
35
+ # (which is really a class property at this point, anyhow).
36
+ # rubocop:disable Metrics/CyclomaticComplexity
37
+ # Disabling cop since it's good to highlight the complexity of this method by
38
+ # including all the logic right here.
39
+ def json_key
40
+ return root if root
41
+ # 1. get from options[:serializer] for empty resource collection
42
+ key = object.empty? &&
43
+ (explicit_serializer_class = options[:serializer]) &&
44
+ explicit_serializer_class._type
45
+ # 2. get from first serializer instance in collection
46
+ key ||= (serializer = serializers.first) && serializer.json_key
47
+ # 3. get from collection name, if a named collection
48
+ key ||= object.respond_to?(:name) ? object.name && object.name.underscore : nil
49
+ # 4. key may be nil for empty collection and no serializer option
50
+ key &&= key.pluralize
51
+ # 5. fail if the key cannot be determined
52
+ key || fail(ArgumentError, 'Cannot infer root key from collection type. Please specify the root or each_serializer option, or render a JSON String')
53
+ end
54
+ # rubocop:enable Metrics/CyclomaticComplexity
55
+
56
+ def paginated?
57
+ ActiveModelSerializers.config.jsonapi_pagination_links_enabled &&
58
+ object.respond_to?(:current_page) &&
59
+ object.respond_to?(:total_pages) &&
60
+ object.respond_to?(:size)
61
+ end
62
+
63
+ protected
64
+
65
+ attr_reader :serializers, :options
66
+
67
+ private
68
+
69
+ def serializers_from_resources
70
+ serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
71
+ object.map do |resource|
72
+ serializer_from_resource(resource, serializer_context_class, options)
73
+ end
74
+ end
75
+
76
+ def serializer_from_resource(resource, serializer_context_class, options)
77
+ serializer_class = options.fetch(:serializer) do
78
+ serializer_context_class.serializer_for(resource, namespace: options[:namespace])
79
+ end
80
+
81
+ if serializer_class.nil?
82
+ ActiveModelSerializers.logger.debug "No serializer found for resource: #{resource.inspect}"
83
+ throw :no_serializer
84
+ else
85
+ serializer_class.new(resource, options.except(:serializer))
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ class Serializer
5
+ UndefinedCacheKey = Class.new(StandardError)
6
+ module Caching
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ with_options instance_writer: false, instance_reader: false do |serializer|
11
+ serializer.class_attribute :_cache # @api private : the cache store
12
+ serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
13
+ serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
14
+ serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
15
+ serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
16
+ # _cache_options include:
17
+ # expires_in
18
+ # compress
19
+ # force
20
+ # race_condition_ttl
21
+ # Passed to ::_cache as
22
+ # serializer.cache_store.fetch(cache_key, @klass._cache_options)
23
+ # Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options)
24
+ serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
25
+ end
26
+ end
27
+
28
+ # Matches
29
+ # "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
30
+ # AND
31
+ # "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
32
+ # AS
33
+ # c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb
34
+ CALLER_FILE = /
35
+ \A # start of string
36
+ .+ # file path (one or more characters)
37
+ (?= # stop previous match when
38
+ :\d+ # a colon is followed by one or more digits
39
+ :in # followed by a colon followed by in
40
+ )
41
+ /x
42
+
43
+ module ClassMethods
44
+ def inherited(base)
45
+ caller_line = caller[1]
46
+ base._cache_digest_file_path = caller_line
47
+ super
48
+ end
49
+
50
+ def _cache_digest
51
+ return @_cache_digest if defined?(@_cache_digest)
52
+ @_cache_digest = digest_caller_file(_cache_digest_file_path)
53
+ end
54
+
55
+ # Hashes contents of file for +_cache_digest+
56
+ def digest_caller_file(caller_line)
57
+ serializer_file_path = caller_line[CALLER_FILE]
58
+ serializer_file_contents = IO.read(serializer_file_path)
59
+ algorithm = ActiveModelSerializers.config.use_sha1_digests ? Digest::SHA1 : Digest::MD5
60
+ algorithm.hexdigest(serializer_file_contents)
61
+ rescue TypeError, Errno::ENOENT
62
+ warn <<-EOF.strip_heredoc
63
+ Cannot digest non-existent file: '#{caller_line}'.
64
+ Please set `::_cache_digest` of the serializer
65
+ if you'd like to cache it.
66
+ EOF
67
+ ''.freeze
68
+ end
69
+
70
+ def _skip_digest?
71
+ _cache_options && _cache_options[:skip_digest]
72
+ end
73
+
74
+ # @api private
75
+ # maps attribute value to explicit key name
76
+ # @see Serializer::attribute
77
+ # @see Serializer::fragmented_attributes
78
+ def _attributes_keys
79
+ _attributes_data
80
+ .each_with_object({}) do |(key, attr), hash|
81
+ next if key == attr.name
82
+ hash[attr.name] = { key: key }
83
+ end
84
+ end
85
+
86
+ def fragmented_attributes
87
+ cached = _cache_only ? _cache_only : _attributes - _cache_except
88
+ cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
89
+ non_cached = _attributes - cached
90
+ non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) }
91
+ {
92
+ cached: cached,
93
+ non_cached: non_cached
94
+ }
95
+ end
96
+
97
+ # Enables a serializer to be automatically cached
98
+ #
99
+ # Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
100
+ # when Rails.configuration.action_controller.perform_caching
101
+ #
102
+ # @param options [Hash] with valid keys:
103
+ # cache_store : @see ::_cache
104
+ # key : @see ::_cache_key
105
+ # only : @see ::_cache_only
106
+ # except : @see ::_cache_except
107
+ # skip_digest : does not include digest in cache_key
108
+ # all else : @see ::_cache_options
109
+ #
110
+ # @example
111
+ # class PostSerializer < ActiveModel::Serializer
112
+ # cache key: 'post', expires_in: 3.hours
113
+ # attributes :title, :body
114
+ #
115
+ # has_many :comments
116
+ # end
117
+ #
118
+ # @todo require less code comments. See
119
+ # https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
120
+ def cache(options = {})
121
+ self._cache =
122
+ options.delete(:cache_store) ||
123
+ ActiveModelSerializers.config.cache_store ||
124
+ ActiveSupport::Cache.lookup_store(:null_store)
125
+ self._cache_key = options.delete(:key)
126
+ self._cache_only = options.delete(:only)
127
+ self._cache_except = options.delete(:except)
128
+ self._cache_options = options.empty? ? nil : options
129
+ end
130
+
131
+ # Value is from ActiveModelSerializers.config.perform_caching. Is used to
132
+ # globally enable or disable all serializer caching, just like
133
+ # Rails.configuration.action_controller.perform_caching, which is its
134
+ # default value in a Rails application.
135
+ # @return [true, false]
136
+ # Memoizes value of config first time it is called with a non-nil value.
137
+ # rubocop:disable Style/ClassVars
138
+ def perform_caching
139
+ return @@perform_caching if defined?(@@perform_caching) && !@@perform_caching.nil?
140
+ @@perform_caching = ActiveModelSerializers.config.perform_caching
141
+ end
142
+ alias perform_caching? perform_caching
143
+ # rubocop:enable Style/ClassVars
144
+
145
+ # The canonical method for getting the cache store for the serializer.
146
+ #
147
+ # @return [nil] when _cache is not set (i.e. when `cache` has not been called)
148
+ # @return [._cache] when _cache is not the NullStore
149
+ # @return [ActiveModelSerializers.config.cache_store] when _cache is the NullStore.
150
+ # This is so we can use `cache` being called to mean the serializer should be cached
151
+ # even if ActiveModelSerializers.config.cache_store has not yet been set.
152
+ # That means that when _cache is the NullStore and ActiveModelSerializers.config.cache_store
153
+ # is configured, `cache_store` becomes `ActiveModelSerializers.config.cache_store`.
154
+ # @return [nil] when _cache is the NullStore and ActiveModelSerializers.config.cache_store is nil.
155
+ def cache_store
156
+ return nil if _cache.nil?
157
+ return _cache if _cache.class != ActiveSupport::Cache::NullStore
158
+ if ActiveModelSerializers.config.cache_store
159
+ self._cache = ActiveModelSerializers.config.cache_store
160
+ else
161
+ nil
162
+ end
163
+ end
164
+
165
+ def cache_enabled?
166
+ perform_caching? && cache_store && !_cache_only && !_cache_except
167
+ end
168
+
169
+ def fragment_cache_enabled?
170
+ perform_caching? && cache_store &&
171
+ (_cache_only && !_cache_except || !_cache_only && _cache_except)
172
+ end
173
+
174
+ # Read cache from cache_store
175
+ # @return [Hash]
176
+ # Used in CollectionSerializer to set :cached_attributes
177
+ def cache_read_multi(collection_serializer, adapter_instance, include_directive)
178
+ return {} if ActiveModelSerializers.config.cache_store.blank?
179
+
180
+ keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)
181
+
182
+ return {} if keys.blank?
183
+
184
+ ActiveModelSerializers.config.cache_store.read_multi(*keys)
185
+ end
186
+
187
+ # Find all cache_key for the collection_serializer
188
+ # @param serializers [ActiveModel::Serializer::CollectionSerializer]
189
+ # @param adapter_instance [ActiveModelSerializers::Adapter::Base]
190
+ # @param include_directive [JSONAPI::IncludeDirective]
191
+ # @return [Array] all cache_key of collection_serializer
192
+ def object_cache_keys(collection_serializer, adapter_instance, include_directive)
193
+ cache_keys = []
194
+
195
+ collection_serializer.each do |serializer|
196
+ cache_keys << object_cache_key(serializer, adapter_instance)
197
+
198
+ serializer.associations(include_directive).each do |association|
199
+ # TODO(BF): Process relationship without evaluating lazy_association
200
+ association_serializer = association.lazy_association.serializer
201
+ if association_serializer.respond_to?(:each)
202
+ association_serializer.each do |sub_serializer|
203
+ cache_keys << object_cache_key(sub_serializer, adapter_instance)
204
+ end
205
+ else
206
+ cache_keys << object_cache_key(association_serializer, adapter_instance)
207
+ end
208
+ end
209
+ end
210
+
211
+ cache_keys.compact.uniq
212
+ end
213
+
214
+ # @return [String, nil] the cache_key of the serializer or nil
215
+ def object_cache_key(serializer, adapter_instance)
216
+ return unless serializer.present? && serializer.object.present?
217
+
218
+ (serializer.class.cache_enabled? || serializer.class.fragment_cache_enabled?) ? serializer.cache_key(adapter_instance) : nil
219
+ end
220
+ end
221
+
222
+ ### INSTANCE METHODS
223
+ def fetch_attributes(fields, cached_attributes, adapter_instance)
224
+ key = cache_key(adapter_instance)
225
+ cached_attributes.fetch(key) do
226
+ fetch(adapter_instance, serializer_class._cache_options, key) do
227
+ attributes(fields, true)
228
+ end
229
+ end
230
+ end
231
+
232
+ def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = nil)
233
+ if serializer_class.cache_store
234
+ key ||= cache_key(adapter_instance)
235
+ serializer_class.cache_store.fetch(key, cache_options) do
236
+ yield
237
+ end
238
+ else
239
+ yield
240
+ end
241
+ end
242
+
243
+ # 1. Determine cached fields from serializer class options
244
+ # 2. Get non_cached_fields and fetch cache_fields
245
+ # 3. Merge the two hashes using adapter_instance#fragment_cache
246
+ def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
247
+ serializer_class._cache_options ||= {}
248
+ serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
249
+ fields = serializer_class.fragmented_attributes
250
+
251
+ non_cached_fields = fields[:non_cached].dup
252
+ non_cached_hash = attributes(non_cached_fields, true)
253
+ include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
254
+ non_cached_hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
255
+
256
+ cached_fields = fields[:cached].dup
257
+ key = cache_key(adapter_instance)
258
+ cached_hash =
259
+ cached_attributes.fetch(key) do
260
+ fetch(adapter_instance, serializer_class._cache_options, key) do
261
+ hash = attributes(cached_fields, true)
262
+ include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
263
+ hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
264
+ end
265
+ end
266
+ # Merge both results
267
+ adapter_instance.fragment_cache(cached_hash, non_cached_hash)
268
+ end
269
+
270
+ def cache_key(adapter_instance)
271
+ return @cache_key if defined?(@cache_key)
272
+
273
+ parts = []
274
+ parts << object_cache_key
275
+ parts << adapter_instance.cache_key
276
+ parts << serializer_class._cache_digest unless serializer_class._skip_digest?
277
+ @cache_key = expand_cache_key(parts)
278
+ end
279
+
280
+ def expand_cache_key(parts)
281
+ ActiveSupport::Cache.expand_cache_key(parts)
282
+ end
283
+
284
+ # Use object's cache_key if available, else derive a key from the object
285
+ # Pass the `key` option to the `cache` declaration or override this method to customize the cache key
286
+ def object_cache_key
287
+ if object.respond_to?(:cache_key_with_version)
288
+ object.cache_key_with_version
289
+ elsif object.respond_to?(:cache_key)
290
+ object.cache_key
291
+ elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key]))
292
+ object_time_safe = object.updated_at
293
+ object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
294
+ "#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
295
+ else
296
+ fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'"
297
+ end
298
+ end
299
+
300
+ def serializer_class
301
+ @serializer_class ||= self.class
302
+ end
303
+ end
304
+ end
305
+ end