cache_crispies 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/spec/base_spec.rb ADDED
@@ -0,0 +1,153 @@
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
23
+ end
24
+ end
25
+
26
+ serialize :nutrition_info, with: NutritionSerializer
27
+
28
+ serialize :organic, to: :bool
29
+
30
+ def id
31
+ model.id.to_s
32
+ end
33
+ end
34
+
35
+ let(:model) do
36
+ OpenStruct.new(
37
+ id: 42,
38
+ brand: 'Cookie Crisp',
39
+ company: 'General Mills',
40
+ deeply_nested: true,
41
+ nutrition_info: OpenStruct.new(calories: 1_000),
42
+ organic: 'true'
43
+ )
44
+ end
45
+
46
+ let(:instance) { CacheCrispiesTestSerializer.new(model) }
47
+ subject { instance }
48
+
49
+ describe '#as_json' do
50
+ it 'serializes to a hash' do
51
+ expect(subject.as_json).to eq(
52
+ id: '42',
53
+ name: 'Cookie Crisp',
54
+ company: 'General Mills',
55
+ nested: {
56
+ nested_again: {
57
+ deeply_nested: true
58
+ }
59
+ },
60
+ nutrition_info: {
61
+ calories: 1000
62
+ },
63
+ organic: true
64
+ )
65
+ end
66
+ end
67
+
68
+ describe '.key' do
69
+ it 'underscores the demodulized class name' do
70
+ expect(subject.class.key).to eq :cache_crispies_test
71
+ end
72
+ end
73
+
74
+ describe '.collection_key' do
75
+ it 'pluralizes the #key' do
76
+ expect(subject.class.collection_key).to eq :cache_crispies_tests
77
+ end
78
+ end
79
+
80
+ describe '.do_caching?' do
81
+ it 'is false by default' do
82
+ expect(subject.class.do_caching?).to be false
83
+ end
84
+ end
85
+
86
+ describe '.cache_key_addons' do
87
+ it 'returns an empty array by default' do
88
+ expect(subject.class.cache_key_addons).to eq []
89
+ end
90
+ end
91
+
92
+ describe '.cache_key_base' do
93
+ # TODO
94
+ end
95
+
96
+ describe '.attributes' do
97
+ subject { instance.class.attributes }
98
+
99
+ it 'contains all the attributes' do
100
+ expect(subject.length).to eq 6
101
+ end
102
+
103
+ it 'preserves the attribute order' do
104
+ expect(subject.map(&:key)).to eq(
105
+ %i[id company name deeply_nested nutrition_info organic]
106
+ )
107
+ end
108
+
109
+ it 'contains the correct attribute values' do
110
+ expect(subject[0].method_name).to eq :id
111
+ expect(subject[0].key).to eq :id
112
+ expect(subject[0].serializer).to be nil
113
+ expect(subject[0].coerce_to).to be String
114
+ expect(subject[0].nesting).to eq []
115
+ expect(subject[0].conditions).to eq []
116
+
117
+ expect(subject[1].method_name).to eq :company
118
+ expect(subject[1].key).to eq :company
119
+ expect(subject[1].serializer).to be nil
120
+ expect(subject[1].coerce_to).to be String
121
+ expect(subject[1].nesting).to eq []
122
+ expect(subject[1].conditions).to eq []
123
+
124
+ expect(subject[2].method_name).to eq :brand
125
+ expect(subject[2].key).to eq :name
126
+ expect(subject[2].serializer).to be nil
127
+ expect(subject[2].coerce_to).to be nil
128
+ expect(subject[2].nesting).to eq []
129
+ expect(subject[2].conditions.length).to be 3
130
+
131
+ expect(subject[3].method_name).to eq :deeply_nested
132
+ expect(subject[3].key).to eq :deeply_nested
133
+ expect(subject[3].serializer).to be nil
134
+ expect(subject[3].coerce_to).to be nil
135
+ expect(subject[3].nesting).to eq %i[nested nested_again]
136
+ expect(subject[3].conditions).to eq []
137
+
138
+ expect(subject[4].method_name).to eq :nutrition_info
139
+ expect(subject[4].key).to eq :nutrition_info
140
+ expect(subject[4].serializer).to be NutritionSerializer
141
+ expect(subject[4].coerce_to).to be nil
142
+ expect(subject[4].nesting).to eq []
143
+ expect(subject[4].conditions).to eq []
144
+
145
+ expect(subject[5].method_name).to eq :organic
146
+ expect(subject[5].key).to eq :organic
147
+ expect(subject[5].serializer).to be nil
148
+ expect(subject[5].coerce_to).to be :bool
149
+ expect(subject[5].nesting).to eq []
150
+ expect(subject[5].conditions).to eq []
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe CacheCrispies::Collection do
4
+ class UncacheableCerealSerializerForCollection < CacheCrispies::Base
5
+ serialize :name
6
+ end
7
+
8
+ class CacheableCerealSerializerForCollection < CacheCrispies::Base
9
+ serialize :name
10
+
11
+ def self.do_caching?
12
+ true
13
+ end
14
+ end
15
+
16
+ let(:name1) { 'Cinnamon Toast Crunch' }
17
+ let(:name2) { 'Cocoa Puffs' }
18
+ let(:model1) { OpenStruct.new(name: name1) }
19
+ let(:model2) { OpenStruct.new(name: name2) }
20
+ let(:uncacheable_models) { [model1, model2] }
21
+ let(:cacheable_models) {
22
+ [model1, model2].tap do |models|
23
+ def models.cache_key
24
+ 'cereals-key'
25
+ end
26
+ end
27
+ }
28
+ let(:collection) { cacheable_models }
29
+ let(:serializer) { CacheableCerealSerializerForCollection }
30
+ let(:options) { {} }
31
+ subject { described_class.new(collection, serializer, options) }
32
+
33
+ describe '#as_json' do
34
+ context "when it's not cacheable" do
35
+ context 'because the collection is not cacheable' do
36
+ let(:collection) { uncacheable_models }
37
+
38
+ it "doesn't cache the results" do
39
+ expect(CacheCrispies::Plan).to_not receive(:new)
40
+ expect(Rails).to_not receive :cache
41
+ expect(subject.as_json).to eq [ {name: name1}, {name: name2} ]
42
+ end
43
+ end
44
+
45
+ context 'because the serializer is not cacheable' do
46
+ let(:serializer) { UncacheableCerealSerializerForCollection }
47
+
48
+ it "doesn't cache the results" do
49
+ expect(CacheCrispies::Plan).to_not receive(:new)
50
+ expect(Rails).to_not receive :cache
51
+ expect(subject.as_json).to eq [ {name: name1}, {name: name2} ]
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when it is cacheable' do
57
+ it 'caches the results' do
58
+ expect(CacheCrispies::Plan).to receive(:new).with(
59
+ serializer, model1, options
60
+ ).and_return double(cache_key: 'cereal-key-1')
61
+
62
+ expect(CacheCrispies::Plan).to receive(:new).with(
63
+ serializer, model2, options
64
+ ).and_return double(cache_key: 'cereal-key-2')
65
+
66
+ expect(Rails).to receive_message_chain(:cache, :fetch_multi).with(
67
+ %w[cereal-key-1 cereal-key-2]
68
+ ).and_yield('cereal-key-1').and_return(name: name1)
69
+ .and_yield('cereal-key-2').and_return(name: name2)
70
+
71
+ subject.as_json
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe CacheCrispies::Condition do
4
+ let(:block) { -> {} }
5
+ subject { described_class.new(block) }
6
+
7
+ describe '#uid' do
8
+ it "is the same as the block's object_id" do
9
+ expect(subject.uid).to be block.object_id
10
+ end
11
+ end
12
+
13
+ describe '#true_for?' do
14
+ let(:block) { ->(_arg1, _arg2) { 'truthy string' } }
15
+ let(:model) { Object.new }
16
+ let(:options) { {} }
17
+
18
+ it 'calls the block with model and options arguments' do
19
+ expect(block).to receive(:call).with(model, options)
20
+ subject.true_for? model, options
21
+ end
22
+
23
+ context 'when the block has one argument' do
24
+ let(:block) { ->(_arg1) { } }
25
+
26
+ it 'calls the block with the model only' do
27
+ expect(block).to receive(:call).with(model)
28
+ subject.true_for? model, options
29
+ end
30
+ end
31
+
32
+ context 'when the block has no arguments' do
33
+ let(:block) { -> {} }
34
+
35
+ it 'calls the block with no arguments' do
36
+ expect(block).to receive(:call).with(no_args)
37
+ subject.true_for? model, options
38
+ end
39
+ end
40
+
41
+ it 'returns a boolean' do
42
+ expect(subject.true_for?(model, options)).to be true
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,54 @@
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
+ serialize :name
17
+
18
+ def self.key
19
+ 'cereal'
20
+ end
21
+ end
22
+
23
+ let(:cereal_names) { ['Count Chocula', 'Eyeholes'] }
24
+ let(:collection) { cereal_names.map { |name| Cereal.new(name: name) } }
25
+
26
+ subject { CerealController.new }
27
+
28
+ before do
29
+ allow_any_instance_of(
30
+ CacheCrispies::Plan
31
+ ).to receive(:etag).and_return 'test-etag'
32
+ allow(subject).to receive_message_chain(:response, :weak_etag=).with 'test-etag'
33
+ end
34
+
35
+ describe '#cache_render' do
36
+ it 'renders a json collection' do
37
+ expect(subject).to receive(:render).with(
38
+ json: {
39
+ cereals: cereal_names.map { |name| { name: name } }
40
+ }.to_json
41
+ )
42
+
43
+ subject.cache_render CerealSerializerForController, collection
44
+ end
45
+
46
+ it 'renders a single json object' do
47
+ expect(subject).to receive(:render).with(
48
+ json: { cereal: { name: cereal_names.first } }.to_json
49
+ )
50
+
51
+ subject.cache_render CerealSerializerForController, collection.first
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,2 @@
1
+ # Nothing has to actually be in this file.
2
+ # It's just here so test can MD5 sum it's content.
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe CacheCrispies::HashBuilder do
4
+ # These example serializers are meant to show a variety of options,
5
+ # configurations, and data types in order to really put the HashBuilder class
6
+ # through the ringer.
7
+ class IngredientSerializer < CacheCrispies::Base
8
+ serialize :name
9
+ end
10
+
11
+ class MarketingBsSerializer < CacheCrispies::Base
12
+ serialize :tagline, :small_print
13
+
14
+ def tagline
15
+ "#{model.tagline}#{options[:footnote_marker]}"
16
+ end
17
+
18
+ def small_print
19
+ "#{options[:footnote_marker]}this doesn't mean jack-squat"
20
+ end
21
+ end
22
+
23
+ class CerealSerializerForHashBuilder < CacheCrispies::Base
24
+ serialize :uid, from: :id, to: String
25
+ serialize :name, :company
26
+ merge :itself, with: MarketingBsSerializer
27
+
28
+ nest_in :about do
29
+ nest_in :nutritional_information do
30
+ serialize :calories
31
+ serialize :ingredients, with: IngredientSerializer
32
+ end
33
+ end
34
+
35
+ show_if ->(model, options) { options[:be_trendy] } do
36
+ nest_in :health do
37
+ serialize :organic
38
+
39
+ show_if ->(model) { model.organic } do
40
+ serialize :certification
41
+ end
42
+ end
43
+ end
44
+
45
+ def certification
46
+ 'Totally Not A Scam Certifiers Inc'
47
+ end
48
+ end
49
+
50
+ let(:organic) { false }
51
+ let(:ingredients) {
52
+ [
53
+ OpenStruct.new(name: 'Sugar'),
54
+ OpenStruct.new(name: 'Other Kind of Sugar')
55
+ ]
56
+ }
57
+ let(:model) {
58
+ OpenStruct.new(
59
+ id: 42,
60
+ name: 'Lucky Charms',
61
+ company: 'General Mills',
62
+ calories: 1_000,
63
+ organic: organic,
64
+ tagline: "Part of a balanced breakfast",
65
+ ingredients: ingredients
66
+ )
67
+ }
68
+ let(:options) { { footnote_marker: '*' } }
69
+ let(:serializer) { CerealSerializerForHashBuilder.new(model, options) }
70
+ subject { described_class.new(serializer) }
71
+
72
+ describe '#call' do
73
+ it 'correctly renders the hash' do
74
+ expect(subject.call).to eq ({
75
+ uid: '42',
76
+ name: 'Lucky Charms',
77
+ company: 'General Mills',
78
+ tagline: 'Part of a balanced breakfast*',
79
+ small_print: "*this doesn't mean jack-squat",
80
+ about: {
81
+ nutritional_information: {
82
+ calories: 1000,
83
+ ingredients: [
84
+ { name: 'Sugar' },
85
+ { name: 'Other Kind of Sugar' },
86
+ ]
87
+ }
88
+ }
89
+ })
90
+ end
91
+
92
+ context 'when the outer show_if is true' do
93
+ let(:options) { { footnote_marker: '†', be_trendy: true } }
94
+
95
+ it 'builds values wrapped in the outer if' do
96
+ expect(subject.call).to eq ({
97
+ uid: '42',
98
+ name: 'Lucky Charms',
99
+ company: 'General Mills',
100
+ tagline: 'Part of a balanced breakfast†',
101
+ small_print: "†this doesn't mean jack-squat",
102
+ about: {
103
+ nutritional_information: {
104
+ calories: 1000,
105
+ ingredients: [
106
+ { name: 'Sugar' },
107
+ { name: 'Other Kind of Sugar' },
108
+ ]
109
+ }
110
+ },
111
+ health: {
112
+ organic: false
113
+ }
114
+ })
115
+ end
116
+
117
+ context 'when the inner show_if is true' do
118
+ let(:organic) { true }
119
+
120
+ it 'builds values wrapped in the outer and inner if' do
121
+ expect(subject.call).to eq ({
122
+ uid: '42',
123
+ name: 'Lucky Charms',
124
+ company: 'General Mills',
125
+ tagline: 'Part of a balanced breakfast†',
126
+ small_print: "†this doesn't mean jack-squat",
127
+ about: {
128
+ nutritional_information: {
129
+ calories: 1000,
130
+ ingredients: [
131
+ { name: 'Sugar' },
132
+ { name: 'Other Kind of Sugar' },
133
+ ]
134
+ }
135
+ },
136
+ health: {
137
+ organic: true,
138
+ certification: 'Totally Not A Scam Certifiers Inc'
139
+ }
140
+ })
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe CacheCrispies::Memoizer do
4
+ describe '#fetch' do
5
+ it 'only calls the block once per key' do
6
+ block = -> {}
7
+
8
+ expect { |block| subject.fetch 1, &block }.to yield_with_no_args
9
+ expect { |block| subject.fetch 1, &block }.to_not yield_with_no_args
10
+ expect { |block| subject.fetch 2, &block }.to yield_with_no_args
11
+ end
12
+
13
+ it "returns the block's initial cached value" do
14
+ block = -> {
15
+ @num ||= 0
16
+ @num += 1
17
+ }
18
+
19
+ expect(subject.fetch(:a, &block)).to eq 1
20
+ expect(subject.fetch(:a, &block)).to eq 1
21
+ expect(subject.fetch(:b, &block)).to eq 2
22
+ end
23
+ end
24
+ end
data/spec/plan_spec.rb ADDED
@@ -0,0 +1,151 @@
1
+ require 'spec_helper'
2
+
3
+ describe CacheCrispies::Plan do
4
+ class CerealSerializerForPlan < CacheCrispies::Base
5
+ def self.key
6
+ :cereal
7
+ end
8
+
9
+ def self.cache_key_addons(options)
10
+ ['addon1', options[:extra_addon]]
11
+ end
12
+
13
+ serialize :name
14
+ end
15
+
16
+ let(:serializer) { CerealSerializerForPlan }
17
+ let(:serializer_file_path) {
18
+ File.expand_path('fixtures/test_serializer.rb', __dir__)
19
+ }
20
+ let(:model_cache_key) { 'model-cache-key' }
21
+ let(:model) { OpenStruct.new(name: 'Sugar Smacks', cache_key: model_cache_key) }
22
+ let(:cacheable) { model }
23
+ let(:options) { {} }
24
+ let(:instance) { described_class.new(serializer, cacheable, options) }
25
+ subject { instance }
26
+
27
+ before do
28
+ allow(Rails).to receive_message_chain(:root, :join).and_return(
29
+ serializer_file_path
30
+ )
31
+ end
32
+
33
+ describe '#collection?' do
34
+ context 'when not a collection' do
35
+ let(:cacheable) { Object.new }
36
+
37
+ it 'returns false' do
38
+ expect(subject.collection?).to be false
39
+ end
40
+ end
41
+
42
+ context 'when a collection' do
43
+ let(:cacheable) { [Object.new] }
44
+
45
+ it 'returns false' do
46
+ expect(subject.collection?).to be true
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#etag' do
52
+
53
+ end
54
+
55
+ describe '#cache_key' do
56
+ let(:options) { { extra_addon: 'addon2' } }
57
+
58
+ it 'returns a string' do
59
+ expect(subject.cache_key).to be_a String
60
+ end
61
+
62
+ it 'includes the CACHE_KEY_PREFIX' do
63
+ expect(subject.cache_key).to include CacheCrispies::CACHE_KEY_PREFIX
64
+ end
65
+
66
+ it "includes the serializer's #cache_key_base" do
67
+ expect(subject.cache_key).to include serializer.cache_key_base
68
+ end
69
+
70
+ it "includes the addons_key" do
71
+ expect(subject.cache_key).to include(
72
+ Digest::MD5.hexdigest('addon1|addon2')
73
+ )
74
+ end
75
+
76
+ it "includes the cacheable #cache_key" do
77
+ expect(subject.cache_key).to include model_cache_key
78
+ end
79
+
80
+ it 'includes the CACHE_KEY_SEPARATOR' do
81
+ expect(subject.cache_key).to include CacheCrispies::CACHE_KEY_SEPARATOR
82
+ end
83
+
84
+ it 'generates the key correctly' do
85
+ expect(subject.cache_key).to eq(
86
+ 'cache-crispies' \
87
+ "+CerealSerializerForPlan-#{Digest::MD5.file(serializer_file_path)}" \
88
+ "+#{Digest::MD5.hexdigest('addon1|addon2')}" \
89
+ '+model-cache-key'
90
+ )
91
+ end
92
+
93
+ context 'without addons' do
94
+ it 'generates the key without that section' do
95
+ expect(serializer).to receive(:cache_key_addons).and_return []
96
+
97
+ expect(subject.cache_key).to eq(
98
+ 'cache-crispies' \
99
+ "+CerealSerializerForPlan-#{Digest::MD5.file(serializer_file_path)}" \
100
+ '+model-cache-key'
101
+ )
102
+ end
103
+ end
104
+ end
105
+
106
+ describe '#cache' do
107
+ end
108
+
109
+ describe '#wrap' do
110
+ let(:json_hash) { { name: 'Kix' } }
111
+ subject { instance.wrap(json_hash) }
112
+
113
+ context 'when the serializer has no key' do
114
+ before { expect(serializer).to receive(:key).and_return nil }
115
+
116
+ it 'returns the json Hash directly' do
117
+ expect(subject).to be json_hash
118
+ end
119
+ end
120
+
121
+ context 'when key is false' do
122
+ let(:options) { { key: false } }
123
+
124
+ it 'returns json_hash unchanged' do
125
+ expect(subject).to be json_hash
126
+ end
127
+ end
128
+
129
+ context 'with an optional key' do
130
+ let(:options) { { key: :cereal_test } }
131
+
132
+ it 'wraps the hash using the provided key option' do
133
+ expect(subject).to eq cereal_test: json_hash
134
+ end
135
+ end
136
+
137
+ context "when it's a colleciton" do
138
+ let(:cacheable) { [model] }
139
+
140
+ it "wraps the hash in the serializer's colleciton_key" do
141
+ expect(subject).to eq cereals: json_hash
142
+ end
143
+ end
144
+
145
+ context "when it's not a collection" do
146
+ it "wraps the hash in the serializer's key" do
147
+ expect(subject).to eq cereal: json_hash
148
+ end
149
+ end
150
+ end
151
+ end