builder-rails_cache 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/README.md +321 -0
- data/Rakefile +8 -0
- data/builder-rails_cache.gemspec +43 -0
- data/lib/builder/rails_cache/version.rb +7 -0
- data/lib/builder/rails_cache.rb +65 -0
- data/sig/builder/rails_cache.rbs +6 -0
- data/spec/shared_examples/cached_method.rb +133 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c4aa749a4640d7ca3fe3729890999c3c51fe0bd1b273cde488367410c9496acf
|
4
|
+
data.tar.gz: a117fcaef9eaa8b354b3d047585d43af8e0261c4bddf66c903f735cfa2ec9f44
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba317a626c14dda2d79925cee3757ce47ead9ec88a5ecb1bfff034c65e72be0619f232d552d79f1d819cf0ce1900c716e957faaa0ef3141c78273e5b53b1fd8d
|
7
|
+
data.tar.gz: ec9cca1b5820d6e03ab90d8d0903f44494c50c193c79d95d6aebf66cf79d34e284d9ee56dd16452f8d57ff23cb1eb8950ef695ab577d1723d639391be1ba838e
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
# Builder::RailsCache
|
2
|
+
|
3
|
+
Provides a convenient `with_cache do ... end` wrapper around caching Rails responses, and shared_examples for use in your controller tests.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
def index
|
7
|
+
# Cache the response across all users
|
8
|
+
json = with_cache do
|
9
|
+
# this code will only be called
|
10
|
+
# if the response is already cached,
|
11
|
+
# and the cache entry has not expired
|
12
|
+
@records = MyModelClass.where(....)
|
13
|
+
# The return value of the block will be
|
14
|
+
# what gets stored in the cache, so it's
|
15
|
+
# simplest to just return the generated JSON
|
16
|
+
@records.to_json
|
17
|
+
end
|
18
|
+
# render whatever we got back from the cache if it was a hit,
|
19
|
+
# or whatever the block generated if it was a cache miss
|
20
|
+
render json: json, status: :ok
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
1. Add the gem to your Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'builder-rails_cache'
|
30
|
+
```
|
31
|
+
|
32
|
+
2. Install the gem
|
33
|
+
|
34
|
+
```bash
|
35
|
+
> bundle
|
36
|
+
```
|
37
|
+
|
38
|
+
_NOTE: future versions will automate as much of the following steps as possible - but for now, this must be a manual process_
|
39
|
+
|
40
|
+
### Installing the gem into your application
|
41
|
+
|
42
|
+
1. Include the module in your `ApplicationController`
|
43
|
+
|
44
|
+
(this can either be the global `BuilderBase::ApplicationController` or just for one specific module if you prefer):
|
45
|
+
|
46
|
+
```
|
47
|
+
class ApplicationController < ActionController::Base
|
48
|
+
include Builder::RailsCache
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
2. Enable caching locally in development mode, if `RAILS_CACHE_STORE` is given
|
53
|
+
|
54
|
+
In `config/environments/development.rb`, you should see a block something like this, usually around line 18 or so:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# Enable/disable caching. By default caching is disabled.
|
58
|
+
# Run rails dev:cache to toggle caching.
|
59
|
+
if Rails.root.join('tmp', 'caching-dev.txt').exist?
|
60
|
+
config.cache_store = :memory_store
|
61
|
+
config.public_file_server.headers = {
|
62
|
+
'Cache-Control' => "public, max-age=#{2.days.to_i}"
|
63
|
+
}
|
64
|
+
else
|
65
|
+
config.action_controller.perform_caching = false
|
66
|
+
config.cache_store = :null_store
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
Add another `elsif` clause to that block, so that it reads:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# Enable/disable caching. By default caching is disabled.
|
74
|
+
# Run rails dev:cache to toggle caching.
|
75
|
+
if Rails.root.join('tmp', 'caching-dev.txt').exist?
|
76
|
+
config.cache_store = :memory_store
|
77
|
+
config.public_file_server.headers = {
|
78
|
+
'Cache-Control' => "public, max-age=#{2.days.to_i}"
|
79
|
+
}
|
80
|
+
# NEW LINES START HERE ----------------
|
81
|
+
elsif ENV['RAILS_CACHE_STORE'].present?
|
82
|
+
config.cache_store = ENV['RAILS_CACHE_STORE'].to_sym
|
83
|
+
# /END OF NEW LINES -------------------
|
84
|
+
else
|
85
|
+
config.action_controller.perform_caching = false
|
86
|
+
config.cache_store = :null_store
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
3. Enable caching in production mode, if `RAILS_CACHE_STORE` is given
|
91
|
+
|
92
|
+
Note: this code will enable caching in all deployed environments (dev, stage, uat, prod etc),
|
93
|
+
_but only if the RAILS_CACHE_STORE environment variable is present_ - so if you're cautious,
|
94
|
+
you can push the code first, and then do a separate MR to provide the config which actually enables the cache
|
95
|
+
in each environment, making it a simple job to revert it if anything goes wrong.
|
96
|
+
|
97
|
+
Add these lines to `config/environment/production.rb`, anywhere _inside_ the `Rails.application.configure do ... end` block:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
if ENV['RAILS_CACHE_STORE'].present?
|
101
|
+
config.cache_store = ENV['RAILS_CACHE_STORE'].to_sym
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
4. Include the `Builder::RailsCache` module in your ApplicationController:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
module BuilderBase
|
109
|
+
class ApplicationController
|
110
|
+
include Builder::RailsCache
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Usage
|
116
|
+
|
117
|
+
### Adding response-caching to a controller method
|
118
|
+
|
119
|
+
For example, assume your controller method looks like this:
|
120
|
+
|
121
|
+
```
|
122
|
+
def index
|
123
|
+
# Slow query:
|
124
|
+
@records = MyModelClass.where(....)
|
125
|
+
# maybe some other code is here too
|
126
|
+
|
127
|
+
# ultimately a response is rendered as JSON:
|
128
|
+
render json: @records.as_json, status: :ok
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
You can add a `with_cache` block around everything involved in generating the JSON - constructing
|
133
|
+
queries, retrieving the data, calling any serializers, etc.
|
134
|
+
|
135
|
+
*NOTE* - The Rails cache ultimately stores _strings_ in Redis (or whatever other cache store you my have configured)
|
136
|
+
So you'll need to split the _conversion of the result to_ JSON, from the _rendering_ of the result back to
|
137
|
+
the caller, like this:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
def index
|
141
|
+
json = with_cache do
|
142
|
+
@records = MyModelClass.where(....)
|
143
|
+
@records.to_json
|
144
|
+
end
|
145
|
+
render json: json, status: :ok
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
#### Options / parameters
|
150
|
+
|
151
|
+
`with_cache` accepts the following parameters, all are *optional*:
|
152
|
+
|
153
|
+
##### `:user_id`
|
154
|
+
|
155
|
+
This value will form the first part of the cache key
|
156
|
+
|
157
|
+
*Caching a user-specific response for each user*
|
158
|
+
Pass the current users' ID (e.g. `current_user.id`) to cache a different value for each user.
|
159
|
+
It's important to do this if anything in the code _inside_ the block (including within any serializers called, etc) references the current user
|
160
|
+
|
161
|
+
*Re-using the same cached value across all users*
|
162
|
+
Pass nil (or just don't supply the parameter, as *nil is the default*) to cache the same value across all users
|
163
|
+
|
164
|
+
|
165
|
+
##### `:cache_time`
|
166
|
+
|
167
|
+
Determines expiry / TTL of the cache entry. If an entry is in the cache, but older than this value, it will be treated
|
168
|
+
as if there is no cache entry there at all.
|
169
|
+
|
170
|
+
*Default*
|
171
|
+
If no value is given, it will use the value of `ENV['RAILS_CACHE_TIMEOUT_SECONDS']`. If that environment
|
172
|
+
variable is not defined, or not parseable to an integer, it will default to zero - i.e. effectively disabling
|
173
|
+
the caching
|
174
|
+
|
175
|
+
*Specifying a number of seconds*
|
176
|
+
If the value is an integer, it will use that as the number of seconds for which the cache entry will be considered valid.
|
177
|
+
|
178
|
+
*Using a particular environment variable*
|
179
|
+
If the value is a string or symbol, it will append that string (uppercased) to `RAILS_CACHE_TIMEOUT_SECONDS` and look
|
180
|
+
for an environment variable with that name to use.
|
181
|
+
For example, if you pass `:short` or `:long` this will use the value of the `ENV['RAILS_CACHE_TIMEOUT_SECONDS_SHORT']` or `ENV['RAILS_CACHE_TIMEOUT_SECONDS_LONG)']` env vars - letting you have different cache timeouts for different methods.
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
##### `:headers_to_include`
|
186
|
+
|
187
|
+
An array of header names, the values of which will be included in the key.
|
188
|
+
This might be useful if your application uses, for instance, a `language` header to render different strings
|
189
|
+
|
190
|
+
|
191
|
+
|
192
|
+
##### `:key`
|
193
|
+
|
194
|
+
If a value is given, it will use that value as the cache key.
|
195
|
+
If no value is given (*default*), it will construct a key using a combination of :
|
196
|
+
* the given `user_id` (optional)
|
197
|
+
* the full request URL including all query parameters
|
198
|
+
* any headers specified in the given `headers_to_include` (optional).
|
199
|
+
|
200
|
+
If `user_id` is NOT given, then the same cache key will be used for all users (assuming they pass the same parameters and have the same values of the named headers), so make sure you consider this when introducing caching.
|
201
|
+
|
202
|
+
#### Error handling / returning non-200 status
|
203
|
+
|
204
|
+
The contents of the block are not called at all if there's a cache hit - so you may need to put a little thought into how to handle any non-OK status that you might want to return.
|
205
|
+
|
206
|
+
For example, this code -
|
207
|
+
|
208
|
+
```
|
209
|
+
def show
|
210
|
+
@record = MyClass.find(params[:id])
|
211
|
+
render json: @record.to_json, status: :ok
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
- has a potential edge case in the `.find` call. It will raise an `ActiveRecord::RecordNotFound` exception if the ID doesn't exist, which usually gets caught elsewhere and a 404 status returned. For this case, an exception will still bubble out of the block and be handled in the normal way, so this code should not need modifying.
|
216
|
+
|
217
|
+
But if your controller method decides on response status without raising an exception, modification may be needed.
|
218
|
+
For example:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
def show
|
222
|
+
@record = MyClass.find(params[:id])
|
223
|
+
if @record.group_id != current_user.group_id
|
224
|
+
render json: { errors: ["You don't have permission to read that record"] }.to_json, status: :forbidden
|
225
|
+
else
|
226
|
+
render json: @record.to_json, status: :ok
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
This code may need a slight alteration to handle this case, something like this:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
def show
|
235
|
+
result = with_cache do
|
236
|
+
@record = MyClass.find(params[:id])
|
237
|
+
if @record.group_id != current_user.group_id
|
238
|
+
'forbidden'
|
239
|
+
else
|
240
|
+
@record.to_json
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
if result == 'forbidden'
|
245
|
+
render json: { errors: ["You don't have permission to read that record"] }.to_json, status: :forbidden
|
246
|
+
else
|
247
|
+
render json: result, status: :ok
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
|
253
|
+
### Testing caching behaviour
|
254
|
+
|
255
|
+
#### Shared examples
|
256
|
+
|
257
|
+
The gem provides three shared examples, which you can add to your controller tests as follows:
|
258
|
+
|
259
|
+
Assuming your controller method looks like the example above,
|
260
|
+
you can use these shared examples in your specs like this:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
describe 'GET :index' do
|
264
|
+
# define the object & method which your caching block
|
265
|
+
# surrounds:
|
266
|
+
let(:object_with_cache_miss_method) { MyModelClass }
|
267
|
+
let(:method_called_on_cache_miss) { :where }
|
268
|
+
|
269
|
+
# two user accounts, so that it can test the caching behaviour
|
270
|
+
# across different users accessing the same methods
|
271
|
+
let(:account_1) { ...user account object 1... }
|
272
|
+
let(:account_2) { ...user account object 2... }
|
273
|
+
|
274
|
+
# test the universal cache behaviour
|
275
|
+
it_behaves_like 'a cached method'
|
276
|
+
|
277
|
+
# ...and EITHER
|
278
|
+
it_behaves_like 'it has a different cache entry for each user'
|
279
|
+
# OR
|
280
|
+
it_behaves_like 'it has the same cache entry for all users'
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
That will test for standard caching behaviour, using an expectation like:
|
285
|
+
|
286
|
+
`expect(object_with_cache_miss_method).to receive(:method_called_on_cache_miss)`
|
287
|
+
|
288
|
+
to signal a cache miss, and:
|
289
|
+
|
290
|
+
`expect(object_with_cache_miss_method).not_to receive(:method_called_on_cache_miss)`
|
291
|
+
|
292
|
+
to signal a cache hit.
|
293
|
+
|
294
|
+
You should ultimately get results that look something like this:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
behaves like a cached method
|
298
|
+
caching
|
299
|
+
when the cache_timeout is more than zero
|
300
|
+
hitting the url twice for the same user with the same params
|
301
|
+
does not calls the code surrounded by with_cache the second time
|
302
|
+
returns the same response each time
|
303
|
+
after the cache expiry
|
304
|
+
calls the code surrounded by with_cache both times
|
305
|
+
hitting the url twice for the same user with different params
|
306
|
+
calls the code surrounded by with_cache both times
|
307
|
+
behaves like it has a different cache entry for each user
|
308
|
+
hitting the url twice for two different users with the same params
|
309
|
+
calls the code surrounded by with_cache both times
|
310
|
+
|
311
|
+
```
|
312
|
+
|
313
|
+
## Development
|
314
|
+
|
315
|
+
After checking out the repo, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
316
|
+
|
317
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
318
|
+
|
319
|
+
## Contributing
|
320
|
+
|
321
|
+
Bug reports and pull requests are welcome on Builder's GitLab at https://gitlab.builder.ai/cte/alistair-davidson/builder-rails_cache.
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/builder/rails_cache/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "builder-rails_cache"
|
7
|
+
spec.version = Builder::RailsCache::VERSION
|
8
|
+
spec.authors = ["Alistair Davidson"]
|
9
|
+
spec.email = ["alistair.davidson@builder.ai"]
|
10
|
+
|
11
|
+
spec.summary = "Provides convenience method `with_cache` for caching API response JSON"
|
12
|
+
spec.description = "Provides convenience method `with_cache` for caching API response JSON"
|
13
|
+
spec.homepage = "https://gitlab.builder.ai/cte/alistair-davidson/builder-rails_cache"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
# spec.metadata["allowed_push_host"] = "https://gem.fury.io/engineerai"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
# make our shared examples available to projects which install the gem
|
32
|
+
spec.files += `git ls-files -- spec/shared_examples/*.rb`.split("\n")
|
33
|
+
|
34
|
+
# spec.bindir = "exe"
|
35
|
+
# spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
36
|
+
spec.require_paths = ["lib"]
|
37
|
+
|
38
|
+
spec.add_dependency 'rails', '>= 4.0'
|
39
|
+
spec.add_development_dependency "byebug"
|
40
|
+
|
41
|
+
# For more information and examples about making a new gem, check out our
|
42
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
43
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rails_cache/version"
|
4
|
+
|
5
|
+
module Builder
|
6
|
+
module RailsCache
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
# :cache_time determines expiry
|
12
|
+
# pass :short or :long to use the ENV['RAILS_CACHE_TIMEOUT_SECONDS_(SHORT|LONG)'] env vars
|
13
|
+
# or an integer to use a specific number of seconds
|
14
|
+
#
|
15
|
+
# :user_id will be the first part of the key
|
16
|
+
# Pass @token.id to cache a different value for each user
|
17
|
+
# or nil (default) to cache the same value across all users
|
18
|
+
#
|
19
|
+
# :key will default to nil, in which case a key will be constructed
|
20
|
+
# from the user_id, the url, and the headers_to_include
|
21
|
+
def with_cache(cache_time: :short, user_id: nil, key: nil, headers_to_include: [])
|
22
|
+
timeout = cache_timeout(cache_time)
|
23
|
+
key ||= cache_key(user_id: user_id, headers_to_include: headers_to_include)
|
24
|
+
logger.debug "cache key: #{key}, timeout: #{timeout}"
|
25
|
+
cache_hit = true
|
26
|
+
|
27
|
+
data = cache_instance.fetch(key, expires_in: timeout) do
|
28
|
+
cache_hit = false
|
29
|
+
yield if block_given?
|
30
|
+
end
|
31
|
+
logger.debug( cache_hit ? 'CACHE HIT' : 'CACHE MISS')
|
32
|
+
data
|
33
|
+
end
|
34
|
+
|
35
|
+
def cache_instance
|
36
|
+
Rails.cache
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache_key(user_id: nil, headers_to_include: [])
|
40
|
+
headers = request.headers.select{|k,v| headers_to_include.include?(k)}.map{|k,v| [k,v].join('=')}.flatten.join('&')
|
41
|
+
[user_id, request.url, headers]
|
42
|
+
end
|
43
|
+
|
44
|
+
# pass a symbol or string to use the value of the ENV['RAILS_CACHE_TIMEOUT_SECONDS_(...)'] env var
|
45
|
+
# (e.g. if you pass :short, it will use ENV['RAILS_CACHE_TIMEOUT_SECONDS_SHORT'])
|
46
|
+
# pass an integer to use a specific number of seconds - if this 0, it's essentially a force-refresh
|
47
|
+
# the default is 0
|
48
|
+
def cache_timeout(short_long_or_seconds = nil)
|
49
|
+
if short_long_or_seconds.is_a?(Integer)
|
50
|
+
short_long_or_seconds
|
51
|
+
elsif short_long_or_seconds.is_a?(Symbol) || short_long_or_seconds.is_a?(String)
|
52
|
+
ENV[ ["RAILS_CACHE_TIMEOUT_SECONDS", short_long_or_seconds.to_s.upcase].join('_') ].to_i
|
53
|
+
else
|
54
|
+
0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# Make the shared examples available to projects which install this gem
|
62
|
+
# as per https://stackoverflow.com/a/50688950
|
63
|
+
if ENV['RAILS_ENV'] == 'test'
|
64
|
+
require File.expand_path('../../spec/shared_examples/cached_method.rb', __dir__)
|
65
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
#
|
2
|
+
# If your controller method looks like this:
|
3
|
+
# ```
|
4
|
+
# def index
|
5
|
+
# json = with_cache do
|
6
|
+
# @records = MyModelClass.where(....)
|
7
|
+
# @records.to_json
|
8
|
+
# end
|
9
|
+
# render json: json, status: :ok
|
10
|
+
# ```
|
11
|
+
#
|
12
|
+
# You can use this shared example in your specs like this:
|
13
|
+
#
|
14
|
+
# ```
|
15
|
+
# describe 'GET :index' do
|
16
|
+
# let(:object_with_cache_miss_method) { MyModelClass }
|
17
|
+
# let(:method_called_on_cache_miss) { :where }
|
18
|
+
#
|
19
|
+
# it_behaves_like 'a cached method'
|
20
|
+
# # and EITHER
|
21
|
+
# it_behaves_like 'it has a different cache entry for each user'
|
22
|
+
# # OR
|
23
|
+
# it_behaves_like 'it has the same cache entry for all users'
|
24
|
+
# end
|
25
|
+
# ```
|
26
|
+
#
|
27
|
+
# That will test for standard caching behaviour, using the expectation:
|
28
|
+
# `expect(object_with_cache_miss_method).to receive(:method_called_on_cache_miss)`
|
29
|
+
# to signal a cache miss, and:
|
30
|
+
# `expect(object_with_cache_miss_method).not_to receive(:method_called_on_cache_miss)`
|
31
|
+
# to signal a cache hit
|
32
|
+
#
|
33
|
+
RSpec.shared_examples "a cached method" do
|
34
|
+
describe 'caching' do
|
35
|
+
let(:token_1) { BuilderJsonWebToken.encode(account_1.id, 1.day.from_now, token_type: 'login') }
|
36
|
+
let(:token_2) { BuilderJsonWebToken.encode(account_2.id, 1.day.from_now, token_type: 'login') }
|
37
|
+
|
38
|
+
# default cache for test environment is null_store, which does not actually write data
|
39
|
+
# so we need to force usage of memory_store in these tests
|
40
|
+
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
|
41
|
+
let(:cache) { Rails.cache }
|
42
|
+
|
43
|
+
before do
|
44
|
+
allow(Rails).to receive(:cache).and_return(memory_store)
|
45
|
+
Rails.cache.clear
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when the cache_timeout is more than zero' do
|
49
|
+
before do
|
50
|
+
allow(controller).to receive(:cache_timeout).and_return(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'hitting the url twice for the same user with the same params' do
|
54
|
+
let(:params) { {some_param: 3, token: token_1} }
|
55
|
+
before do
|
56
|
+
request.headers['token'] = token_1
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'does not calls the code surrounded by with_cache the second time' do
|
60
|
+
expect(object_with_cache_miss_method).to receive(method_called_on_cache_miss).exactly(:once).and_return([])
|
61
|
+
get :current_conditions, params: params
|
62
|
+
get :current_conditions, params: params
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'returns the same response each time' do
|
66
|
+
get :current_conditions, params: params
|
67
|
+
response_1 = response.body.dup
|
68
|
+
get :current_conditions, params: params
|
69
|
+
response_2 = response.body.dup
|
70
|
+
expect(response_1).to eq(response_2)
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'after the cache expiry' do
|
74
|
+
it 'calls the code surrounded by with_cache both times' do
|
75
|
+
expect(object_with_cache_miss_method).to receive(method_called_on_cache_miss).exactly(:twice).and_return([])
|
76
|
+
get :current_conditions, params: params
|
77
|
+
sleep(1.1)
|
78
|
+
get :current_conditions, params: params
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'hitting the url twice for the same user with different params' do
|
84
|
+
let(:params_1) { {some_param: 3} }
|
85
|
+
let(:params_2) { {some_param: 3, foo: 'bar'} }
|
86
|
+
|
87
|
+
it 'calls the code surrounded by with_cache both times' do
|
88
|
+
expect(object_with_cache_miss_method).to receive(method_called_on_cache_miss).exactly(:twice).and_return([])
|
89
|
+
request.headers['token'] = token_1
|
90
|
+
get :current_conditions, params: params_1
|
91
|
+
request.headers['token'] = token_1
|
92
|
+
get :current_conditions, params: params_2
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
RSpec.shared_examples 'it has a different cache entry for each user' do
|
100
|
+
let(:token_1) { BuilderJsonWebToken.encode(account_1.id, 1.day.from_now, token_type: 'login') }
|
101
|
+
let(:token_2) { BuilderJsonWebToken.encode(account_2.id, 1.day.from_now, token_type: 'login') }
|
102
|
+
|
103
|
+
context 'hitting the url twice for two different users with the same params' do
|
104
|
+
let(:params_1) { {some_param: 3} }
|
105
|
+
let(:params_2) { {some_param: 3} }
|
106
|
+
|
107
|
+
it 'calls the code surrounded by with_cache both times' do
|
108
|
+
expect(object_with_cache_miss_method).to receive(method_called_on_cache_miss).exactly(:twice).and_return([])
|
109
|
+
request.headers['token'] = token_1
|
110
|
+
get :current_conditions, params: params_1
|
111
|
+
request.headers['token'] = token_2
|
112
|
+
get :current_conditions, params: params_2
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
RSpec.shared_examples 'it has the same cache entry for all users' do
|
118
|
+
let(:token_1) { BuilderJsonWebToken.encode(account_1.id, 1.day.from_now, token_type: 'login') }
|
119
|
+
let(:token_2) { BuilderJsonWebToken.encode(account_2.id, 1.day.from_now, token_type: 'login') }
|
120
|
+
|
121
|
+
context 'hitting the url twice for two different users with the same params' do
|
122
|
+
let(:params_1) { {some_param: 3} }
|
123
|
+
let(:params_2) { {some_param: 3} }
|
124
|
+
|
125
|
+
it 'does not calls the code surrounded by with_cache for the second user' do
|
126
|
+
expect(object_with_cache_miss_method).to receive(method_called_on_cache_miss).exactly(:once).and_return([])
|
127
|
+
request.headers['token'] = token_1
|
128
|
+
get :current_conditions, params: params_1
|
129
|
+
request.headers['token'] = token_2
|
130
|
+
get :current_conditions, params: params_2
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: builder-rails_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alistair Davidson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-11-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Provides convenience method `with_cache` for caching API response JSON
|
42
|
+
email:
|
43
|
+
- alistair.davidson@builder.ai
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rspec"
|
49
|
+
- CHANGELOG.md
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- builder-rails_cache.gemspec
|
53
|
+
- lib/builder/rails_cache.rb
|
54
|
+
- lib/builder/rails_cache/version.rb
|
55
|
+
- sig/builder/rails_cache.rbs
|
56
|
+
- spec/shared_examples/cached_method.rb
|
57
|
+
homepage: https://gitlab.builder.ai/cte/alistair-davidson/builder-rails_cache
|
58
|
+
licenses: []
|
59
|
+
metadata:
|
60
|
+
homepage_uri: https://gitlab.builder.ai/cte/alistair-davidson/builder-rails_cache
|
61
|
+
source_code_uri: https://gitlab.builder.ai/cte/alistair-davidson/builder-rails_cache
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.6.0
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubygems_version: 3.0.3
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Provides convenience method `with_cache` for caching API response JSON
|
81
|
+
test_files: []
|