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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 63e018cc1939d1fefb02b7d90f810a71afdd15b9
4
- data.tar.gz: 408cdbabae9f4a6d144b0c799341bf4c7a46adea
3
+ metadata.gz: f34a3a6ee332a8ce2480f41e1071020237c0ae1e
4
+ data.tar.gz: e1fc2519d29fc6656e156ffa4d9bf722e8b17297
5
5
  SHA512:
6
- metadata.gz: 5247068c970b755688f710a557a4d6e6366cbb909c303f16a67e1cb1814240e6a709b4b7bba3d75048c31b0c716b36ce34b4b85152455e9d13e299e92be08ead
7
- data.tar.gz: bfe06e86bdd004958ac7ca444c346f3fd5060fa8d61e54e66075464c215afb3bca14e7c5ed10714bd78ebc79a77e7aab7bdb2326b1e919e3b442d7980ff5aabe
6
+ metadata.gz: aa4045303477608539cc6f844daf4ebade16ac4e3e4ba7ecc5c11a4f9f096c4895893f773708607826dc1428b50814cf5e6e1298330e36f6e5c4f3cc8d5a817c
7
+ data.tar.gz: f56ed4f205684f471a39fd471b107e3fb417ffcdb945af2c67ad5d018958bbc93873076e0eeab0b97ac6f4984243aba5d27bf73246480c0a1e675e4009bfbc8c
@@ -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, :store_options, :shared_cache]
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, *args)
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
- @storage = Storage.new(options)
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 can_cache?(@request[:method])
107
- response = process(env)
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
- # Internal: Should this cache instance act like a "shared cache" according
119
- # to the the definition in RFC 2616?
120
- def shared_cache?
121
- @shared_cache
122
- end
124
+ protected
123
125
 
124
- private
125
- # Internal: Receive the deprecated arguments to initialize the old API
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
- options[:store] = args.shift
129
+ # Internal: Gets the storage instance associated with the middleware.
130
+ attr_reader :storage
168
131
 
169
- if args.first.is_a? Hash
170
- hash_params = args.first
171
- options[:serializer] = hash_params.delete(:serializer)
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
- @logger = hash_params[:logger]
174
- @shared_cache = hash_params.fetch(:shared_cache, true)
175
- end
141
+ private
176
142
 
177
- options[:store_options] = args
178
- options
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: Validates if the current request method is valid for caching.
149
+ # Internal: Checks if the current request method should remove any existing
150
+ # cache entries for the same resource.
182
151
  #
183
- # Returns true if the method is ':get' or ':head'.
184
- def can_cache?(method)
185
- method == :get || method == :head
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
- hash = env.to_hash
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[:method].to_s.upcase
323
- path = @request[:url].request_uri
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: Writes a response with a key based on the given request.
37
+ # Internal: Store a response inside the cache.
40
38
  #
41
- # request - The Hash containing the request information.
42
- # :method - The HTTP Method used for the request.
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
- value = @serializer.dump(response.serializable_hash)
49
- @cache.write(key, value)
50
- rescue Encoding::UndefinedConversionError => e
51
- if @logger
52
- @logger.warn("Response could not be serialized: #{e.message}. Try using Marshal to serialize.")
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
- raise
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: Reads a key based on the given request from the underlying cache.
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
- # request - The Hash containing the request information.
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
- found = @cache.read(cache_key)
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
- klass.new(payload)
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: Generates a String key for a given request object.
87
+ # Internal: Retrieve a response Hash from the list of entries that match
88
+ # the given request.
80
89
  #
81
- # Returns the digested String.
82
- def cache_key_for(request)
83
- digest = Digest::SHA1.new
84
- digest.update 'method'
85
- digest.update request[:method].to_s
86
- digest.update 'request_headers'
87
- request[:request_headers].keys.sort.each do |key|
88
- digest.update key.to_s
89
- digest.update request[:request_headers][key].to_s
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: Creates a cache store from 'ActiveSupport' with a set of options.
103
+ # Internal: Check if a cached response and request matches the given
104
+ # request.
98
105
  #
99
- # store - A 'Symbol' with the store name.
100
- # options - Additional options for the cache store.
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 an 'ActiveSupport::Cache' store.
103
- def lookup_store(store, options)
104
- if @logger
105
- @logger.warn "Passing a Symbol as the 'store' is deprecated, please pass the cache store instead."
106
- end
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
- begin
109
- require 'active_support/cache'
110
- ActiveSupport::Cache.lookup_store(store, options)
111
- rescue LoadError => e
112
- puts "You're missing the 'activesupport' gem. Add it to your Gemfile, bundle it and try again"
113
- raise e
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 @cache.respond_to?(:read) && @cache.respond_to?(:write)
125
- raise ArgumentError.new("#{@cache.inspect} is not a valid cache store as it does not responds to 'read' and 'write'.")
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
- it 'logs that a response with a bad status code is invalid' do
36
- expect(logger).to receive(:debug).with('HTTP Cache: [GET /broken] miss, invalid')
37
- client.get('broken')
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 requests with a explicit no-store directive' do
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('2')
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, request_headers: {}, url: URI.parse('http://foo.bar/') }
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) { ActiveSupport::Cache.lookup_store }
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
- shared_examples 'serialization' do
44
- it 'writes the response json to the underlying cache using a digest as the key' do
45
- expect(cache).to receive(:write).with(cache_key, serialized)
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 default serializer' do
51
- let(:serialized) { JSON.dump(response.serializable_hash) }
52
- let(:cache_key) { '084dd517af7651a9ca7823728544b9b55e0cc130' }
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 'with ASCII character in response that cannot be converted to UTF-8' do
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(:storage) { Faraday::HttpCache::Storage.new store: cache, serializer: Marshal }
75
- let(:serialized) { Marshal.dump(response.serializable_hash) }
76
- let(:cache_key) { '084dd517af7651a9ca7823728544b9b55e0cc130' }
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 = {
@@ -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.2
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: 2014-08-17 00:00:00.000000000 Z
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/middleware_spec.rb
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.2.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/middleware_spec.rb
82
+ - spec/request_spec.rb
80
83
  - spec/response_spec.rb
81
84
  - spec/spec_helper.rb
82
85
  - spec/storage_spec.rb