active_model_serializers 0.9.0 → 0.10.12

Sign up to get free protection for your applications and to get access to all the features.
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