File: caching.rb

package info (click to toggle)
ruby-active-model-serializers 0.10.12-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,752 kB
  • sloc: ruby: 13,138; sh: 53; makefile: 6
file content (305 lines) | stat: -rw-r--r-- 13,164 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# frozen_string_literal: true

module ActiveModel
  class Serializer
    UndefinedCacheKey = Class.new(StandardError)
    module Caching
      extend ActiveSupport::Concern

      included do
        with_options instance_writer: false, instance_reader: false do |serializer|
          serializer.class_attribute :_cache         # @api private : the cache store
          serializer.class_attribute :_cache_key     # @api private : when present, is first item in cache_key.  Ignored if the serializable object defines #cache_key.
          serializer.class_attribute :_cache_only    # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
          serializer.class_attribute :_cache_except  # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
          serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
          #  _cache_options include:
          #    expires_in
          #    compress
          #    force
          #    race_condition_ttl
          #  Passed to ::_cache as
          #    serializer.cache_store.fetch(cache_key, @klass._cache_options)
          #  Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options)
          serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
        end
      end

      # Matches
      #  "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
      #  AND
      #  "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
      #  AS
      #  c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb
      CALLER_FILE = /
        \A       # start of string
        .+       # file path (one or more characters)
        (?=      # stop previous match when
          :\d+     # a colon is followed by one or more digits
          :in      # followed by a colon followed by in
        )
      /x

      module ClassMethods
        def inherited(base)
          caller_line = caller[1]
          base._cache_digest_file_path = caller_line
          super
        end

        def _cache_digest
          return @_cache_digest if defined?(@_cache_digest)
          @_cache_digest = digest_caller_file(_cache_digest_file_path)
        end

        # Hashes contents of file for +_cache_digest+
        def digest_caller_file(caller_line)
          serializer_file_path = caller_line[CALLER_FILE]
          serializer_file_contents = IO.read(serializer_file_path)
          algorithm = ActiveModelSerializers.config.use_sha1_digests ? Digest::SHA1 : Digest::MD5
          algorithm.hexdigest(serializer_file_contents)
        rescue TypeError, Errno::ENOENT
          warn <<-EOF.strip_heredoc
            Cannot digest non-existent file: '#{caller_line}'.
            Please set `::_cache_digest` of the serializer
            if you'd like to cache it.
            EOF
          ''.freeze
        end

        def _skip_digest?
          _cache_options && _cache_options[:skip_digest]
        end

        # @api private
        # maps attribute value to explicit key name
        # @see Serializer::attribute
        # @see Serializer::fragmented_attributes
        def _attributes_keys
          _attributes_data
            .each_with_object({}) do |(key, attr), hash|
            next if key == attr.name
            hash[attr.name] = { key: key }
          end
        end

        def fragmented_attributes
          cached = _cache_only ? _cache_only : _attributes - _cache_except
          cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
          non_cached = _attributes - cached
          non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) }
          {
            cached: cached,
            non_cached: non_cached
          }
        end

        # Enables a serializer to be automatically cached
        #
        # Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
        #   when Rails.configuration.action_controller.perform_caching
        #
        # @param options [Hash] with valid keys:
        #   cache_store    : @see ::_cache
        #   key            : @see ::_cache_key
        #   only           : @see ::_cache_only
        #   except         : @see ::_cache_except
        #   skip_digest    : does not include digest in cache_key
        #   all else       : @see ::_cache_options
        #
        # @example
        #   class PostSerializer < ActiveModel::Serializer
        #     cache key: 'post', expires_in: 3.hours
        #     attributes :title, :body
        #
        #     has_many :comments
        #   end
        #
        # @todo require less code comments. See
        # https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
        def cache(options = {})
          self._cache =
            options.delete(:cache_store) ||
            ActiveModelSerializers.config.cache_store ||
            ActiveSupport::Cache.lookup_store(:null_store)
          self._cache_key = options.delete(:key)
          self._cache_only = options.delete(:only)
          self._cache_except = options.delete(:except)
          self._cache_options = options.empty? ? nil : options
        end

        # Value is from ActiveModelSerializers.config.perform_caching. Is used to
        # globally enable or disable all serializer caching, just like
        # Rails.configuration.action_controller.perform_caching, which is its
        # default value in a Rails application.
        # @return [true, false]
        # Memoizes value of config first time it is called with a non-nil value.
        # rubocop:disable Style/ClassVars
        def perform_caching
          return @@perform_caching if defined?(@@perform_caching) && !@@perform_caching.nil?
          @@perform_caching = ActiveModelSerializers.config.perform_caching
        end
        alias perform_caching? perform_caching
        # rubocop:enable Style/ClassVars

        # The canonical method for getting the cache store for the serializer.
        #
        # @return [nil] when _cache is not set (i.e. when `cache` has not been called)
        # @return [._cache] when _cache is not the NullStore
        # @return [ActiveModelSerializers.config.cache_store] when _cache is the NullStore.
        #   This is so we can use `cache` being called to mean the serializer should be cached
        #   even if ActiveModelSerializers.config.cache_store has not yet been set.
        #   That means that when _cache is the NullStore and ActiveModelSerializers.config.cache_store
        #   is configured, `cache_store` becomes `ActiveModelSerializers.config.cache_store`.
        # @return [nil] when _cache is the NullStore and ActiveModelSerializers.config.cache_store is nil.
        def cache_store
          return nil if _cache.nil?
          return _cache if _cache.class != ActiveSupport::Cache::NullStore
          if ActiveModelSerializers.config.cache_store
            self._cache = ActiveModelSerializers.config.cache_store
          else
            nil
          end
        end

        def cache_enabled?
          perform_caching? && cache_store && !_cache_only && !_cache_except
        end

        def fragment_cache_enabled?
          perform_caching? && cache_store &&
            (_cache_only && !_cache_except || !_cache_only && _cache_except)
        end

        # Read cache from cache_store
        # @return [Hash]
        # Used in CollectionSerializer to set :cached_attributes
        def cache_read_multi(collection_serializer, adapter_instance, include_directive)
          return {} if ActiveModelSerializers.config.cache_store.blank?

          keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)

          return {} if keys.blank?

          ActiveModelSerializers.config.cache_store.read_multi(*keys)
        end

        # Find all cache_key for the collection_serializer
        # @param serializers [ActiveModel::Serializer::CollectionSerializer]
        # @param adapter_instance [ActiveModelSerializers::Adapter::Base]
        # @param include_directive [JSONAPI::IncludeDirective]
        # @return [Array] all cache_key of collection_serializer
        def object_cache_keys(collection_serializer, adapter_instance, include_directive)
          cache_keys = []

          collection_serializer.each do |serializer|
            cache_keys << object_cache_key(serializer, adapter_instance)

            serializer.associations(include_directive).each do |association|
              # TODO(BF): Process relationship without evaluating lazy_association
              association_serializer = association.lazy_association.serializer
              if association_serializer.respond_to?(:each)
                association_serializer.each do |sub_serializer|
                  cache_keys << object_cache_key(sub_serializer, adapter_instance)
                end
              else
                cache_keys << object_cache_key(association_serializer, adapter_instance)
              end
            end
          end

          cache_keys.compact.uniq
        end

        # @return [String, nil] the cache_key of the serializer or nil
        def object_cache_key(serializer, adapter_instance)
          return unless serializer.present? && serializer.object.present?

          (serializer.class.cache_enabled? || serializer.class.fragment_cache_enabled?) ? serializer.cache_key(adapter_instance) : nil
        end
      end

      ### INSTANCE METHODS
      def fetch_attributes(fields, cached_attributes, adapter_instance)
        key = cache_key(adapter_instance)
        cached_attributes.fetch(key) do
          fetch(adapter_instance, serializer_class._cache_options, key) do
            attributes(fields, true)
          end
        end
      end

      def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = nil)
        if serializer_class.cache_store
          key ||= cache_key(adapter_instance)
          serializer_class.cache_store.fetch(key, cache_options) do
            yield
          end
        else
          yield
        end
      end

      # 1. Determine cached fields from serializer class options
      # 2. Get non_cached_fields and fetch cache_fields
      # 3. Merge the two hashes using adapter_instance#fragment_cache
      def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
        serializer_class._cache_options ||= {}
        serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
        fields = serializer_class.fragmented_attributes

        non_cached_fields = fields[:non_cached].dup
        non_cached_hash = attributes(non_cached_fields, true)
        include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
        non_cached_hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)

        cached_fields = fields[:cached].dup
        key = cache_key(adapter_instance)
        cached_hash =
          cached_attributes.fetch(key) do
            fetch(adapter_instance, serializer_class._cache_options, key) do
              hash = attributes(cached_fields, true)
              include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
              hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
            end
          end
        # Merge both results
        adapter_instance.fragment_cache(cached_hash, non_cached_hash)
      end

      def cache_key(adapter_instance)
        return @cache_key if defined?(@cache_key)

        parts = []
        parts << object_cache_key
        parts << adapter_instance.cache_key
        parts << serializer_class._cache_digest unless serializer_class._skip_digest?
        @cache_key = expand_cache_key(parts)
      end

      def expand_cache_key(parts)
        ActiveSupport::Cache.expand_cache_key(parts)
      end

      # Use object's cache_key if available, else derive a key from the object
      # Pass the `key` option to the `cache` declaration or override this method to customize the cache key
      def object_cache_key
        if object.respond_to?(:cache_key_with_version)
          object.cache_key_with_version
        elsif object.respond_to?(:cache_key)
          object.cache_key
        elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key]))
          object_time_safe = object.updated_at
          object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
          "#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
        else
          fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'"
        end
      end

      def serializer_class
        @serializer_class ||= self.class
      end
    end
  end
end