faraday-http-cache 0.4.2 → 1.0.0

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