cache_crispies 0.3.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/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
|