faraday-http-cache 0.4.2 → 1.0.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 +4 -4
- data/lib/faraday/http_cache.rb +59 -91
- data/lib/faraday/http_cache/request.rb +46 -0
- data/lib/faraday/http_cache/storage.rb +107 -61
- data/spec/{middleware_spec.rb → http_cache_spec.rb} +95 -53
- data/spec/request_spec.rb +48 -0
- data/spec/storage_spec.rb +27 -39
- data/spec/support/test_app.rb +36 -0
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f34a3a6ee332a8ce2480f41e1071020237c0ae1e
|
4
|
+
data.tar.gz: e1fc2519d29fc6656e156ffa4d9bf722e8b17297
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa4045303477608539cc6f844daf4ebade16ac4e3e4ba7ecc5c11a4f9f096c4895893f773708607826dc1428b50814cf5e6e1298330e36f6e5c4f3cc8d5a817c
|
7
|
+
data.tar.gz: f56ed4f205684f471a39fd471b107e3fb417ffcdb945af2c67ad5d018958bbc93873076e0eeab0b97ac6f4984243aba5d27bf73246480c0a1e675e4009bfbc8c
|
data/lib/faraday/http_cache.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'faraday'
|
2
2
|
|
3
3
|
require 'faraday/http_cache/storage'
|
4
|
+
require 'faraday/http_cache/request'
|
4
5
|
require 'faraday/http_cache/response'
|
5
6
|
|
6
7
|
module Faraday
|
@@ -38,7 +39,11 @@ module Faraday
|
|
38
39
|
# end
|
39
40
|
class HttpCache < Faraday::Middleware
|
40
41
|
# Internal: valid options for the 'initialize' configuration Hash.
|
41
|
-
VALID_OPTIONS = [:store, :serializer, :logger, :
|
42
|
+
VALID_OPTIONS = [:store, :serializer, :logger, :shared_cache]
|
43
|
+
|
44
|
+
UNSAFE_METHODS = [:post, :put, :delete, :patch]
|
45
|
+
|
46
|
+
ERROR_STATUSES = 400..499
|
42
47
|
|
43
48
|
# Public: Initializes a new HttpCache middleware.
|
44
49
|
#
|
@@ -48,7 +53,6 @@ module Faraday
|
|
48
53
|
# :serializer - A serializer that should respond to 'dump' and 'load'.
|
49
54
|
# :shared_cache - A flag to mark the middleware as a shared cache or not.
|
50
55
|
# :store - A cache store that should respond to 'read' and 'write'.
|
51
|
-
# :store_options - Deprecated: additional options to setup the cache store.
|
52
56
|
#
|
53
57
|
# Examples:
|
54
58
|
#
|
@@ -65,20 +69,13 @@ module Faraday
|
|
65
69
|
# # Initialize the middleware with a MemoryStore and logger
|
66
70
|
# store = ActiveSupport::Cache.lookup_store
|
67
71
|
# Faraday::HttpCache.new(app, store: store, logger: my_logger)
|
68
|
-
def initialize(app,
|
72
|
+
def initialize(app, options = {})
|
69
73
|
super(app)
|
70
|
-
@logger = nil
|
71
|
-
@shared_cache = true
|
72
|
-
if args.first.is_a? Hash
|
73
|
-
options = args.first
|
74
|
-
@logger = options[:logger]
|
75
|
-
@shared_cache = options.fetch(:shared_cache, true)
|
76
|
-
else
|
77
|
-
options = parse_deprecated_options(*args)
|
78
|
-
end
|
79
|
-
|
80
74
|
assert_valid_options!(options)
|
81
|
-
|
75
|
+
|
76
|
+
@logger = options[:logger]
|
77
|
+
@shared_cache = options.fetch(:shared_cache, true)
|
78
|
+
@storage = create_storage(options)
|
82
79
|
end
|
83
80
|
|
84
81
|
# Public: Process the request into a duplicate of this instance to
|
@@ -103,86 +100,58 @@ module Faraday
|
|
103
100
|
|
104
101
|
response = nil
|
105
102
|
|
106
|
-
if
|
107
|
-
response =
|
103
|
+
if @request.cacheable?
|
104
|
+
response = if @request.no_cache?
|
105
|
+
trace :bypass
|
106
|
+
@app.call(env).on_complete do |fresh_env|
|
107
|
+
response = Response.new(create_response(fresh_env))
|
108
|
+
store(response)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
process(env)
|
112
|
+
end
|
108
113
|
else
|
109
114
|
trace :unacceptable
|
110
115
|
response = @app.call(env)
|
111
116
|
end
|
112
117
|
|
113
118
|
response.on_complete do
|
119
|
+
delete(@request, response) if should_delete?(response.status, @request.method)
|
114
120
|
log_request
|
115
121
|
end
|
116
122
|
end
|
117
123
|
|
118
|
-
|
119
|
-
# to the the definition in RFC 2616?
|
120
|
-
def shared_cache?
|
121
|
-
@shared_cache
|
122
|
-
end
|
124
|
+
protected
|
123
125
|
|
124
|
-
|
125
|
-
|
126
|
-
# and returns a Hash compatible with the new API
|
127
|
-
#
|
128
|
-
# Examples:
|
129
|
-
#
|
130
|
-
# parse_deprecated_options(Rails.cache)
|
131
|
-
# # => { store: Rails.cache }
|
132
|
-
#
|
133
|
-
# parse_deprecated_options(:mem_cache_store)
|
134
|
-
# # => { store: :mem_cache_store }
|
135
|
-
#
|
136
|
-
# parse_deprecated_options(:mem_cache_store, logger: Rails.logger)
|
137
|
-
# # => { store: :mem_cache_store, logger: Rails.logger }
|
138
|
-
#
|
139
|
-
# parse_deprecated_options(:mem_cache_store, 'localhost:11211')
|
140
|
-
# # => { store: :mem_cache_store, store_options: ['localhost:11211] }
|
141
|
-
#
|
142
|
-
# parse_deprecated_options(:mem_cache_store, logger: Rails.logger, serializer: Marshal)
|
143
|
-
# # => { store: :mem_cache_store, logger: Rails.logger, serializer: Marshal }
|
144
|
-
#
|
145
|
-
# parse_deprecated_options(serializer: Marshal)
|
146
|
-
# # => { serializer: Marshal }
|
147
|
-
#
|
148
|
-
# parse_deprecated_options(:file_store, { serializer: Marshal }, 'tmp')
|
149
|
-
# # => { store: :file_store, serializer: Marshal, store_options: ['tmp'] }
|
150
|
-
#
|
151
|
-
# parse_deprecated_options(:memory_store, size: 1024)
|
152
|
-
# # => { store: :memory_store, store_options: [size: 1024] }
|
153
|
-
#
|
154
|
-
# Returns a hash with the following keys:
|
155
|
-
# - store
|
156
|
-
# - serializer
|
157
|
-
# - logger
|
158
|
-
# - store_options
|
159
|
-
#
|
160
|
-
# In order to check what each key means, check `Storage#initialize` description.
|
161
|
-
def parse_deprecated_options(*args)
|
162
|
-
options = {}
|
163
|
-
if args.length > 0
|
164
|
-
Kernel.warn('DEPRECATION WARNING: This API is deprecated, refer to the documentation for the new one', caller)
|
165
|
-
end
|
126
|
+
# Internal: Gets the request object created from the Faraday env Hash.
|
127
|
+
attr_reader :request
|
166
128
|
|
167
|
-
|
129
|
+
# Internal: Gets the storage instance associated with the middleware.
|
130
|
+
attr_reader :storage
|
168
131
|
|
169
|
-
|
170
|
-
|
171
|
-
|
132
|
+
# Public: Creates the Storage instance for this middleware.
|
133
|
+
#
|
134
|
+
# options - A Hash of options.
|
135
|
+
#
|
136
|
+
# Returns a Storage instance.
|
137
|
+
def create_storage(options)
|
138
|
+
Storage.new(options)
|
139
|
+
end
|
172
140
|
|
173
|
-
|
174
|
-
@shared_cache = hash_params.fetch(:shared_cache, true)
|
175
|
-
end
|
141
|
+
private
|
176
142
|
|
177
|
-
|
178
|
-
|
143
|
+
# Internal: Should this cache instance act like a "shared cache" according
|
144
|
+
# to the the definition in RFC 2616?
|
145
|
+
def shared_cache?
|
146
|
+
@shared_cache
|
179
147
|
end
|
180
148
|
|
181
|
-
# Internal:
|
149
|
+
# Internal: Checks if the current request method should remove any existing
|
150
|
+
# cache entries for the same resource.
|
182
151
|
#
|
183
|
-
# Returns true
|
184
|
-
def
|
185
|
-
method
|
152
|
+
# Returns true or false.
|
153
|
+
def should_delete?(status, method)
|
154
|
+
UNSAFE_METHODS.include?(method) && !ERROR_STATUSES.cover?(status)
|
186
155
|
end
|
187
156
|
|
188
157
|
# Internal: Tries to locate a valid response or forwards the call to the stack.
|
@@ -266,6 +235,17 @@ module Faraday
|
|
266
235
|
end
|
267
236
|
end
|
268
237
|
|
238
|
+
def delete(request, response)
|
239
|
+
headers = %w(Location Content-Location)
|
240
|
+
headers.each do |header|
|
241
|
+
url = response.headers[header]
|
242
|
+
@storage.delete(url) if url
|
243
|
+
end
|
244
|
+
|
245
|
+
@storage.delete(request.url)
|
246
|
+
trace :delete
|
247
|
+
end
|
248
|
+
|
269
249
|
# Internal: Fetches the response from the Faraday stack and stores it.
|
270
250
|
#
|
271
251
|
# env - the environment 'Hash' from the Faraday stack.
|
@@ -295,20 +275,8 @@ module Faraday
|
|
295
275
|
}
|
296
276
|
end
|
297
277
|
|
298
|
-
# Internal: Creates a new 'Hash' containing the request information.
|
299
|
-
#
|
300
|
-
# env - the environment 'Hash' from the Faraday stack.
|
301
|
-
#
|
302
|
-
# Returns a 'Hash' containing the ':method', ':url' and 'request_headers'
|
303
|
-
# entries.
|
304
278
|
def create_request(env)
|
305
|
-
|
306
|
-
|
307
|
-
{
|
308
|
-
method: hash[:method],
|
309
|
-
url: hash[:url],
|
310
|
-
request_headers: hash[:request_headers].dup
|
311
|
-
}
|
279
|
+
Request.from_env(env)
|
312
280
|
end
|
313
281
|
|
314
282
|
# Internal: Logs the trace info about the incoming request
|
@@ -319,8 +287,8 @@ module Faraday
|
|
319
287
|
def log_request
|
320
288
|
return unless @logger
|
321
289
|
|
322
|
-
method = @request
|
323
|
-
path = @request
|
290
|
+
method = @request.method.to_s.upcase
|
291
|
+
path = @request.url.request_uri
|
324
292
|
line = "HTTP Cache: [#{method} #{path}] #{@trace.join(', ')}"
|
325
293
|
@logger.debug(line)
|
326
294
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Faraday
|
2
|
+
class HttpCache < Faraday::Middleware
|
3
|
+
# Internal: A class to represent a request
|
4
|
+
class Request
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def from_env(env)
|
8
|
+
hash = env.to_hash
|
9
|
+
new(method: hash[:method], url: hash[:url], headers: hash[:request_headers].dup)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :method, :url, :headers
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@method, @url, @headers = options[:method], options[:url], options[:headers]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Internal: Validates if the current request method is valid for caching.
|
20
|
+
#
|
21
|
+
# Returns true if the method is ':get' or ':head'.
|
22
|
+
def cacheable?
|
23
|
+
return false if method != :get && method != :head
|
24
|
+
return false if cache_control.no_store?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def no_cache?
|
29
|
+
cache_control.no_cache?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Internal: Gets the 'CacheControl' object.
|
33
|
+
def cache_control
|
34
|
+
@cache_control ||= CacheControl.new(headers['Cache-Control'])
|
35
|
+
end
|
36
|
+
|
37
|
+
def serializable_hash
|
38
|
+
{
|
39
|
+
method: @method,
|
40
|
+
url: @url,
|
41
|
+
headers: @headers
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -16,6 +16,9 @@ module Faraday
|
|
16
16
|
# # Creates a new Storage using Marshal for serialization.
|
17
17
|
# Faraday::HttpCache::Storage.new(:memory_store, serializer: Marshal)
|
18
18
|
class Storage
|
19
|
+
# Public: Gets the underlying cache store object.
|
20
|
+
attr_reader :cache
|
21
|
+
|
19
22
|
# Internal: Initialize a new Storage object with a cache backend.
|
20
23
|
#
|
21
24
|
# options - Storage options (default: {}).
|
@@ -24,96 +27,131 @@ module Faraday
|
|
24
27
|
# respond to 'dump' and 'load'.
|
25
28
|
# :serializer - A serializer object that should
|
26
29
|
# respond to 'dump' and 'load'.
|
27
|
-
# :store_options - An array containg the options for
|
28
|
-
# the cache store.
|
29
30
|
def initialize(options = {})
|
30
31
|
@cache = options[:store] || MemoryStore.new
|
31
32
|
@serializer = options[:serializer] || JSON
|
32
33
|
@logger = options[:logger]
|
33
|
-
if @cache.is_a? Symbol
|
34
|
-
@cache = lookup_store(@cache, options[:store_options])
|
35
|
-
end
|
36
34
|
assert_valid_store!
|
37
35
|
end
|
38
36
|
|
39
|
-
# Internal:
|
37
|
+
# Internal: Store a response inside the cache.
|
40
38
|
#
|
41
|
-
# request
|
42
|
-
#
|
43
|
-
# :url - The requested URL.
|
44
|
-
# :request_headers - The custom headers for the request.
|
39
|
+
# request - A Faraday::HttpCache::::Request instance of the executed HTTP
|
40
|
+
# request.
|
45
41
|
# response - The Faraday::HttpCache::Response instance to be stored.
|
42
|
+
#
|
43
|
+
# Returns nothing.
|
46
44
|
def write(request, response)
|
47
|
-
key = cache_key_for(request)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
45
|
+
key = cache_key_for(request.url)
|
46
|
+
entry = serialize_entry(request.serializable_hash, response.serializable_hash)
|
47
|
+
|
48
|
+
entries = cache.read(key) || []
|
49
|
+
|
50
|
+
entries.reject! do |(cached_request, cached_response)|
|
51
|
+
response_matches?(request, deserialize_object(cached_request), deserialize_object(cached_response))
|
53
52
|
end
|
54
|
-
|
53
|
+
|
54
|
+
entries << entry
|
55
|
+
|
56
|
+
cache.write(key, entries)
|
57
|
+
rescue Encoding::UndefinedConversionError => e
|
58
|
+
warn "Response could not be serialized: #{e.message}. Try using Marshal to serialize."
|
59
|
+
raise e
|
55
60
|
end
|
56
61
|
|
57
|
-
# Internal:
|
62
|
+
# Internal: Attempt to retrieve an stored response that suits the incoming
|
63
|
+
# HTTP request.
|
64
|
+
#
|
65
|
+
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
66
|
+
# request.
|
67
|
+
# klass - The Class to be instantiated with the stored response.
|
58
68
|
#
|
59
|
-
#
|
60
|
-
# :method - The HTTP Method used for the request.
|
61
|
-
# :url - The requested URL.
|
62
|
-
# :request_headers - The custom headers for the request.
|
63
|
-
# klass - The Class to be instantiated with the recovered informations.
|
69
|
+
# Returns an instance of 'klass'.
|
64
70
|
def read(request, klass = Faraday::HttpCache::Response)
|
65
|
-
cache_key = cache_key_for(request)
|
66
|
-
|
67
|
-
|
68
|
-
if found
|
69
|
-
payload = @serializer.load(found).each_with_object({}) do |(key,value), hash|
|
70
|
-
hash[key.to_sym] = value
|
71
|
-
end
|
71
|
+
cache_key = cache_key_for(request.url)
|
72
|
+
entries = cache.read(cache_key)
|
73
|
+
response = lookup_response(request, entries)
|
72
74
|
|
73
|
-
|
75
|
+
if response
|
76
|
+
klass.new(response)
|
74
77
|
end
|
75
78
|
end
|
76
79
|
|
80
|
+
def delete(url)
|
81
|
+
cache_key = cache_key_for(url)
|
82
|
+
cache.delete(cache_key)
|
83
|
+
end
|
84
|
+
|
77
85
|
private
|
78
86
|
|
79
|
-
# Internal:
|
87
|
+
# Internal: Retrieve a response Hash from the list of entries that match
|
88
|
+
# the given request.
|
80
89
|
#
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
+
# request - A Faraday::HttpCache::::Request instance of the incoming HTTP
|
91
|
+
# request.
|
92
|
+
# entries - An Array of pairs of Hashes (request, response).
|
93
|
+
#
|
94
|
+
# Returns a Hash or nil.
|
95
|
+
def lookup_response(request, entries)
|
96
|
+
if entries
|
97
|
+
entries = entries.map { |entry| deserialize_entry(*entry) }
|
98
|
+
_, response = entries.find { |req, res| response_matches?(request, req, res) }
|
99
|
+
response
|
90
100
|
end
|
91
|
-
digest.update 'url'
|
92
|
-
digest.update request[:url].to_s
|
93
|
-
|
94
|
-
digest.to_s
|
95
101
|
end
|
96
102
|
|
97
|
-
# Internal:
|
103
|
+
# Internal: Check if a cached response and request matches the given
|
104
|
+
# request.
|
98
105
|
#
|
99
|
-
#
|
100
|
-
#
|
106
|
+
# request - A Faraday::HttpCache::::Request instance of the
|
107
|
+
# current HTTP request.
|
108
|
+
# cached_request - The Hash of the request that was cached.
|
109
|
+
# cached_response - The Hash of the response that was cached.
|
101
110
|
#
|
102
|
-
# Returns
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
111
|
+
# Returns true or false.
|
112
|
+
def response_matches?(request, cached_request, cached_response)
|
113
|
+
request.method.to_s == cached_request[:method] &&
|
114
|
+
vary_matches?(cached_response, request, cached_request)
|
115
|
+
end
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
117
|
+
def vary_matches?(cached_response, request, cached_request)
|
118
|
+
headers = Faraday::Utils::Headers.new(cached_response[:response_headers])
|
119
|
+
vary = headers['Vary'].to_s
|
120
|
+
|
121
|
+
vary.empty? || (vary != '*' && vary.split(/[\s,]+/).all? do |header|
|
122
|
+
request.headers[header] == cached_request[:headers][header]
|
123
|
+
end)
|
124
|
+
end
|
125
|
+
|
126
|
+
def serialize_entry(*objects)
|
127
|
+
objects.map { |object| serialize_object(object) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def serialize_object(object)
|
131
|
+
@serializer.dump(object)
|
132
|
+
end
|
133
|
+
|
134
|
+
def deserialize_entry(*objects)
|
135
|
+
objects.map { |object| deserialize_object(object) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def deserialize_object(object)
|
139
|
+
@serializer.load(object).each_with_object({}) do |(key, value), hash|
|
140
|
+
hash[key.to_sym] = value
|
114
141
|
end
|
115
142
|
end
|
116
143
|
|
144
|
+
# Internal: Computes the cache key for a specific request, taking in
|
145
|
+
# account the current serializer to avoid cross serialization issues.
|
146
|
+
#
|
147
|
+
# url - The request URL.
|
148
|
+
#
|
149
|
+
# Returns a String.
|
150
|
+
def cache_key_for(url)
|
151
|
+
prefix = (@serializer.is_a?(Module) ? @serializer : @serializer.class).name
|
152
|
+
Digest::SHA1.hexdigest("#{prefix}#{url}")
|
153
|
+
end
|
154
|
+
|
117
155
|
# Internal: Checks if the given cache object supports the
|
118
156
|
# expect API ('read' and 'write').
|
119
157
|
#
|
@@ -121,10 +159,14 @@ module Faraday
|
|
121
159
|
#
|
122
160
|
# Returns nothing.
|
123
161
|
def assert_valid_store!
|
124
|
-
unless
|
125
|
-
raise ArgumentError.new("#{
|
162
|
+
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:delete)
|
163
|
+
raise ArgumentError.new("#{cache.inspect} is not a valid cache store as it does not responds to 'read', 'write' or 'delete'.")
|
126
164
|
end
|
127
165
|
end
|
166
|
+
|
167
|
+
def warn(message)
|
168
|
+
@logger.warn(message) if @logger
|
169
|
+
end
|
128
170
|
end
|
129
171
|
|
130
172
|
# Internal: A Hash based store to be used by the 'Storage' class
|
@@ -138,6 +180,10 @@ module Faraday
|
|
138
180
|
@cache[key]
|
139
181
|
end
|
140
182
|
|
183
|
+
def delete(key)
|
184
|
+
@cache.delete(key)
|
185
|
+
end
|
186
|
+
|
141
187
|
def write(key, value)
|
142
188
|
@cache[key] = value
|
143
189
|
end
|
@@ -23,7 +23,7 @@ describe Faraday::HttpCache do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'logs that a POST request is unacceptable' do
|
26
|
-
expect(logger).to receive(:debug).with('HTTP Cache: [POST /post] unacceptable')
|
26
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [POST /post] unacceptable, delete')
|
27
27
|
client.post('post').body
|
28
28
|
end
|
29
29
|
|
@@ -32,9 +32,73 @@ describe Faraday::HttpCache do
|
|
32
32
|
expect(client.get('broken').body).to eq('2')
|
33
33
|
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
describe 'cache invalidation' do
|
36
|
+
it 'expires POST requests' do
|
37
|
+
client.get('counter')
|
38
|
+
client.post('counter')
|
39
|
+
expect(client.get('counter').body).to eq('2')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'logs that a POST request was deleted from the cache' do
|
43
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [POST /counter] unacceptable, delete')
|
44
|
+
client.post('counter')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'does not expires POST requests that failed' do
|
48
|
+
client.get('get')
|
49
|
+
client.post('get')
|
50
|
+
expect(client.get('get').body).to eq('1')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'expires PUT requests' do
|
54
|
+
client.get('counter')
|
55
|
+
client.put('counter')
|
56
|
+
expect(client.get('counter').body).to eq('2')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'logs that a PUT request was deleted from the cache' do
|
60
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [PUT /counter] unacceptable, delete')
|
61
|
+
client.put('counter')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'expires DELETE requests' do
|
65
|
+
client.get('counter')
|
66
|
+
client.delete('counter')
|
67
|
+
expect(client.get('counter').body).to eq('2')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'logs that a DELETE request was deleted from the cache' do
|
71
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [DELETE /counter] unacceptable, delete')
|
72
|
+
client.delete('counter')
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'expires PATCH requests' do
|
76
|
+
client.get('counter')
|
77
|
+
client.patch('counter')
|
78
|
+
expect(client.get('counter').body).to eq('2')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'logs that a PATCH request was deleted from the cache' do
|
82
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [PATCH /counter] unacceptable, delete')
|
83
|
+
client.patch('counter')
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'logs that a response with a bad status code is invalid' do
|
87
|
+
expect(logger).to receive(:debug).with('HTTP Cache: [GET /broken] miss, invalid')
|
88
|
+
client.get('broken')
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'expires entries for the "Location" header' do
|
92
|
+
client.get('get')
|
93
|
+
client.post('delete-with-location')
|
94
|
+
expect(client.get('get').body).to eq('2')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'expires entries for the "Content-Location" header' do
|
98
|
+
client.get('get')
|
99
|
+
client.post('delete-with-content-location')
|
100
|
+
expect(client.get('get').body).to eq('2')
|
101
|
+
end
|
38
102
|
end
|
39
103
|
|
40
104
|
describe 'when acting as a shared cache' do
|
@@ -65,7 +129,7 @@ describe Faraday::HttpCache do
|
|
65
129
|
end
|
66
130
|
end
|
67
131
|
|
68
|
-
it 'does not cache
|
132
|
+
it 'does not cache responses with a explicit no-store directive' do
|
69
133
|
client.get('dontstore')
|
70
134
|
expect(client.get('dontstore').body).to eq('2')
|
71
135
|
end
|
@@ -75,10 +139,22 @@ describe Faraday::HttpCache do
|
|
75
139
|
client.get('dontstore')
|
76
140
|
end
|
77
141
|
|
78
|
-
it 'caches multiple responses when the headers differ' do
|
142
|
+
it 'does not caches multiple responses when the headers differ' do
|
79
143
|
client.get('get', nil, 'HTTP_ACCEPT' => 'text/html')
|
80
144
|
expect(client.get('get', nil, 'HTTP_ACCEPT' => 'text/html').body).to eq('1')
|
81
|
-
expect(client.get('get', nil, 'HTTP_ACCEPT' => 'application/json').body).to eq('
|
145
|
+
expect(client.get('get', nil, 'HTTP_ACCEPT' => 'application/json').body).to eq('1')
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'caches multiples responses based on the "Vary" header' do
|
149
|
+
client.get('vary', nil, 'User-Agent' => 'Agent/1.0')
|
150
|
+
expect(client.get('vary', nil, 'User-Agent' => 'Agent/1.0').body).to eq('1')
|
151
|
+
expect(client.get('vary', nil, 'User-Agent' => 'Agent/2.0').body).to eq('2')
|
152
|
+
expect(client.get('vary', nil, 'User-Agent' => 'Agent/3.0').body).to eq('3')
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'never caches responses with the wildcard "Vary" header' do
|
156
|
+
client.get('vary-wildcard')
|
157
|
+
expect(client.get('vary-wildcard').body).to eq('2')
|
82
158
|
end
|
83
159
|
|
84
160
|
it 'caches requests with the "Expires" header' do
|
@@ -96,6 +172,18 @@ describe Faraday::HttpCache do
|
|
96
172
|
expect(client.get('get').body).to eq('1')
|
97
173
|
end
|
98
174
|
|
175
|
+
context 'when the request has a "no-cache" directive' do
|
176
|
+
it 'by-passes the cache' do
|
177
|
+
client.get('get', nil, 'Cache-Control' => 'no-cache')
|
178
|
+
expect(client.get('get', nil, 'Cache-Control' => 'no-cache').body).to eq('2')
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'caches the response' do
|
182
|
+
client.get('get', nil, 'Cache-Control' => 'no-cache')
|
183
|
+
expect(client.get('get', nil).body).to eq('1')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
99
187
|
it 'logs that a GET response is stored' do
|
100
188
|
expect(logger).to receive(:debug).with('HTTP Cache: [GET /get] miss, store')
|
101
189
|
client.get('get')
|
@@ -190,51 +278,5 @@ describe Faraday::HttpCache do
|
|
190
278
|
expect(Faraday::HttpCache::Storage).to receive(:new).with(store: store)
|
191
279
|
Faraday::HttpCache.new(app, store: store)
|
192
280
|
end
|
193
|
-
|
194
|
-
it 'accepts a Hash option' do
|
195
|
-
expect(ActiveSupport::Cache).to receive(:lookup_store).with(:memory_store, [{ size: 1024 }]).and_call_original
|
196
|
-
Faraday::HttpCache.new(app, store: :memory_store, store_options: [size: 1024])
|
197
|
-
end
|
198
|
-
|
199
|
-
it 'consumes the "logger" key' do
|
200
|
-
expect(ActiveSupport::Cache).to receive(:lookup_store).with(:memory_store, nil).and_call_original
|
201
|
-
Faraday::HttpCache.new(app, store: :memory_store, logger: logger)
|
202
|
-
end
|
203
|
-
|
204
|
-
describe '#shared_cache?' do
|
205
|
-
it 'is true by default' do
|
206
|
-
expect(Faraday::HttpCache.new(app).shared_cache?).to eq(true)
|
207
|
-
end
|
208
|
-
|
209
|
-
it 'is true when configured to true' do
|
210
|
-
expect(Faraday::HttpCache.new(app, shared_cache: true).shared_cache?).to eq(true)
|
211
|
-
end
|
212
|
-
|
213
|
-
it 'is false when configured to be false' do
|
214
|
-
expect(Faraday::HttpCache.new(app, shared_cache: false).shared_cache?).to eq(false)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
context 'with deprecated options format' do
|
219
|
-
before do
|
220
|
-
allow(Kernel).to receive(:warn)
|
221
|
-
end
|
222
|
-
|
223
|
-
it 'uses the options to create a Cache Store' do
|
224
|
-
expect(ActiveSupport::Cache).to receive(:lookup_store).with(:file_store, ['tmp']).and_call_original
|
225
|
-
Faraday::HttpCache.new(app, :file_store, 'tmp')
|
226
|
-
end
|
227
|
-
|
228
|
-
it 'accepts a Hash option' do
|
229
|
-
expect(ActiveSupport::Cache).to receive(:lookup_store).with(:memory_store, [{ size: 1024 }]).and_call_original
|
230
|
-
Faraday::HttpCache.new(app, :memory_store, size: 1024)
|
231
|
-
end
|
232
|
-
|
233
|
-
it 'warns the user about the deprecated options' do
|
234
|
-
expect(Kernel).to receive(:warn)
|
235
|
-
|
236
|
-
Faraday::HttpCache.new(app, :memory_store, logger: logger)
|
237
|
-
end
|
238
|
-
end
|
239
281
|
end
|
240
282
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Faraday::HttpCache::Request do
|
4
|
+
subject { Faraday::HttpCache::Request.new method: method, url: url, headers: headers }
|
5
|
+
let(:method) { :get }
|
6
|
+
let(:url) { URI.parse('http://example.com/path/to/somewhere') }
|
7
|
+
let(:headers) { {} }
|
8
|
+
|
9
|
+
context 'a GET request' do
|
10
|
+
it { should be_cacheable }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'a HEAD request' do
|
14
|
+
let(:method) { :head }
|
15
|
+
it { should be_cacheable }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'a POST request' do
|
19
|
+
let(:method) { :post }
|
20
|
+
it { should_not be_cacheable }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'a PUT request' do
|
24
|
+
let(:method) { :put }
|
25
|
+
it { should_not be_cacheable }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'an OPTIONS request' do
|
29
|
+
let(:method) { :options }
|
30
|
+
it { should_not be_cacheable }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'a DELETE request' do
|
34
|
+
let(:method) { :delete }
|
35
|
+
it { should_not be_cacheable }
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'a TRACE request' do
|
39
|
+
let(:method) { :trace }
|
40
|
+
it { should_not be_cacheable }
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with "Cache-Control: no-store"' do
|
44
|
+
let(:headers) { { 'Cache-Control' => 'no-store' } }
|
45
|
+
it { should_not be_cacheable }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/spec/storage_spec.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Faraday::HttpCache::Storage do
|
4
|
+
let(:cache_key) { '6e3b941d0f7572291c777b3e48c04b74124a55d0' }
|
4
5
|
let(:request) do
|
5
|
-
{ method: :get,
|
6
|
+
env = { method: :get, url: 'http://test/index' }
|
7
|
+
double(env.merge(serializable_hash: env))
|
6
8
|
end
|
7
9
|
|
8
|
-
let(:response) { double(serializable_hash: {}) }
|
10
|
+
let(:response) { double(serializable_hash: { response_headers: {} }) }
|
9
11
|
|
10
|
-
let(:cache) {
|
12
|
+
let(:cache) { Faraday::HttpCache::MemoryStore.new }
|
11
13
|
|
12
14
|
let(:storage) { Faraday::HttpCache::Storage.new(store: cache) }
|
13
15
|
subject { storage }
|
@@ -18,17 +20,6 @@ describe Faraday::HttpCache::Storage do
|
|
18
20
|
Faraday::HttpCache::Storage.new
|
19
21
|
end
|
20
22
|
|
21
|
-
it 'lookups an ActiveSupport cache store if a Symbol is given' do
|
22
|
-
expect(ActiveSupport::Cache).to receive(:lookup_store).with(:file_store, ['/tmp']).and_call_original
|
23
|
-
Faraday::HttpCache::Storage.new(store: :file_store, store_options: ['/tmp'])
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'emits a warning when doing the lookup of an ActiveSupport cache store' do
|
27
|
-
logger = double
|
28
|
-
expect(logger).to receive(:warn).with(/Passing a Symbol as the 'store' is deprecated/)
|
29
|
-
Faraday::HttpCache::Storage.new(store: :file_store, logger: logger)
|
30
|
-
end
|
31
|
-
|
32
23
|
it 'raises an error when the given store is not valid' do
|
33
24
|
wrong = double
|
34
25
|
|
@@ -39,20 +30,19 @@ describe Faraday::HttpCache::Storage do
|
|
39
30
|
end
|
40
31
|
|
41
32
|
describe 'storing responses' do
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
expect(cache).to receive(:write).with(cache_key,
|
33
|
+
shared_examples 'A storage with serialization' do
|
34
|
+
it 'writes the response object to the underlying cache' do
|
35
|
+
entry = [serializer.dump(request.serializable_hash), serializer.dump(response.serializable_hash)]
|
36
|
+
expect(cache).to receive(:write).with(cache_key, [entry])
|
46
37
|
subject.write(request, response)
|
47
38
|
end
|
48
39
|
end
|
49
40
|
|
50
|
-
context 'with
|
51
|
-
let(:
|
52
|
-
|
53
|
-
it_behaves_like 'serialization'
|
41
|
+
context 'with the JSON serializer' do
|
42
|
+
let(:serializer) { JSON }
|
43
|
+
it_behaves_like 'A storage with serialization'
|
54
44
|
|
55
|
-
context '
|
45
|
+
context 'when ASCII characters in response cannot be converted to UTF-8' do
|
56
46
|
let(:response) do
|
57
47
|
body = "\u2665".force_encoding('ASCII-8BIT')
|
58
48
|
double(:response, serializable_hash: { 'body' => body })
|
@@ -70,22 +60,12 @@ describe Faraday::HttpCache::Storage do
|
|
70
60
|
end
|
71
61
|
end
|
72
62
|
|
73
|
-
context 'with Marshal serializer' do
|
74
|
-
let(:
|
75
|
-
let(:
|
76
|
-
let(:
|
77
|
-
|
78
|
-
it_behaves_like 'serialization'
|
79
|
-
|
80
|
-
it 'should have a unique cache key' do
|
81
|
-
request = { method: :get, request_headers: {}, url: URI.parse('http://foo.bar/path/to/somewhere') }
|
82
|
-
duplicate_request = { method: :get, request_headers: {}, url: URI.parse('http://foo.bar/path/to/somewhere') }
|
83
|
-
storage = Faraday::HttpCache::Storage.new(serializer: Marshal)
|
84
|
-
response = Faraday::HttpCache::Response.new(status: 200, body: 'body')
|
85
|
-
storage.write(request, response)
|
86
|
-
read_response = storage.read(duplicate_request).serializable_hash
|
87
|
-
expect(read_response).to eq(response.serializable_hash)
|
88
|
-
end
|
63
|
+
context 'with the Marshal serializer' do
|
64
|
+
let(:cache_key) { '337d1e9c6c92423dd1c48a23054139058f97be40' }
|
65
|
+
let(:serializer) { Marshal }
|
66
|
+
let(:storage) { Faraday::HttpCache::Storage.new(store: cache, serializer: Marshal) }
|
67
|
+
|
68
|
+
it_behaves_like 'A storage with serialization'
|
89
69
|
end
|
90
70
|
end
|
91
71
|
|
@@ -101,6 +81,14 @@ describe Faraday::HttpCache::Storage do
|
|
101
81
|
end
|
102
82
|
end
|
103
83
|
|
84
|
+
describe 'deleting responses' do
|
85
|
+
it 'removes the entries from the cache of the given URL' do
|
86
|
+
subject.write(request, response)
|
87
|
+
subject.delete(request.url)
|
88
|
+
expect(subject.read(request)).to be_nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
104
92
|
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
105
93
|
it 'is fresh if the response still has some time to live' do
|
106
94
|
headers = {
|
data/spec/support/test_app.rb
CHANGED
@@ -40,10 +40,38 @@ class TestApp < Sinatra::Base
|
|
40
40
|
[500, { 'Cache-Control' => 'max-age=400' }, increment_counter]
|
41
41
|
end
|
42
42
|
|
43
|
+
get '/counter' do
|
44
|
+
[200, { 'Cache-Control' => 'max-age=200' }, increment_counter]
|
45
|
+
end
|
46
|
+
|
47
|
+
post '/counter' do
|
48
|
+
end
|
49
|
+
|
50
|
+
put '/counter' do
|
51
|
+
end
|
52
|
+
|
53
|
+
delete '/counter' do
|
54
|
+
end
|
55
|
+
|
56
|
+
patch '/counter' do
|
57
|
+
end
|
58
|
+
|
43
59
|
get '/get' do
|
44
60
|
[200, { 'Cache-Control' => 'max-age=200' }, increment_counter]
|
45
61
|
end
|
46
62
|
|
63
|
+
post '/delete-with-location' do
|
64
|
+
[200, { 'Location' => "#{request.base_url}/get" }, '']
|
65
|
+
end
|
66
|
+
|
67
|
+
post '/delete-with-content-location' do
|
68
|
+
[200, { 'Content-Location' => "#{request.base_url}/get" }, '']
|
69
|
+
end
|
70
|
+
|
71
|
+
post '/get' do
|
72
|
+
halt 405
|
73
|
+
end
|
74
|
+
|
47
75
|
get '/private' do
|
48
76
|
[200, { 'Cache-Control' => 'private, max-age=100' }, increment_counter]
|
49
77
|
end
|
@@ -82,6 +110,14 @@ class TestApp < Sinatra::Base
|
|
82
110
|
end
|
83
111
|
end
|
84
112
|
|
113
|
+
get '/vary' do
|
114
|
+
[200, { 'Cache-Control' => 'max-age=50', 'Vary' => 'User-Agent' }, increment_counter]
|
115
|
+
end
|
116
|
+
|
117
|
+
get '/vary-wildcard' do
|
118
|
+
[200, { 'Cache-Control' => 'max-age=50', 'Vary' => '*' }, increment_counter]
|
119
|
+
end
|
120
|
+
|
85
121
|
# Increments the 'requests' counter to act as a newly processed response.
|
86
122
|
def increment_counter
|
87
123
|
(settings.requests += 1).to_s
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faraday-http-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lucas Mazza
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -36,12 +36,14 @@ files:
|
|
36
36
|
- lib/faraday-http-cache.rb
|
37
37
|
- lib/faraday/http_cache.rb
|
38
38
|
- lib/faraday/http_cache/cache_control.rb
|
39
|
+
- lib/faraday/http_cache/request.rb
|
39
40
|
- lib/faraday/http_cache/response.rb
|
40
41
|
- lib/faraday/http_cache/storage.rb
|
41
42
|
- spec/binary_spec.rb
|
42
43
|
- spec/cache_control_spec.rb
|
44
|
+
- spec/http_cache_spec.rb
|
43
45
|
- spec/json_spec.rb
|
44
|
-
- spec/
|
46
|
+
- spec/request_spec.rb
|
45
47
|
- spec/response_spec.rb
|
46
48
|
- spec/spec_helper.rb
|
47
49
|
- spec/storage_spec.rb
|
@@ -68,15 +70,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
70
|
version: '0'
|
69
71
|
requirements: []
|
70
72
|
rubyforge_project:
|
71
|
-
rubygems_version: 2.
|
73
|
+
rubygems_version: 2.4.5
|
72
74
|
signing_key:
|
73
75
|
specification_version: 4
|
74
76
|
summary: A Faraday middleware that stores and validates cache expiration.
|
75
77
|
test_files:
|
76
78
|
- spec/binary_spec.rb
|
77
79
|
- spec/cache_control_spec.rb
|
80
|
+
- spec/http_cache_spec.rb
|
78
81
|
- spec/json_spec.rb
|
79
|
-
- spec/
|
82
|
+
- spec/request_spec.rb
|
80
83
|
- spec/response_spec.rb
|
81
84
|
- spec/spec_helper.rb
|
82
85
|
- spec/storage_spec.rb
|