cache_crispies 0.3.1 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cache_crispies.rb +21 -8
- data/lib/cache_crispies/attribute.rb +31 -7
- data/lib/cache_crispies/base.rb +100 -31
- data/lib/cache_crispies/collection.rb +2 -4
- data/lib/cache_crispies/configuration.rb +28 -0
- data/lib/cache_crispies/controller.rb +7 -5
- data/lib/cache_crispies/plan.rb +2 -1
- data/lib/cache_crispies/version.rb +1 -1
- data/spec/{attribute_spec.rb → cache_crispies/attribute_spec.rb} +29 -0
- data/spec/cache_crispies/base_spec.rb +289 -0
- data/spec/{collection_spec.rb → cache_crispies/collection_spec.rb} +31 -21
- data/spec/{condition_spec.rb → cache_crispies/condition_spec.rb} +0 -0
- data/spec/cache_crispies/configuration_spec.rb +42 -0
- data/spec/cache_crispies/controller_spec.rb +90 -0
- data/spec/{hash_builder_spec.rb → cache_crispies/hash_builder_spec.rb} +0 -0
- data/spec/{memoizer_spec.rb → cache_crispies/memoizer_spec.rb} +0 -0
- data/spec/{plan_spec.rb → cache_crispies/plan_spec.rb} +12 -7
- data/spec/cache_crispies_spec.rb +27 -0
- metadata +49 -24
- data/spec/base_spec.rb +0 -186
- data/spec/controller_spec.rb +0 -69
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe CacheCrispies::Base do
|
5
|
+
class NutritionSerializer < CacheCrispies::Base
|
6
|
+
serialize :calories
|
7
|
+
end
|
8
|
+
|
9
|
+
class CacheCrispiesTestSerializer < CacheCrispies::Base
|
10
|
+
serialize :id, :company, to: String
|
11
|
+
|
12
|
+
show_if -> { true } do
|
13
|
+
show_if -> { true } do
|
14
|
+
show_if -> { true } do
|
15
|
+
serialize :name, from: :brand
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
nest_in :nested do
|
21
|
+
nest_in :nested_again do
|
22
|
+
serialize :deeply_nested do |model, _opts|
|
23
|
+
model.deeply_nested.to_s.upcase
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
serialize :nutrition_info, with: NutritionSerializer
|
29
|
+
|
30
|
+
serialize :organic, to: :bool
|
31
|
+
serialize :parent_company, through: :legal
|
32
|
+
|
33
|
+
def id
|
34
|
+
model.id.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:model) do
|
39
|
+
OpenStruct.new(
|
40
|
+
id: 42,
|
41
|
+
brand: 'Cookie Crisp',
|
42
|
+
company: 'General Mills',
|
43
|
+
deeply_nested: true,
|
44
|
+
nutrition_info: OpenStruct.new(calories: 1_000),
|
45
|
+
organic: 'true',
|
46
|
+
legal: OpenStruct.new(parent_company: 'Disney probably')
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
let(:serializer) { CacheCrispiesTestSerializer }
|
51
|
+
let(:instance) { serializer.new(model) }
|
52
|
+
subject { instance }
|
53
|
+
|
54
|
+
describe '#as_json' do
|
55
|
+
it 'serializes to a hash' do
|
56
|
+
expect(subject.as_json).to eq(
|
57
|
+
id: '42',
|
58
|
+
name: 'Cookie Crisp',
|
59
|
+
company: 'General Mills',
|
60
|
+
nested: {
|
61
|
+
nested_again: {
|
62
|
+
deeply_nested: 'TRUE'
|
63
|
+
}
|
64
|
+
},
|
65
|
+
nutrition_info: {
|
66
|
+
calories: 1000
|
67
|
+
},
|
68
|
+
organic: true,
|
69
|
+
parent_company: 'Disney probably'
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '.key' do
|
75
|
+
it 'underscores the demodulized class name by default' do
|
76
|
+
expect(serializer.key).to eq :cache_crispies_test
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with a custom key' do
|
80
|
+
after { serializer.remove_instance_variable :@key }
|
81
|
+
|
82
|
+
it 'sets and returns a custom key' do
|
83
|
+
expect {
|
84
|
+
serializer.key 'custom_key'
|
85
|
+
}.to change { serializer.key }.from(:cache_crispies_test).to :custom_key
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'sets and returns a nil key' do
|
89
|
+
expect {
|
90
|
+
serializer.key nil
|
91
|
+
}.to change { serializer.key }.from(:cache_crispies_test).to nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '.collection_key' do
|
97
|
+
it 'pluralizes the #key by default' do
|
98
|
+
expect(serializer.collection_key).to eq :cache_crispies_tests
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with a custom key' do
|
102
|
+
after { serializer.remove_instance_variable :@collection_key }
|
103
|
+
|
104
|
+
it 'sets and returns a custom key' do
|
105
|
+
expect {
|
106
|
+
serializer.collection_key 'custom_key'
|
107
|
+
}.to change {
|
108
|
+
serializer.collection_key
|
109
|
+
}.from(:cache_crispies_tests).to :custom_key
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'sets and returns a nil key' do
|
113
|
+
expect {
|
114
|
+
serializer.collection_key nil
|
115
|
+
}.to change {
|
116
|
+
serializer.collection_key
|
117
|
+
}.from(:cache_crispies_tests).to nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '.do_caching' do
|
123
|
+
it 'is false by default' do
|
124
|
+
expect(serializer.do_caching).to be false
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'when called with an argument' do
|
128
|
+
after { serializer.remove_instance_variable :@do_caching }
|
129
|
+
|
130
|
+
it 'sets and returns a value' do
|
131
|
+
expect {
|
132
|
+
serializer.do_caching true
|
133
|
+
}.to change {
|
134
|
+
serializer.do_caching
|
135
|
+
}.from(false).to true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '.cache_key_addons' do
|
141
|
+
it 'returns an empty array by default' do
|
142
|
+
expect(serializer.cache_key_addons).to eq []
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'when given a block' do
|
146
|
+
it 'stores the block for later execution' do
|
147
|
+
serializer.cache_key_addons { |opts| opts[:username].downcase }
|
148
|
+
expect(
|
149
|
+
serializer.cache_key_addons(username: 'CapnCrunch')
|
150
|
+
).to eq ['capncrunch']
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'returns nil' do
|
154
|
+
expect(serializer.cache_key_addons { |_opts| }).to be nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe '.dependency_key' do
|
160
|
+
it 'returns nil by default' do
|
161
|
+
expect(serializer.dependency_key).to be nil
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'after being set' do
|
165
|
+
let(:key) { SecureRandom.hex }
|
166
|
+
before { serializer.dependency_key key }
|
167
|
+
|
168
|
+
it 'returns the set value' do
|
169
|
+
expect(serializer.dependency_key).to be key
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '.cache_key_base' do
|
175
|
+
let(:nested_serializer_digest) { 'nutrition-serializer-digest' }
|
176
|
+
let(:serializer_file_path) {
|
177
|
+
File.expand_path('../fixtures/test_serializer.rb', __dir__)
|
178
|
+
}
|
179
|
+
let(:serializer_file_digest) {
|
180
|
+
Digest::MD5.file(serializer_file_path).to_s
|
181
|
+
}
|
182
|
+
|
183
|
+
before do
|
184
|
+
allow(NutritionSerializer).to receive(:file_hash).and_return(
|
185
|
+
nested_serializer_digest
|
186
|
+
)
|
187
|
+
allow(Rails).to receive_message_chain(:root, :join).and_return(
|
188
|
+
serializer_file_path
|
189
|
+
)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'includes the file name' do
|
193
|
+
expect(serializer.cache_key_base).to include serializer.to_s
|
194
|
+
end
|
195
|
+
|
196
|
+
it "includes a digest of the serializer class file's contents" do
|
197
|
+
expect(serializer.cache_key_base).to include serializer_file_digest
|
198
|
+
end
|
199
|
+
|
200
|
+
it "includes a digest of the nested serializer class file's contents" do
|
201
|
+
expect(serializer.cache_key_base).to include nested_serializer_digest
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'correctly formats the key' do
|
205
|
+
expect(serializer.cache_key_base).to eq(
|
206
|
+
"#{serializer}-#{serializer_file_digest}+#{nested_serializer_digest}"
|
207
|
+
)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe '.attributes' do
|
212
|
+
subject { instance.class.attributes }
|
213
|
+
|
214
|
+
it 'contains all the attributes' do
|
215
|
+
expect(subject.length).to eq 7
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'preserves the attribute order' do
|
219
|
+
expect(subject.map(&:key)).to eq(
|
220
|
+
%i[id company name deeply_nested nutrition_info organic parent_company]
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'contains the correct attribute values' do
|
225
|
+
expect(subject[0].method_name).to eq :id
|
226
|
+
expect(subject[0].key).to eq :id
|
227
|
+
expect(subject[0].serializer).to be nil
|
228
|
+
expect(subject[0].through).to be nil
|
229
|
+
expect(subject[0].coerce_to).to be String
|
230
|
+
expect(subject[0].nesting).to eq []
|
231
|
+
expect(subject[0].conditions).to eq []
|
232
|
+
expect(subject[0].block).to be nil
|
233
|
+
|
234
|
+
expect(subject[1].method_name).to eq :company
|
235
|
+
expect(subject[1].key).to eq :company
|
236
|
+
expect(subject[1].serializer).to be nil
|
237
|
+
expect(subject[1].through).to be nil
|
238
|
+
expect(subject[1].coerce_to).to be String
|
239
|
+
expect(subject[1].nesting).to eq []
|
240
|
+
expect(subject[1].conditions).to eq []
|
241
|
+
expect(subject[1].block).to be nil
|
242
|
+
|
243
|
+
expect(subject[2].method_name).to eq :brand
|
244
|
+
expect(subject[2].key).to eq :name
|
245
|
+
expect(subject[2].serializer).to be nil
|
246
|
+
expect(subject[2].through).to be nil
|
247
|
+
expect(subject[2].coerce_to).to be nil
|
248
|
+
expect(subject[2].nesting).to eq []
|
249
|
+
expect(subject[2].conditions.length).to be 3
|
250
|
+
expect(subject[2].block).to be nil
|
251
|
+
|
252
|
+
expect(subject[3].method_name).to eq :deeply_nested
|
253
|
+
expect(subject[3].key).to eq :deeply_nested
|
254
|
+
expect(subject[3].serializer).to be nil
|
255
|
+
expect(subject[3].through).to be nil
|
256
|
+
expect(subject[3].coerce_to).to be nil
|
257
|
+
expect(subject[3].nesting).to eq %i[nested nested_again]
|
258
|
+
expect(subject[3].conditions).to eq []
|
259
|
+
expect(subject[3].block).to be_a_kind_of Proc
|
260
|
+
|
261
|
+
expect(subject[4].method_name).to eq :nutrition_info
|
262
|
+
expect(subject[4].key).to eq :nutrition_info
|
263
|
+
expect(subject[4].serializer).to be NutritionSerializer
|
264
|
+
expect(subject[4].through).to be nil
|
265
|
+
expect(subject[4].coerce_to).to be nil
|
266
|
+
expect(subject[4].nesting).to eq []
|
267
|
+
expect(subject[4].conditions).to eq []
|
268
|
+
expect(subject[4].block).to be nil
|
269
|
+
|
270
|
+
expect(subject[5].method_name).to eq :organic
|
271
|
+
expect(subject[5].key).to eq :organic
|
272
|
+
expect(subject[5].serializer).to be nil
|
273
|
+
expect(subject[5].through).to be nil
|
274
|
+
expect(subject[5].coerce_to).to be :bool
|
275
|
+
expect(subject[5].nesting).to eq []
|
276
|
+
expect(subject[5].conditions).to eq []
|
277
|
+
expect(subject[5].block).to be nil
|
278
|
+
|
279
|
+
expect(subject[6].method_name).to eq :parent_company
|
280
|
+
expect(subject[6].key).to eq :parent_company
|
281
|
+
expect(subject[6].serializer).to be nil
|
282
|
+
expect(subject[6].through).to be :legal
|
283
|
+
expect(subject[6].coerce_to).to be nil
|
284
|
+
expect(subject[6].nesting).to eq []
|
285
|
+
expect(subject[6].conditions).to eq []
|
286
|
+
expect(subject[6].block).to be nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -7,10 +7,7 @@ describe CacheCrispies::Collection do
|
|
7
7
|
|
8
8
|
class CacheableCerealSerializerForCollection < CacheCrispies::Base
|
9
9
|
serialize :name
|
10
|
-
|
11
|
-
def self.do_caching?
|
12
|
-
true
|
13
|
-
end
|
10
|
+
do_caching true
|
14
11
|
end
|
15
12
|
|
16
13
|
let(:name1) { 'Cinnamon Toast Crunch' }
|
@@ -35,8 +32,8 @@ describe CacheCrispies::Collection do
|
|
35
32
|
|
36
33
|
it "doesn't cache the results" do
|
37
34
|
expect(CacheCrispies::Plan).to_not receive(:new)
|
38
|
-
expect(
|
39
|
-
expect(subject.as_json).to eq [
|
35
|
+
expect(CacheCrispies).to_not receive :cache
|
36
|
+
expect(subject.as_json).to eq [{ name: name1 }, { name: name2 }]
|
40
37
|
end
|
41
38
|
end
|
42
39
|
|
@@ -45,29 +42,42 @@ describe CacheCrispies::Collection do
|
|
45
42
|
|
46
43
|
it "doesn't cache the results" do
|
47
44
|
expect(CacheCrispies::Plan).to_not receive(:new)
|
48
|
-
expect(
|
49
|
-
expect(subject.as_json).to eq [
|
45
|
+
expect(CacheCrispies).to_not receive :cache
|
46
|
+
expect(subject.as_json).to eq [{ name: name1 }, { name: name2 }]
|
50
47
|
end
|
51
48
|
end
|
52
49
|
end
|
53
50
|
|
54
51
|
context 'when it is cacheable' do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
context 'when the collection cache key misses' do
|
53
|
+
before do
|
54
|
+
allow(CacheCrispies).to receive_message_chain(
|
55
|
+
:cache, :fetch
|
56
|
+
).with('cacheable-collection-key').and_yield
|
57
|
+
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
it 'fetches the cache for each object in the collection' do
|
60
|
+
expect(CacheCrispies::Plan).to receive(:new).with(
|
61
|
+
serializer, model1, options
|
62
|
+
).and_return double('plan-dbl-1', cache_key: 'cereal-key-1')
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
.and_yield('cereal-key-2').and_return(name: name2)
|
64
|
+
expect(CacheCrispies::Plan).to receive(:new).with(
|
65
|
+
serializer, model2, options
|
66
|
+
).and_return double('plan-dbl-2', cache_key: 'cereal-key-2')
|
68
67
|
|
69
|
-
|
68
|
+
expect(CacheCrispies).to receive_message_chain(
|
69
|
+
:cache, :fetch_multi
|
70
|
+
).with(
|
71
|
+
'cereal-key-1', 'cereal-key-2'
|
72
|
+
).and_yield('cereal-key-1').and_return(
|
73
|
+
name: name1
|
74
|
+
).and_yield('cereal-key-2').and_return(
|
75
|
+
name: name2
|
76
|
+
)
|
77
|
+
|
78
|
+
subject.as_json
|
79
|
+
end
|
70
80
|
end
|
71
81
|
end
|
72
82
|
end
|
73
|
-
end
|
83
|
+
end
|
File without changes
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CacheCrispies::Configuration do
|
4
|
+
describe '#cache_store' do
|
5
|
+
context 'when Rails.cache is defined' do
|
6
|
+
let(:cache_double) { double }
|
7
|
+
before { expect(Rails).to receive(:cache).and_return cache_double }
|
8
|
+
|
9
|
+
it 'is Rails.cache by default' do
|
10
|
+
expect(subject.cache_store).to be cache_double
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when Rails.cache is nil' do
|
15
|
+
before { expect(Rails).to receive(:cache).and_return nil }
|
16
|
+
|
17
|
+
it 'is NullStore by default' do
|
18
|
+
expect(subject.cache_store).to be_kind_of ActiveSupport::Cache::NullStore
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'can be changed' do
|
23
|
+
cache_store = ActiveSupport::Cache::NullStore.new
|
24
|
+
|
25
|
+
expect {
|
26
|
+
subject.cache_store = cache_store
|
27
|
+
}.to change { subject.cache_store }.to cache_store
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#etags' do
|
32
|
+
it 'is false by default' do
|
33
|
+
expect(subject.etags).to be false
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'can be changed' do
|
37
|
+
expect {
|
38
|
+
subject.etags = true
|
39
|
+
}.to change { subject.etags }.to true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_model'
|
3
|
+
require 'action_controller'
|
4
|
+
|
5
|
+
describe CacheCrispies::Controller do
|
6
|
+
class Cereal
|
7
|
+
include ActiveModel::Model
|
8
|
+
attr_accessor :name
|
9
|
+
end
|
10
|
+
|
11
|
+
class CerealController < ActionController::Base
|
12
|
+
include CacheCrispies::Controller
|
13
|
+
end
|
14
|
+
|
15
|
+
class CerealSerializerForController < CacheCrispies::Base
|
16
|
+
key 'cereal'
|
17
|
+
serialize :name
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:cereal_names) { ['Count Chocula', 'Eyeholes'] }
|
21
|
+
let(:collection) { cereal_names.map { |name| Cereal.new(name: name) } }
|
22
|
+
|
23
|
+
subject { CerealController.new }
|
24
|
+
|
25
|
+
describe '#cache_render' do
|
26
|
+
let(:etags) { false }
|
27
|
+
let(:single_json) { { cereal: { name: cereal_names.first } }.to_json }
|
28
|
+
let(:collection_json) {
|
29
|
+
{ cereals: cereal_names.map { |name| { name: name } } }.to_json
|
30
|
+
}
|
31
|
+
|
32
|
+
before do
|
33
|
+
expect(
|
34
|
+
CacheCrispies
|
35
|
+
).to receive_message_chain(:config, :etags?).and_return etags
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with etags disabled' do
|
39
|
+
it 'does not set etags' do
|
40
|
+
expect(subject).to receive(:render).with json: collection_json
|
41
|
+
response_double = double
|
42
|
+
allow(subject).to receive(:response).and_return response_double
|
43
|
+
expect(response_double).to_not receive(:weak_etag=)
|
44
|
+
|
45
|
+
subject.cache_render CerealSerializerForController, collection
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'with etags enabled' do
|
50
|
+
let(:etags) { true }
|
51
|
+
|
52
|
+
it 'sets etags' do
|
53
|
+
expect(subject).to receive(:render).with json: collection_json
|
54
|
+
expect_any_instance_of(
|
55
|
+
CacheCrispies::Plan
|
56
|
+
).to receive(:etag).and_return 'test-etag'
|
57
|
+
expect(subject).to receive_message_chain(:response, :weak_etag=).with 'test-etag'
|
58
|
+
|
59
|
+
subject.cache_render CerealSerializerForController, collection
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'renders a json collection' do
|
64
|
+
expect(subject).to receive(:render).with json: collection_json
|
65
|
+
|
66
|
+
subject.cache_render CerealSerializerForController, collection
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'renders a single json object' do
|
70
|
+
expect(subject).to receive(:render).with json: single_json
|
71
|
+
|
72
|
+
subject.cache_render CerealSerializerForController, collection.first
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with a status: option' do
|
76
|
+
it 'passes the status option to the Rails render call' do
|
77
|
+
expect(subject).to receive(:render).with(
|
78
|
+
json: single_json,
|
79
|
+
status: 418
|
80
|
+
)
|
81
|
+
|
82
|
+
subject.cache_render(
|
83
|
+
CerealSerializerForController,
|
84
|
+
collection.first,
|
85
|
+
status: 418
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|