faraday-http-cache 2.1.0 → 2.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a2147bf25b6776f94d77ce49cd1147a790978446
4
- data.tar.gz: cd45d9ddd630c3b627595e32826629a5c1e1d848
2
+ SHA256:
3
+ metadata.gz: 6603be3bbf6a2840ba11f947f7e42f030ab52600912e9a62d16aaf27ee5e6446
4
+ data.tar.gz: 95b8b2f4b08525406027d47e14341f5f301409a2518332b243dc08d2bde4e693
5
5
  SHA512:
6
- metadata.gz: 432baf735bfa450affe7fa9a88aff70667a8a37b93fcbcb490ccd75f8b69498459716f273af9b32093f6435680a4f20f9d2d1ef9212d91646eba2ef53ad73745
7
- data.tar.gz: ceb797b25268e5ef954684f7332ed790c49db1ca0f9709be87e4722a94553d806787932d5021e4829b65952be15982338266b377b65432da441584255f8712e1
6
+ metadata.gz: 4675c865c18641322c71b77881a74d3ca4edbf4fb1d628ac0fe7a6388d6a3c973946f1d7fa7b09d654f1eff61484a8c2d8524c966e4377c1e12854bf8114e13a
7
+ data.tar.gz: 832d7579eb75d3631e539ca7af996b9d062dbe43582468f98c349b3dbc963dde17cecf57a0202af1916297013143becba1a0fc1f32ddf92ab98490876e3efc84
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Faraday Http Cache
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/sourcelevel/faraday-http-cache.svg?branch=master)](https://travis-ci.org/sourcelevel/faraday-http-cache)
3
+ [![Gem Version](https://badge.fury.io/rb/faraday-http-cache.svg)](https://rubygems.org/gems/faraday-http-cache)
4
+ [![Build](https://github.com/sourcelevel/faraday-http-cache/actions/workflows/main.yml/badge.svg)](https://github.com/sourcelevel/faraday-http-cache/actions)
4
5
 
5
- a [Faraday](https://github.com/lostisland/faraday) middleware that respects HTTP cache,
6
+ A [Faraday](https://github.com/lostisland/faraday) middleware that respects HTTP cache,
6
7
  by checking expiration and validation of the stored responses.
7
8
 
8
9
  ## Installation
@@ -53,15 +54,15 @@ This type of store **might not be persisted across multiple processes or connect
53
54
  so it is probably not suitable for most production environments.
54
55
  Make sure that you configure a store that is suitable for you.
55
56
 
56
- The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
57
- characters in responses. For example, if your JSON returns `"name": "Raül"` then you might see
58
- errors like:
57
+ The stdlib `JSON` module is used for serialization by default, which can struggle with unicode
58
+ characters in responses in Ruby < 3.1. For example, if your JSON returns `"name": "Raül"` then
59
+ you might see errors like:
59
60
 
60
61
  ```
61
62
  Response could not be serialized: "\xC3" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.
62
63
  ```
63
64
 
64
- For full unicode support, or if you expect to be dealing with images, you can use
65
+ For full unicode support, or if you expect to be dealing with images, you can use the stdlib
65
66
  [Marshal][marshal] instead. Alternatively you could use another json library like `oj` or `yajl-ruby`.
66
67
 
67
68
  ```ruby
@@ -71,9 +72,47 @@ client = Faraday.new do |builder|
71
72
  end
72
73
  ```
73
74
 
75
+ ### Strategies
76
+
77
+ You can provide a `:strategy` option to the middleware to specify the strategy to use.
78
+
79
+ ```ruby
80
+ client = Faraday.new do |builder|
81
+ builder.use :http_cache, store: Rails.cache, strategy: Faraday::HttpCache::Strategies::ByVary
82
+ builder.adapter Faraday.default_adapter
83
+ end
84
+ ```
85
+
86
+ Available strategies are:
87
+
88
+ #### `Faraday::HttpCache::Strategies::ByUrl`
89
+
90
+ The default strategy.
91
+ It Uses URL + HTTP method to generate cache keys and stores an array of request + response for each key.
92
+
93
+ #### `Faraday::HttpCache::Strategies::ByVary`
94
+
95
+ This strategy uses headers from `Vary` header to generate cache keys.
96
+ It also uses cache to store `Vary` headers mapped to the request URL.
97
+ This strategy is more suitable for caching private responses with the same URLs but different results for different users, like `https://api.github.com/user`.
98
+
99
+ *Note:* To automatically remove stale cache keys, you might want to use the `:expires_in` option.
100
+
101
+ ```ruby
102
+ store = ActiveSupport::Cache.lookup_store(:redis_cache_store, expires_in: 1.day, url: 'redis://localhost:6379/0')
103
+ client = Faraday.new do |builder|
104
+ builder.use :http_cache, store: store, strategy: Faraday::HttpCache::Strategies::ByVary
105
+ builder.adapter Faraday.default_adapter
106
+ end
107
+ ```
108
+
109
+ #### Custom strategies
110
+
111
+ You can write your own strategy by subclassing `Faraday::HttpCache::Strategies::BaseStrategy` and implementing `#write`, `#read` and `#delete` methods.
112
+
74
113
  ### Logging
75
114
 
76
- You can provide a `:logger` option that will be receive debug informations based on the middleware
115
+ You can provide a `:logger` option that will receive debug information based on the middleware
77
116
  operations:
78
117
 
79
118
  ```ruby
@@ -82,7 +121,7 @@ client = Faraday.new do |builder|
82
121
  builder.adapter Faraday.default_adapter
83
122
  end
84
123
 
85
- client.get('http://site/api/users')
124
+ client.get('https://site/api/users')
86
125
  # logs "HTTP Cache: [GET users] miss, store"
87
126
  ```
88
127
 
@@ -133,6 +172,7 @@ execute the files under the `examples` directory to see a sample of the middlewa
133
172
  ## What gets cached?
134
173
 
135
174
  The middleware will use the following headers to make caching decisions:
175
+ - Vary
136
176
  - Cache-Control
137
177
  - Age
138
178
  - Last-Modified
@@ -155,7 +195,7 @@ client = Faraday.new do |builder|
155
195
  builder.adapter Faraday.default_adapter
156
196
  end
157
197
 
158
- client.get('http://site/api/some-private-resource') # => will be cached
198
+ client.get('https://site/api/some-private-resource') # => will be cached
159
199
  ```
160
200
 
161
201
  ## License
@@ -163,4 +203,4 @@ client.get('http://site/api/some-private-resource') # => will be cached
163
203
  Copyright (c) 2012-2018 Plataformatec.
164
204
  Copyright (c) 2019 SourceLevel and contributors.
165
205
 
166
- [marshal]: http://www.ruby-doc.org/core-2.0/Marshal.html
206
+ [marshal]: https://www.ruby-doc.org/core-3.0/Marshal.html
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Faraday
3
4
  class HttpCache < Faraday::Middleware
4
5
  # Internal: A class to represent the 'Cache-Control' header options.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ class HttpCache < Faraday::Middleware
5
+ # @private
6
+ # A Hash based store to be used by strategies
7
+ # when a `store` is not provided for the middleware setup.
8
+ class MemoryStore
9
+ def initialize
10
+ @cache = {}
11
+ end
12
+
13
+ def read(key)
14
+ @cache[key]
15
+ end
16
+
17
+ def delete(key)
18
+ @cache.delete(key)
19
+ end
20
+
21
+ def write(key, value)
22
+ @cache[key] = value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Faraday
3
4
  class HttpCache < Faraday::Middleware
4
5
  # Internal: A class to represent a request
@@ -24,6 +25,7 @@ module Faraday
24
25
  def cacheable?
25
26
  return false if method != :get && method != :head
26
27
  return false if cache_control.no_store?
28
+
27
29
  true
28
30
  end
29
31
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'time'
3
4
  require 'faraday/http_cache/cache_control'
4
5
 
@@ -204,7 +205,7 @@ module Faraday
204
205
  # Returns nothing.
205
206
  def ensure_date_header!
206
207
  date
207
- rescue
208
+ rescue StandardError
208
209
  headers['Date'] = @now.httpdate
209
210
  end
210
211
 
@@ -1,193 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'json'
3
- require 'digest/sha1'
2
+
3
+ require 'faraday/http_cache/strategies/by_url'
4
4
 
5
5
  module Faraday
6
6
  class HttpCache < Faraday::Middleware
7
- # Internal: A wrapper around an ActiveSupport::CacheStore to store responses.
8
- #
9
- # Examples
10
- #
11
- # # Creates a new Storage using a MemCached backend from ActiveSupport.
12
- # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
13
- # Faraday::HttpCache::Storage.new(store: mem_cache_store)
14
- #
15
- # # Reuse some other instance of an ActiveSupport::Cache::Store object.
16
- # Faraday::HttpCache::Storage.new(store: Rails.cache)
17
- #
18
- # # Creates a new Storage using Marshal for serialization.
19
- # Faraday::HttpCache::Storage.new(store: Rails.cache, serializer: Marshal)
20
- class Storage
21
- # Public: Gets the underlying cache store object.
22
- attr_reader :cache
23
-
24
- # Internal: Initialize a new Storage object with a cache backend.
25
- #
26
- # :logger - A Logger object to be used to emit warnings.
27
- # :store - An cache store object that should respond to 'read',
28
- # 'write', and 'delete'.
29
- # :serializer - A serializer object that should respond to 'dump'
30
- # and 'load'.
31
- def initialize(store: nil, serializer: nil, logger: nil)
32
- @cache = store || MemoryStore.new
33
- @serializer = serializer || JSON
34
- @logger = logger
35
- assert_valid_store!
36
- end
37
-
38
- # Internal: Store a response inside the cache.
39
- #
40
- # request - A Faraday::HttpCache::::Request instance of the executed HTTP
41
- # request.
42
- # response - The Faraday::HttpCache::Response instance to be stored.
43
- #
44
- # Returns nothing.
45
- def write(request, response)
46
- key = cache_key_for(request.url)
47
- entry = serialize_entry(request.serializable_hash, response.serializable_hash)
48
-
49
- entries = cache.read(key) || []
50
- entries = entries.dup if entries.frozen?
51
-
52
- entries.reject! do |(cached_request, cached_response)|
53
- response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
54
- end
55
-
56
- entries << entry
57
-
58
- cache.write(key, entries)
59
- rescue ::Encoding::UndefinedConversionError => e
60
- warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
61
- raise e
62
- end
63
-
64
- # Internal: Attempt to retrieve an stored response that suits the incoming
65
- # HTTP request.
66
- #
67
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
68
- # request.
69
- # klass - The Class to be instantiated with the stored response.
70
- #
71
- # Returns an instance of 'klass'.
72
- def read(request, klass: Faraday::HttpCache::Response)
73
- cache_key = cache_key_for(request.url)
74
- entries = cache.read(cache_key)
75
- response = lookup_response(request, entries)
76
-
77
- if response
78
- klass.new(response)
79
- end
80
- end
81
-
82
- def delete(url)
83
- cache_key = cache_key_for(url)
84
- cache.delete(cache_key)
85
- end
86
-
87
- private
88
-
89
- # Internal: Retrieve a response Hash from the list of entries that match
90
- # the given request.
91
- #
92
- # request - A Faraday::HttpCache::::Request instance of the incoming HTTP
93
- # request.
94
- # entries - An Array of pairs of Hashes (request, response).
95
- #
96
- # Returns a Hash or nil.
97
- def lookup_response(request, entries)
98
- if entries
99
- entries = entries.map { |entry| deserialize_entry(*entry) }
100
- _, response = entries.find { |req, res| response_matches?(request, req, res) }
101
- response
102
- end
103
- end
104
-
105
- # Internal: Check if a cached response and request matches the given
106
- # request.
107
- #
108
- # request - A Faraday::HttpCache::::Request instance of the
109
- # current HTTP request.
110
- # cached_request - The Hash of the request that was cached.
111
- # cached_response - The Hash of the response that was cached.
112
- #
113
- # Returns true or false.
114
- def response_matches?(request, cached_request, cached_response)
115
- request.method.to_s == cached_request[:method].to_s &&
116
- vary_matches?(cached_response, request, cached_request)
117
- end
118
-
119
- def vary_matches?(cached_response, request, cached_request)
120
- headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
121
- vary = headers['Vary'].to_s
122
-
123
- vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
124
- request.headers[header] == cached_request[:headers][header]
125
- end)
126
- end
127
-
128
- def serialize_entry(*objects)
129
- objects.map { |object| serialize_object(object) }
130
- end
131
-
132
- def serialize_object(object)
133
- @serializer.dump(object)
134
- end
135
-
136
- def deserialize_entry(*objects)
137
- objects.map { |object| deserialize_object(object) }
138
- end
139
-
140
- def deserialize_object(object)
141
- @serializer.load(object).each_with_object({}) do |(key, value), hash|
142
- hash[key.to_sym] = value
143
- end
144
- end
145
-
146
- # Internal: Computes the cache key for a specific request, taking in
147
- # account the current serializer to avoid cross serialization issues.
148
- #
149
- # url - The request URL.
150
- #
151
- # Returns a String.
152
- def cache_key_for(url)
153
- prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
154
- Digest::SHA1.hexdigest("#{prefix}#{url}")
155
- end
156
-
157
- # Internal: Checks if the given cache object supports the
158
- # expect API ('read' and 'write').
159
- #
160
- # Raises an 'ArgumentError'.
161
- #
162
- # Returns nothing.
163
- def assert_valid_store!
164
- unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
165
- raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
166
- end
167
- end
168
-
169
- def warn(message)
170
- @logger.warn(message) if @logger
171
- end
172
- end
173
-
174
- # Internal: A Hash based store to be used by the 'Storage' class
175
- # when a 'store' is not provided for the middleware setup.
176
- class MemoryStore
177
- def initialize
178
- @cache = {}
179
- end
180
-
181
- def read(key)
182
- @cache[key]
183
- end
184
-
185
- def delete(key)
186
- @cache.delete(key)
187
- end
188
-
189
- def write(key, value)
190
- @cache[key] = value
7
+ # @deprecated Use Faraday::HttpCache::Strategies::ByUrl instead.
8
+ class Storage < Faraday::HttpCache::Strategies::ByUrl
9
+ def initialize(*)
10
+ Kernel.warn("Deprecated: #{self.class} is deprecated and will be removed in " \
11
+ 'the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.')
12
+ super
191
13
  end
192
14
  end
193
15
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'faraday/http_cache/memory_store'
5
+
6
+ module Faraday
7
+ class HttpCache < Faraday::Middleware
8
+ module Strategies
9
+ # Base class for all strategies.
10
+ # @abstract
11
+ #
12
+ # @example
13
+ #
14
+ # # Creates a new strategy using a MemCached backend from ActiveSupport.
15
+ # mem_cache_store = ActiveSupport::Cache.lookup_store(:mem_cache_store, ['localhost:11211'])
16
+ # Faraday::HttpCache::Strategies::ByVary.new(store: mem_cache_store)
17
+ #
18
+ # # Reuse some other instance of an ActiveSupport::Cache::Store object.
19
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache)
20
+ #
21
+ # # Creates a new strategy using Marshal for serialization.
22
+ # Faraday::HttpCache::Strategies::ByVary.new(store: Rails.cache, serializer: Marshal)
23
+ class BaseStrategy
24
+ # Returns the underlying cache store object.
25
+ attr_reader :cache
26
+
27
+ # @param [Hash] options the options to create a message with.
28
+ # @option options [Faraday::HttpCache::MemoryStore, nil] :store - a cache
29
+ # store object that should respond to 'read', 'write', and 'delete'.
30
+ # @option options [#dump#load] :serializer - an object that should
31
+ # respond to 'dump' and 'load'.
32
+ # @option options [Logger, nil] :logger - an object to be used to emit warnings.
33
+ def initialize(options = {})
34
+ @cache = options[:store] || Faraday::HttpCache::MemoryStore.new
35
+ @serializer = options[:serializer] || JSON
36
+ @logger = options[:logger] || Logger.new(IO::NULL)
37
+ @cache_salt = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
38
+ assert_valid_store!
39
+ end
40
+
41
+ # Store a response inside the cache.
42
+ # @abstract
43
+ def write(_request, _response)
44
+ raise NotImplementedError, 'Implement this method in your strategy'
45
+ end
46
+
47
+ # Read a response from the cache.
48
+ # @abstract
49
+ def read(_request)
50
+ raise NotImplementedError, 'Implement this method in your strategy'
51
+ end
52
+
53
+ # Delete responses from the cache by the url.
54
+ # @abstract
55
+ def delete(_url)
56
+ raise NotImplementedError, 'Implement this method in your strategy'
57
+ end
58
+
59
+ private
60
+
61
+ # @private
62
+ # @raise [ArgumentError] if the cache object doesn't support the expect API.
63
+ def assert_valid_store!
64
+ unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
65
+ raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
66
+ end
67
+ end
68
+
69
+ def serialize_entry(*objects)
70
+ objects.map { |object| serialize_object(object) }
71
+ end
72
+
73
+ def serialize_object(object)
74
+ @serializer.dump(object)
75
+ end
76
+
77
+ def deserialize_entry(*objects)
78
+ objects.map { |object| deserialize_object(object) }
79
+ end
80
+
81
+ def deserialize_object(object)
82
+ @serializer.load(object).each_with_object({}) do |(key, value), hash|
83
+ hash[key.to_sym] = value
84
+ end
85
+ end
86
+
87
+ def warn(message)
88
+ @logger.warn(message)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'faraday/http_cache/strategies/base_strategy'
6
+
7
+ module Faraday
8
+ class HttpCache < Faraday::Middleware
9
+ module Strategies
10
+ # The original strategy by Faraday::HttpCache.
11
+ # Uses URL + HTTP method to generate cache keys.
12
+ class ByUrl < BaseStrategy
13
+ # Store a response inside the cache.
14
+ #
15
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
16
+ # @param [Faraday::HttpCache::Response] response - instance to be stored.
17
+ #
18
+ # @return [void]
19
+ def write(request, response)
20
+ key = cache_key_for(request.url)
21
+ entry = serialize_entry(request.serializable_hash, response.serializable_hash)
22
+ entries = cache.read(key) || []
23
+ entries = entries.dup if entries.frozen?
24
+ entries.reject! do |(cached_request, cached_response)|
25
+ response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
26
+ end
27
+
28
+ entries << entry
29
+
30
+ cache.write(key, entries)
31
+ rescue ::Encoding::UndefinedConversionError => e
32
+ warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
33
+ raise e
34
+ end
35
+
36
+ # Fetch a stored response that suits the incoming HTTP request or return nil.
37
+ #
38
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
39
+ #
40
+ # @return [Faraday::HttpCache::Response, nil]
41
+ def read(request)
42
+ cache_key = cache_key_for(request.url)
43
+ entries = cache.read(cache_key)
44
+ response = lookup_response(request, entries)
45
+ return nil unless response
46
+
47
+ Faraday::HttpCache::Response.new(response)
48
+ end
49
+
50
+ # @param [String] url – the url of a changed resource, will be used to invalidate the cache.
51
+ #
52
+ # @return [void]
53
+ def delete(url)
54
+ cache_key = cache_key_for(url)
55
+ cache.delete(cache_key)
56
+ end
57
+
58
+ private
59
+
60
+ # Retrieve a response Hash from the list of entries that match the given request.
61
+ #
62
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
63
+ # @param [Array<Array(Hash, Hash)>] entries - pairs of Hashes (request, response).
64
+ #
65
+ # @return [Hash, nil]
66
+ def lookup_response(request, entries)
67
+ if entries
68
+ entries = entries.map { |entry| deserialize_entry(*entry) }
69
+ _, response = entries.find { |req, res| response_matches?(request, req, res) }
70
+ response
71
+ end
72
+ end
73
+
74
+ # Check if a cached response and request matches the given request.
75
+ #
76
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
77
+ # @param [Hash] cached_request - a Hash of the request that was cached.
78
+ # @param [Hash] cached_response - a Hash of the response that was cached.
79
+ #
80
+ # @return [true, false]
81
+ def response_matches?(request, cached_request, cached_response)
82
+ request.method.to_s == cached_request[:method].to_s &&
83
+ vary_matches?(cached_response, request, cached_request)
84
+ end
85
+
86
+ # Check if the cached request matches the incoming
87
+ # request based on the Vary header of cached response.
88
+ #
89
+ # If Vary header is not present, the request is considered to match.
90
+ # If Vary header is '*', the request is considered to not match.
91
+ #
92
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
93
+ # @param [Hash] cached_request - a Hash of the request that was cached.
94
+ # @param [Hash] cached_response - a Hash of the response that was cached.
95
+ #
96
+ # @return [true, false]
97
+ def vary_matches?(cached_response, request, cached_request)
98
+ headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
99
+ vary = headers['Vary'].to_s
100
+
101
+ vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
102
+ request.headers[header] == cached_request[:headers][header]
103
+ end)
104
+ end
105
+
106
+ # Computes the cache key for a specific request, taking
107
+ # in account the current serializer to avoid cross serialization issues.
108
+ #
109
+ # @param [String] url - the request URL.
110
+ #
111
+ # @return [String]
112
+ def cache_key_for(url)
113
+ Digest::SHA1.hexdigest("#{@cache_salt}#{url}")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha1'
4
+
5
+ require 'faraday/http_cache/strategies/base_strategy'
6
+
7
+ module Faraday
8
+ class HttpCache < Faraday::Middleware
9
+ module Strategies
10
+ # This strategy uses headers from the Vary response header to generate cache keys.
11
+ # It also uses the index with Vary headers mapped to the request url.
12
+ # This strategy is more suitable for caching private responses with the same urls,
13
+ # like https://api.github.com/user.
14
+ #
15
+ # This strategy does not support #delete method to clear cache on unsafe methods.
16
+ class ByVary < BaseStrategy
17
+ # Store a response inside the cache.
18
+ #
19
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
20
+ # @param [Faraday::HttpCache::Response] response - instance to be stored.
21
+ #
22
+ # @return [void]
23
+ def write(request, response)
24
+ vary_cache_key = vary_cache_key_for(request)
25
+ headers = Faraday::Utils::Headers.new(response.payload[:response_headers])
26
+ vary = headers['Vary'].to_s
27
+ cache.write(vary_cache_key, vary)
28
+
29
+ response_cache_key = response_cache_key_for(request, vary)
30
+ entry = serialize_object(response.serializable_hash)
31
+ cache.write(response_cache_key, entry)
32
+ rescue ::Encoding::UndefinedConversionError => e
33
+ warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
34
+ raise e
35
+ end
36
+
37
+ # Fetch a stored response that suits the incoming HTTP request or return nil.
38
+ #
39
+ # @param [Faraday::HttpCache::Request] request - an instance of the incoming HTTP request.
40
+ #
41
+ # @return [Faraday::HttpCache::Response, nil]
42
+ def read(request)
43
+ vary_cache_key = vary_cache_key_for(request)
44
+ vary = cache.read(vary_cache_key)
45
+ return nil if vary.nil? || vary == '*'
46
+
47
+ cache_key = response_cache_key_for(request, vary)
48
+ response = cache.read(cache_key)
49
+ return nil if response.nil?
50
+
51
+ Faraday::HttpCache::Response.new(deserialize_object(response))
52
+ end
53
+
54
+ # This strategy does not support #delete method to clear cache on unsafe methods.
55
+ # @return [void]
56
+ def delete(_url)
57
+ # do nothing since we can't find the key by url
58
+ end
59
+
60
+ private
61
+
62
+ # Computes the cache key for the index with Vary headers.
63
+ #
64
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
65
+ #
66
+ # @return [String]
67
+ def vary_cache_key_for(request)
68
+ method = request.method.to_s
69
+ Digest::SHA1.hexdigest("by_vary_index#{@cache_salt}#{method}#{request.url}")
70
+ end
71
+
72
+ # Computes the cache key for the response.
73
+ #
74
+ # @param [Faraday::HttpCache::Request] request - instance of the executed HTTP request.
75
+ # @param [String] vary - the Vary header value.
76
+ #
77
+ # @return [String]
78
+ def response_cache_key_for(request, vary)
79
+ method = request.method.to_s
80
+ headers = vary.split(/[\s,]+/).map { |header| request.headers[header] }
81
+ Digest::SHA1.hexdigest("by_vary#{@cache_salt}#{method}#{request.url}#{headers.join}")
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end