cache_crispies 0.1.0

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.
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