rails-patterns 0.4.0 → 0.8.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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +33 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +85 -75
- data/README.md +164 -6
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/lib/patterns/calculation.rb +59 -0
- data/lib/patterns/form.rb +27 -10
- data/lib/patterns/query.rb +20 -15
- data/lib/patterns/rule.rb +25 -0
- data/lib/patterns/ruleset.rb +69 -0
- data/lib/patterns/service.rb +10 -6
- data/lib/patterns/strong_ruleset.rb +19 -0
- data/lib/rails-patterns.rb +4 -0
- data/rails-patterns.gemspec +28 -20
- data/spec/helpers/custom_calculation.rb +16 -0
- data/spec/helpers/custom_calculation_script.rb +4 -0
- data/spec/helpers/rails_redis_cache_mock.rb +5 -0
- data/spec/patterns/calculation_spec.rb +200 -0
- data/spec/patterns/form_spec.rb +96 -50
- data/spec/patterns/rule_spec.rb +44 -0
- data/spec/patterns/ruleset_spec.rb +260 -0
- data/spec/patterns/service_spec.rb +16 -1
- data/spec/patterns/strong_ruleset_spec.rb +79 -0
- data/spec/spec_helper.rb +6 -1
- metadata +35 -10
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'active_support/testing/time_helpers'
|
3
|
+
require_relative 'rails_redis_cache_mock'
|
4
|
+
require_relative '../../lib/patterns/calculation'
|
5
|
+
|
6
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
7
|
+
set_cache_expiry_every 1.hour
|
8
|
+
class_attribute :counter
|
9
|
+
self.counter = 0
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def result
|
14
|
+
self.class.counter += 1
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
RSpec.describe Patterns::Calculation do
|
2
|
+
before(:all) do
|
3
|
+
class Rails
|
4
|
+
def self.cache
|
5
|
+
@cache ||= ActiveSupport::Cache::MemoryStore.new
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:all) do
|
11
|
+
Object.send(:remove_const, :Rails)
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
Object.send(:remove_const, :CustomCalculation) if defined?(CustomCalculation)
|
16
|
+
Rails.cache.clear
|
17
|
+
ActiveSupport::Cache::RedisCacheStore.new.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ".result" do
|
21
|
+
it "returns a result of the calculation within a #result method" do
|
22
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
23
|
+
private
|
24
|
+
|
25
|
+
def result
|
26
|
+
50
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
expect(CustomCalculation.result).to eq 50
|
31
|
+
end
|
32
|
+
|
33
|
+
it "#result, #result_for and #calculate are aliases" do
|
34
|
+
CustomCalculation = Class.new(Patterns::Calculation)
|
35
|
+
|
36
|
+
expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:result_for)
|
37
|
+
expect(CustomCalculation.method(:result)).to eq CustomCalculation.method(:calculate)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "exposes the first argument as a subject" do
|
41
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
42
|
+
private
|
43
|
+
|
44
|
+
def result
|
45
|
+
subject
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
expect(CustomCalculation.result('test')).to eq 'test'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "exposes all keyword arguments using #options" do
|
53
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
54
|
+
private
|
55
|
+
|
56
|
+
def result
|
57
|
+
[options[:arg_1], options[:arg_2]]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "caching" do
|
66
|
+
it "caches result for 'set_cache_expiry_every' period" do
|
67
|
+
travel_to DateTime.new(2017, 1, 1, 12, 0) do
|
68
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
69
|
+
set_cache_expiry_every 1.hour
|
70
|
+
|
71
|
+
class_attribute :counter
|
72
|
+
self.counter = 0
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def result
|
77
|
+
self.class.counter += 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
expect(CustomCalculation.result).to eq 1
|
82
|
+
expect(CustomCalculation.result).to eq 1
|
83
|
+
end
|
84
|
+
|
85
|
+
travel_to DateTime.new(2017, 1, 1, 13, 1) do
|
86
|
+
expect(CustomCalculation.result).to eq 2
|
87
|
+
expect(CustomCalculation.result).to eq 2
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "caches result for every option passed" do
|
92
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
93
|
+
set_cache_expiry_every 1.hour
|
94
|
+
|
95
|
+
class_attribute :counter
|
96
|
+
self.counter = 0
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def result
|
101
|
+
self.class.counter += 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
expect(CustomCalculation.result(123)).to eq 1
|
106
|
+
expect(CustomCalculation.result(123)).to eq 1
|
107
|
+
expect(CustomCalculation.result(1024)).to eq 2
|
108
|
+
expect(CustomCalculation.result(1024)).to eq 2
|
109
|
+
expect(CustomCalculation.result(1024, arg: 1)).to eq 3
|
110
|
+
expect(CustomCalculation.result(1024, arg: 1)).to eq 3
|
111
|
+
end
|
112
|
+
|
113
|
+
it "caches result for every option passed dependant on the class" do
|
114
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
115
|
+
set_cache_expiry_every 1.hour
|
116
|
+
|
117
|
+
class_attribute :counter
|
118
|
+
self.counter = 0
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def result
|
123
|
+
self.class.counter += 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
DifferentCalculation = Class.new(Patterns::Calculation) do
|
128
|
+
set_cache_expiry_every 1.hour
|
129
|
+
|
130
|
+
class_attribute :counter
|
131
|
+
self.counter = 100
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def result
|
136
|
+
self.class.counter += 1
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
expect(CustomCalculation.result(123)).to eq 1
|
141
|
+
expect(CustomCalculation.result(123)).to eq 1
|
142
|
+
expect(DifferentCalculation.result(123)).to eq 101
|
143
|
+
expect(DifferentCalculation.result(123)).to eq 101
|
144
|
+
|
145
|
+
Object.send(:remove_const, :DifferentCalculation)
|
146
|
+
end
|
147
|
+
|
148
|
+
it "does not cache result if 'set_cache_expiry_every' is not set" do
|
149
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
150
|
+
class_attribute :counter
|
151
|
+
self.counter = 0
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def result
|
156
|
+
self.class.counter += 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
expect(CustomCalculation.result).to eq 1
|
161
|
+
expect(CustomCalculation.result).to eq 2
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "when RedisCacheStore is used" do
|
165
|
+
it "does not store data in cache if 'cache_expiry_period' is not set" do
|
166
|
+
client = Redis.new
|
167
|
+
class Rails
|
168
|
+
def self.cache
|
169
|
+
@cache ||= ActiveSupport::Cache::RedisCacheStore.new
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
174
|
+
class_attribute :counter
|
175
|
+
self.counter = 0
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def result
|
180
|
+
self.class.counter += 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
expect(CustomCalculation.result).to eq 1
|
185
|
+
expect(CustomCalculation.result).to eq 2
|
186
|
+
expect(client.keys).to be_empty
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it "uses cache keys consistent between processes" do
|
191
|
+
`bundle exec ruby spec/helpers/custom_calculation.rb`
|
192
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
193
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
194
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
195
|
+
Process.waitall
|
196
|
+
|
197
|
+
expect(Redis.new.keys.length).to eq 1
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/spec/patterns/form_spec.rb
CHANGED
@@ -316,7 +316,7 @@ RSpec.describe Patterns::Form do
|
|
316
316
|
end
|
317
317
|
|
318
318
|
describe "#to_model" do
|
319
|
-
it "
|
319
|
+
it "returns itself" do
|
320
320
|
CustomForm = Class.new(Patterns::Form)
|
321
321
|
|
322
322
|
form = CustomForm.new(double)
|
@@ -345,86 +345,132 @@ RSpec.describe Patterns::Form do
|
|
345
345
|
end
|
346
346
|
end
|
347
347
|
|
348
|
-
describe "#
|
349
|
-
|
350
|
-
context "resource
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
CustomForm = Class.new(Patterns::Form)
|
355
|
-
resource = double(model_name: double(param_key: "resource_key"))
|
356
|
-
|
357
|
-
form = CustomForm.new(resource)
|
358
|
-
result = form.model_name
|
359
|
-
|
360
|
-
expect(result).to respond_to(:param_key)
|
361
|
-
expect(result.param_key).to eq "resource_key"
|
362
|
-
end
|
363
|
-
end
|
364
|
-
|
365
|
-
context "param_key is defined" do
|
366
|
-
it "returns param_key" do
|
367
|
-
CustomForm = Class.new(Patterns::Form) do
|
368
|
-
param_key "test_key"
|
369
|
-
end
|
370
|
-
resource = double(model_name: double(param_key: "resource_key"))
|
348
|
+
describe "#to_param" do
|
349
|
+
context "resource exists" do
|
350
|
+
context "resource responds to #to_param" do
|
351
|
+
it "returns resource#to_param" do
|
352
|
+
CustomForm = Class.new(Patterns::Form)
|
353
|
+
resource = double(to_param: 100)
|
371
354
|
|
372
|
-
|
373
|
-
result = form.model_name
|
355
|
+
form = CustomForm.new(resource)
|
374
356
|
|
375
|
-
|
376
|
-
end
|
377
|
-
end
|
357
|
+
expect(form.to_param).to eq 100
|
378
358
|
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
context "resource does not exist" do
|
363
|
+
it "returns nil" do
|
364
|
+
CustomForm = Class.new(Patterns::Form)
|
379
365
|
|
380
|
-
|
381
|
-
context "param_key is not defined" do
|
382
|
-
it "raises NoParamKey" do
|
383
|
-
CustomForm = Class.new(Patterns::Form)
|
366
|
+
form = CustomForm.new
|
384
367
|
|
385
|
-
|
368
|
+
expect(form.to_param).to eq nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
386
372
|
|
387
|
-
|
388
|
-
|
389
|
-
|
373
|
+
describe "#model_name" do
|
374
|
+
context "resource exists" do
|
375
|
+
context "resource responds to #model_name" do
|
376
|
+
context "param_key is not defined" do
|
377
|
+
it "returns object's model name param_key, route_key and singular_route_key" do
|
378
|
+
CustomForm = Class.new(Patterns::Form)
|
379
|
+
resource = double(model_name: double(
|
380
|
+
param_key: "resource_key",
|
381
|
+
route_key: "resource_keys",
|
382
|
+
singular_route_key: "resource_key"
|
383
|
+
))
|
390
384
|
|
391
|
-
|
392
|
-
|
393
|
-
CustomForm = Class.new(Patterns::Form) do
|
394
|
-
param_key "test_key"
|
395
|
-
end
|
385
|
+
form = CustomForm.new(resource)
|
386
|
+
result = form.model_name
|
396
387
|
|
397
|
-
|
398
|
-
|
388
|
+
expect(result).to have_attributes(
|
389
|
+
param_key: "resource_key",
|
390
|
+
route_key: "resource_keys",
|
391
|
+
singular_route_key: "resource_key"
|
392
|
+
)
|
393
|
+
end
|
394
|
+
end
|
399
395
|
|
400
|
-
|
396
|
+
context "param_key is defined" do
|
397
|
+
it "returns param_key, route_key and singular_route_key derived from param key" do
|
398
|
+
CustomForm = Class.new(Patterns::Form) do
|
399
|
+
param_key "test_key"
|
401
400
|
end
|
401
|
+
resource = double(model_name: double(
|
402
|
+
param_key: "resource_key",
|
403
|
+
route_key: "resource_keys",
|
404
|
+
singular_route_key: "resource_key"
|
405
|
+
))
|
406
|
+
|
407
|
+
form = CustomForm.new(resource)
|
408
|
+
result = form.model_name
|
409
|
+
|
410
|
+
expect(result).to have_attributes(
|
411
|
+
param_key: "test_key",
|
412
|
+
route_key: "test_keys",
|
413
|
+
singular_route_key: "test_key"
|
414
|
+
)
|
402
415
|
end
|
403
416
|
end
|
404
417
|
end
|
405
418
|
|
406
|
-
context "resource does not
|
419
|
+
context "resource does not respond to #model_name" do
|
407
420
|
context "param_key is not defined" do
|
408
421
|
it "raises NoParamKey" do
|
409
422
|
CustomForm = Class.new(Patterns::Form)
|
410
423
|
|
411
|
-
form = CustomForm.new
|
424
|
+
form = CustomForm.new(double)
|
412
425
|
|
413
426
|
expect { form.model_name }.to raise_error(Patterns::Form::NoParamKey)
|
414
427
|
end
|
415
428
|
end
|
416
429
|
|
417
430
|
context "param_key is defined" do
|
418
|
-
it "returns param_key" do
|
431
|
+
it "returns param_key, route_key and singular_route_key derived from param key" do
|
419
432
|
CustomForm = Class.new(Patterns::Form) do
|
420
433
|
param_key "test_key"
|
421
434
|
end
|
422
435
|
|
423
|
-
form = CustomForm.new
|
436
|
+
form = CustomForm.new(double)
|
424
437
|
result = form.model_name
|
425
438
|
|
426
|
-
expect(result
|
439
|
+
expect(result).to have_attributes(
|
440
|
+
param_key: "test_key",
|
441
|
+
route_key: "test_keys",
|
442
|
+
singular_route_key: "test_key"
|
443
|
+
)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
context "resource does not exist" do
|
450
|
+
context "param_key is not defined" do
|
451
|
+
it "raises NoParamKey" do
|
452
|
+
CustomForm = Class.new(Patterns::Form)
|
453
|
+
|
454
|
+
form = CustomForm.new
|
455
|
+
|
456
|
+
expect { form.model_name }.to raise_error(Patterns::Form::NoParamKey)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
context "param_key is defined" do
|
461
|
+
it "returns param_key, route_key and singular_route_key derived from param key" do
|
462
|
+
CustomForm = Class.new(Patterns::Form) do
|
463
|
+
param_key "test_key"
|
427
464
|
end
|
465
|
+
|
466
|
+
form = CustomForm.new
|
467
|
+
result = form.model_name
|
468
|
+
|
469
|
+
expect(result).to have_attributes(
|
470
|
+
param_key: "test_key",
|
471
|
+
route_key: "test_keys",
|
472
|
+
singular_route_key: "test_key"
|
473
|
+
)
|
428
474
|
end
|
429
475
|
end
|
430
476
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
RSpec.describe Rule do
|
2
|
+
after(:each) do
|
3
|
+
Object.send(:remove_const, :CustomRule) if defined?(CustomRule)
|
4
|
+
end
|
5
|
+
|
6
|
+
it 'requires subject as the first argument' do
|
7
|
+
CustomRule = Class.new(Rule)
|
8
|
+
|
9
|
+
expect { CustomRule.new }.to raise_error ArgumentError
|
10
|
+
expect { CustomRule.new(Object.new) }.not_to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'requires #satisfied? method to be defined' do
|
14
|
+
InvalidCustomRule = Class.new(Rule)
|
15
|
+
CustomRule = Class.new(Rule) do
|
16
|
+
def satisfied?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
expect { InvalidCustomRule.new(Object.new).satisfied? }.to raise_error NotImplementedError
|
22
|
+
expect { CustomRule.new(Object.new).satisfied? }.not_to raise_error
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#satisfied?' do
|
26
|
+
context 'when subject meets the conditions' do
|
27
|
+
it 'returns true' do
|
28
|
+
article = OpenStruct.new('published?' => true, 'deleted?' => false)
|
29
|
+
|
30
|
+
ArticleIsPublishedRule = Class.new(Rule) do
|
31
|
+
def satisfied?
|
32
|
+
subject.published?
|
33
|
+
end
|
34
|
+
|
35
|
+
def not_applicable?
|
36
|
+
subject.deleted?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
expect(ArticleIsPublishedRule.new(article).satisfied?).to eq true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|