rails-patterns 0.6.0 → 0.10.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 +5 -2
- data/Gemfile.lock +79 -60
- data/README.md +100 -9
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/lib/patterns/calculation.rb +20 -6
- data/lib/patterns/rule.rb +27 -0
- data/lib/patterns/ruleset.rb +71 -0
- data/lib/patterns/service.rb +10 -6
- data/lib/patterns/strong_ruleset.rb +21 -0
- data/lib/rails-patterns.rb +3 -0
- data/rails-patterns.gemspec +24 -10
- 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 +49 -0
- 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 +1 -0
- metadata +37 -14
data/rails-patterns.gemspec
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: rails-patterns 0.
|
|
5
|
+
# stub: rails-patterns 0.10.0 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "rails-patterns".freeze
|
|
9
|
-
s.version = "0.
|
|
9
|
+
s.version = "0.10.0"
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
13
13
|
s.authors = ["Stevo".freeze]
|
|
14
|
-
s.date = "
|
|
14
|
+
s.date = "2021-12-10"
|
|
15
15
|
s.description = "A collection of lightweight, standardized, rails-oriented patterns.".freeze
|
|
16
16
|
s.email = "b.kosmowski@selleo.com".freeze
|
|
17
17
|
s.extra_rdoc_files = [
|
|
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
|
|
|
20
20
|
]
|
|
21
21
|
s.files = [
|
|
22
22
|
".document",
|
|
23
|
+
".github/workflows/ruby.yml",
|
|
23
24
|
".rspec",
|
|
24
25
|
"Gemfile",
|
|
25
26
|
"Gemfile.lock",
|
|
@@ -32,19 +33,29 @@ Gem::Specification.new do |s|
|
|
|
32
33
|
"lib/patterns/collection.rb",
|
|
33
34
|
"lib/patterns/form.rb",
|
|
34
35
|
"lib/patterns/query.rb",
|
|
36
|
+
"lib/patterns/rule.rb",
|
|
37
|
+
"lib/patterns/ruleset.rb",
|
|
35
38
|
"lib/patterns/service.rb",
|
|
39
|
+
"lib/patterns/strong_ruleset.rb",
|
|
36
40
|
"lib/rails-patterns.rb",
|
|
37
41
|
"rails-patterns.gemspec",
|
|
42
|
+
"spec/helpers/custom_calculation.rb",
|
|
43
|
+
"spec/helpers/custom_calculation_script.rb",
|
|
44
|
+
"spec/helpers/rails_redis_cache_mock.rb",
|
|
38
45
|
"spec/patterns/calculation_spec.rb",
|
|
39
46
|
"spec/patterns/collection_spec.rb",
|
|
40
47
|
"spec/patterns/form_spec.rb",
|
|
41
48
|
"spec/patterns/query_spec.rb",
|
|
49
|
+
"spec/patterns/rule_spec.rb",
|
|
50
|
+
"spec/patterns/ruleset_spec.rb",
|
|
42
51
|
"spec/patterns/service_spec.rb",
|
|
52
|
+
"spec/patterns/strong_ruleset_spec.rb",
|
|
43
53
|
"spec/spec_helper.rb"
|
|
44
54
|
]
|
|
45
55
|
s.homepage = "http://github.com/selleo/pattern".freeze
|
|
46
56
|
s.licenses = ["MIT".freeze]
|
|
47
|
-
s.
|
|
57
|
+
s.required_ruby_version = Gem::Requirement.new(">= 2.5.0".freeze)
|
|
58
|
+
s.rubygems_version = "3.0.8".freeze
|
|
48
59
|
s.summary = "A collection of lightweight, standardized, rails-oriented patterns.".freeze
|
|
49
60
|
|
|
50
61
|
if s.respond_to? :specification_version then
|
|
@@ -54,24 +65,27 @@ Gem::Specification.new do |s|
|
|
|
54
65
|
s.add_runtime_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
|
55
66
|
s.add_runtime_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
|
56
67
|
s.add_runtime_dependency(%q<virtus>.freeze, [">= 0"])
|
|
68
|
+
s.add_runtime_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
|
|
57
69
|
s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
|
|
58
|
-
s.add_development_dependency(%q<bundler>.freeze, ["~>
|
|
59
|
-
s.add_development_dependency(%q<juwelier>.freeze, ["
|
|
70
|
+
s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
|
71
|
+
s.add_development_dependency(%q<juwelier>.freeze, [">= 0"])
|
|
60
72
|
else
|
|
61
73
|
s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
|
62
74
|
s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
|
63
75
|
s.add_dependency(%q<virtus>.freeze, [">= 0"])
|
|
76
|
+
s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
|
|
64
77
|
s.add_dependency(%q<rspec>.freeze, [">= 0"])
|
|
65
|
-
s.add_dependency(%q<bundler>.freeze, ["~>
|
|
66
|
-
s.add_dependency(%q<juwelier>.freeze, ["
|
|
78
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
|
79
|
+
s.add_dependency(%q<juwelier>.freeze, [">= 0"])
|
|
67
80
|
end
|
|
68
81
|
else
|
|
69
82
|
s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
|
70
83
|
s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
|
71
84
|
s.add_dependency(%q<virtus>.freeze, [">= 0"])
|
|
85
|
+
s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
|
|
72
86
|
s.add_dependency(%q<rspec>.freeze, [">= 0"])
|
|
73
|
-
s.add_dependency(%q<bundler>.freeze, ["~>
|
|
74
|
-
s.add_dependency(%q<juwelier>.freeze, ["
|
|
87
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
|
88
|
+
s.add_dependency(%q<juwelier>.freeze, [">= 0"])
|
|
75
89
|
end
|
|
76
90
|
end
|
|
77
91
|
|
|
@@ -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
|
|
@@ -14,6 +14,7 @@ RSpec.describe Patterns::Calculation do
|
|
|
14
14
|
after do
|
|
15
15
|
Object.send(:remove_const, :CustomCalculation) if defined?(CustomCalculation)
|
|
16
16
|
Rails.cache.clear
|
|
17
|
+
ActiveSupport::Cache::RedisCacheStore.new.clear
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
describe ".result" do
|
|
@@ -59,6 +60,18 @@ RSpec.describe Patterns::Calculation do
|
|
|
59
60
|
|
|
60
61
|
expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
|
|
61
62
|
end
|
|
63
|
+
|
|
64
|
+
it 'executes calculation with given block' do
|
|
65
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def result
|
|
69
|
+
yield(subject)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
expect(CustomCalculation.result(5) { |a| a * 3 }).to eq(15)
|
|
74
|
+
end
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
describe "caching" do
|
|
@@ -159,5 +172,41 @@ RSpec.describe Patterns::Calculation do
|
|
|
159
172
|
expect(CustomCalculation.result).to eq 1
|
|
160
173
|
expect(CustomCalculation.result).to eq 2
|
|
161
174
|
end
|
|
175
|
+
|
|
176
|
+
describe "when RedisCacheStore is used" do
|
|
177
|
+
it "does not store data in cache if 'cache_expiry_period' is not set" do
|
|
178
|
+
client = Redis.new
|
|
179
|
+
class Rails
|
|
180
|
+
def self.cache
|
|
181
|
+
@cache ||= ActiveSupport::Cache::RedisCacheStore.new
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
CustomCalculation = Class.new(Patterns::Calculation) do
|
|
186
|
+
class_attribute :counter
|
|
187
|
+
self.counter = 0
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def result
|
|
192
|
+
self.class.counter += 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
expect(CustomCalculation.result).to eq 1
|
|
197
|
+
expect(CustomCalculation.result).to eq 2
|
|
198
|
+
expect(client.keys).to be_empty
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "uses cache keys consistent between processes" do
|
|
203
|
+
`bundle exec ruby spec/helpers/custom_calculation.rb`
|
|
204
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
|
205
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
|
206
|
+
Process.spawn('bundle exec ruby spec/helpers/custom_calculation_script.rb')
|
|
207
|
+
Process.waitall
|
|
208
|
+
|
|
209
|
+
expect(Redis.new.keys.length).to eq 1
|
|
210
|
+
end
|
|
162
211
|
end
|
|
163
212
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
RSpec.describe Patterns::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(Patterns::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(Patterns::Rule)
|
|
15
|
+
CustomRule = Class.new(Patterns::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(Patterns::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
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
RSpec.describe Patterns::Ruleset do
|
|
2
|
+
context 'when empty ruleset is initialized' do
|
|
3
|
+
it 'raises an error' do
|
|
4
|
+
empty_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
5
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
6
|
+
subject = double
|
|
7
|
+
|
|
8
|
+
with_mocked_rules do |rules|
|
|
9
|
+
rules << mock_rule(:rule_1)
|
|
10
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
11
|
+
|
|
12
|
+
expect { custom_ruleset_klass.new(subject) }.not_to raise_error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
expect { empty_ruleset_klass.new(subject) }.to raise_error Patterns::Ruleset::EmptyRuleset
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '#forceable?' do
|
|
20
|
+
context 'all rules are forceable' do
|
|
21
|
+
it 'returns true' do
|
|
22
|
+
with_mocked_rules do |rules|
|
|
23
|
+
subject = double
|
|
24
|
+
rules << mock_rule(:rule_1, is_forceable: true)
|
|
25
|
+
rules << mock_rule(:rule_2, is_forceable: true)
|
|
26
|
+
|
|
27
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
28
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
29
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
30
|
+
|
|
31
|
+
expect(custom_ruleset_klass.new(subject).forceable?).to eq true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'at least one rule is not forceable' do
|
|
37
|
+
it 'returns false' do
|
|
38
|
+
with_mocked_rules do |rules|
|
|
39
|
+
subject = double
|
|
40
|
+
rules << mock_rule(:rule_1, is_forceable: false, is_satisfied: false, is_applicable: true)
|
|
41
|
+
rules << mock_rule(:rule_2, is_forceable: true)
|
|
42
|
+
|
|
43
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
44
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
45
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
46
|
+
|
|
47
|
+
expect(custom_ruleset_klass.new(subject).forceable?).to eq false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'and rule is satisfied' do
|
|
52
|
+
it 'returns true' do
|
|
53
|
+
with_mocked_rules do |rules|
|
|
54
|
+
subject = double
|
|
55
|
+
rules << mock_rule(
|
|
56
|
+
:rule_1,
|
|
57
|
+
is_forceable: false,
|
|
58
|
+
is_satisfied: true,
|
|
59
|
+
is_applicable: true
|
|
60
|
+
)
|
|
61
|
+
rules << mock_rule(:rule_2, is_forceable: true)
|
|
62
|
+
|
|
63
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
64
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
65
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
66
|
+
|
|
67
|
+
expect(custom_ruleset_klass.new(subject).forceable?).to eq true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'and rule is not applicable' do
|
|
73
|
+
it 'returns true' do
|
|
74
|
+
with_mocked_rules do |rules|
|
|
75
|
+
subject = double
|
|
76
|
+
rules << mock_rule(
|
|
77
|
+
:rule_1,
|
|
78
|
+
is_forceable: false,
|
|
79
|
+
is_satisfied: false,
|
|
80
|
+
is_applicable: false
|
|
81
|
+
)
|
|
82
|
+
rules << mock_rule(:rule_2, is_forceable: true)
|
|
83
|
+
|
|
84
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
85
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
86
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
87
|
+
|
|
88
|
+
expect(custom_ruleset_klass.new(subject).forceable?).to eq true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '#not_applicable?' do
|
|
96
|
+
context 'all rules are not applicable' do
|
|
97
|
+
it 'returns true' do
|
|
98
|
+
with_mocked_rules do |rules|
|
|
99
|
+
subject = double
|
|
100
|
+
rules << mock_rule(:rule_1, is_applicable: false)
|
|
101
|
+
rules << mock_rule(:rule_2, is_applicable: false)
|
|
102
|
+
|
|
103
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
104
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
105
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
106
|
+
|
|
107
|
+
expect(custom_ruleset_klass.new(subject).not_applicable?).to eq true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
context 'at least one rule is applicable' do
|
|
113
|
+
it 'returns false' do
|
|
114
|
+
with_mocked_rules do |rules|
|
|
115
|
+
subject = double
|
|
116
|
+
rules << mock_rule(:rule_1, is_applicable: false)
|
|
117
|
+
rules << mock_rule(:rule_2, is_applicable: true)
|
|
118
|
+
|
|
119
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
120
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
121
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
122
|
+
|
|
123
|
+
expect(custom_ruleset_klass.new(subject).not_applicable?).to eq false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#satisfied?' do
|
|
130
|
+
context 'all rules are satisfied' do
|
|
131
|
+
it 'returns true' do
|
|
132
|
+
with_mocked_rules do |rules|
|
|
133
|
+
subject = double
|
|
134
|
+
rules << mock_rule(:rule_1)
|
|
135
|
+
rules << mock_rule(:rule_2)
|
|
136
|
+
|
|
137
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
138
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
139
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
140
|
+
|
|
141
|
+
expect(custom_ruleset_klass.new(subject).satisfied?).to eq true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'at least one rule is not satisfied' do
|
|
147
|
+
it 'returns false' do
|
|
148
|
+
with_mocked_rules do |rules|
|
|
149
|
+
subject = double
|
|
150
|
+
rules << mock_rule(:rule_1)
|
|
151
|
+
rules << mock_rule(:rule_2, is_satisfied: false)
|
|
152
|
+
|
|
153
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
154
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
155
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
156
|
+
|
|
157
|
+
expect(custom_ruleset_klass.new(subject).satisfied?).to eq false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
context 'when rule is not applicable' do
|
|
162
|
+
it 'returns true' do
|
|
163
|
+
with_mocked_rules do |rules|
|
|
164
|
+
subject = double
|
|
165
|
+
rules << mock_rule(:rule_1)
|
|
166
|
+
rules << mock_rule(:rule_2, is_satisfied: false, is_applicable: false)
|
|
167
|
+
|
|
168
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
169
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
170
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
171
|
+
|
|
172
|
+
expect(custom_ruleset_klass.new(subject).satisfied?).to eq true
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'when provided with force: true' do
|
|
178
|
+
context 'when rule is forceable' do
|
|
179
|
+
it 'returns true' do
|
|
180
|
+
with_mocked_rules do |rules|
|
|
181
|
+
subject = double
|
|
182
|
+
rules << mock_rule(:rule_1)
|
|
183
|
+
rules << mock_rule(:rule_2, is_satisfied: false, is_forceable: true)
|
|
184
|
+
|
|
185
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
186
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
187
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
188
|
+
|
|
189
|
+
expect(custom_ruleset_klass.new(subject).satisfied?(force: true)).to eq true
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context 'when rule is not forceable' do
|
|
195
|
+
it 'returns false' do
|
|
196
|
+
with_mocked_rules do |rules|
|
|
197
|
+
subject = double
|
|
198
|
+
rules << mock_rule(:rule_1)
|
|
199
|
+
rules << mock_rule(:rule_2, is_satisfied: false, is_forceable: false)
|
|
200
|
+
|
|
201
|
+
custom_ruleset_klass = Class.new(Patterns::Ruleset)
|
|
202
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
203
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
204
|
+
|
|
205
|
+
expect(custom_ruleset_klass.new(subject).satisfied?(force: true)).to eq false
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
describe '#each' do
|
|
214
|
+
it 'yields all rules for ruleset' do
|
|
215
|
+
with_mocked_rules do |rules|
|
|
216
|
+
rules << (_, rule_1 = mock_rule(:rule_1))
|
|
217
|
+
rules << (_, rule_2 = mock_rule(:rule_2))
|
|
218
|
+
rules << (_, rule_3 = mock_rule(:rule_3))
|
|
219
|
+
custom_ruleset_klass_1 = Class.new(Patterns::Ruleset)
|
|
220
|
+
custom_ruleset_klass_1.add_rule(:rule_1)
|
|
221
|
+
custom_ruleset_klass_1.add_rule(:rule_2)
|
|
222
|
+
Ruleset2 = Class.new(Patterns::Ruleset)
|
|
223
|
+
Ruleset2.add_rule(:rule_3)
|
|
224
|
+
custom_ruleset_klass_1.add_rule(:ruleset_2)
|
|
225
|
+
|
|
226
|
+
ruleset = custom_ruleset_klass_1.new(double)
|
|
227
|
+
|
|
228
|
+
expect { |b| ruleset.each(&b) }.to yield_successive_args(rule_1, rule_2, rule_3)
|
|
229
|
+
ensure
|
|
230
|
+
remove_class(Ruleset2)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def mock_rule(rule_name, is_applicable: true, is_satisfied: true, is_forceable: true)
|
|
238
|
+
klass = Object.const_set(rule_name.to_s.classify, Class.new(Patterns::Rule))
|
|
239
|
+
rule = double(
|
|
240
|
+
not_applicable?: !is_applicable,
|
|
241
|
+
satisfied?: is_satisfied,
|
|
242
|
+
forceable?: is_forceable
|
|
243
|
+
)
|
|
244
|
+
allow(klass).to receive(:new).with(anything) { rule }
|
|
245
|
+
[klass, rule]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def with_mocked_rules
|
|
249
|
+
rules_storage = []
|
|
250
|
+
yield rules_storage
|
|
251
|
+
ensure
|
|
252
|
+
rules_storage.each do |rule_klass, _rule_instance|
|
|
253
|
+
remove_class(rule_klass)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def remove_class(klass)
|
|
258
|
+
Object.send(:remove_const, klass.name.to_sym)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -10,7 +10,7 @@ RSpec.describe Patterns::Service do
|
|
|
10
10
|
expect(DoSomething.call).to be_kind_of(DoSomething)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
it "instantiates service object passing arguments to constructor" do
|
|
13
|
+
it "instantiates service object passing keyword arguments to constructor" do
|
|
14
14
|
DoSomething = Class.new(Patterns::Service) do
|
|
15
15
|
def initialize(argument_1:, argument_2:); end
|
|
16
16
|
def call; end
|
|
@@ -25,6 +25,21 @@ RSpec.describe Patterns::Service do
|
|
|
25
25
|
}.not_to raise_error
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
it "instantiates service object passing positional arguments to constructor" do
|
|
29
|
+
DoSomething = Class.new(Patterns::Service) do
|
|
30
|
+
def initialize(argument_1, argument_2); end
|
|
31
|
+
def call; end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect {
|
|
35
|
+
DoSomething.call
|
|
36
|
+
}.to raise_error ArgumentError
|
|
37
|
+
|
|
38
|
+
expect {
|
|
39
|
+
DoSomething.call(10, 20)
|
|
40
|
+
}.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
|
|
28
43
|
it "calls #call method on service object instance" do
|
|
29
44
|
Spy = Class.new do
|
|
30
45
|
def self.some_method; end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
RSpec.describe Patterns::StrongRuleset do
|
|
2
|
+
it 'inherites from Ruleset' do
|
|
3
|
+
custom_strong_ruleset_klass = Class.new(Patterns::StrongRuleset)
|
|
4
|
+
expect(custom_strong_ruleset_klass.ancestors).to include Patterns::Ruleset
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
context 'when any of rules is not applicable' do
|
|
8
|
+
it 'is not satisfied' do
|
|
9
|
+
with_mocked_rules do |rules|
|
|
10
|
+
subject = double
|
|
11
|
+
rules << mock_rule(:rule_1, is_applicable: false)
|
|
12
|
+
rules << mock_rule(:rule_2)
|
|
13
|
+
|
|
14
|
+
custom_ruleset_klass = Class.new(Patterns::StrongRuleset)
|
|
15
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
16
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
17
|
+
|
|
18
|
+
expect(custom_ruleset_klass.new(subject).satisfied?).to eq false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'when not applicable rule is not satisfied' do
|
|
23
|
+
it 'is not forceable' do
|
|
24
|
+
with_mocked_rules do |rules|
|
|
25
|
+
subject = double
|
|
26
|
+
rules << mock_rule(:rule_1, is_applicable: false, is_satisfied: false)
|
|
27
|
+
rules << mock_rule(:rule_2)
|
|
28
|
+
|
|
29
|
+
custom_ruleset_klass = Class.new(Patterns::StrongRuleset)
|
|
30
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
31
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
32
|
+
|
|
33
|
+
expect(custom_ruleset_klass.new(subject).forceable?).to eq false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'is not applicable' do
|
|
39
|
+
with_mocked_rules do |rules|
|
|
40
|
+
subject = double
|
|
41
|
+
rules << mock_rule(:rule_1, is_applicable: false)
|
|
42
|
+
rules << mock_rule(:rule_2)
|
|
43
|
+
|
|
44
|
+
custom_ruleset_klass = Class.new(Patterns::StrongRuleset)
|
|
45
|
+
custom_ruleset_klass.add_rule(:rule_1)
|
|
46
|
+
custom_ruleset_klass.add_rule(:rule_2)
|
|
47
|
+
|
|
48
|
+
expect(custom_ruleset_klass.new(subject).applicable?).to eq false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def mock_rule(rule_name, is_applicable: true, is_satisfied: true, is_forceable: true)
|
|
56
|
+
klass = Object.const_set(rule_name.to_s.classify, Class.new(Patterns::Rule))
|
|
57
|
+
rule = double(
|
|
58
|
+
not_applicable?: !is_applicable,
|
|
59
|
+
applicable?: is_applicable,
|
|
60
|
+
satisfied?: is_satisfied,
|
|
61
|
+
forceable?: is_forceable
|
|
62
|
+
)
|
|
63
|
+
allow(klass).to receive(:new).with(anything) { rule }
|
|
64
|
+
[klass, rule]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def with_mocked_rules
|
|
68
|
+
rules_storage = []
|
|
69
|
+
yield rules_storage
|
|
70
|
+
ensure
|
|
71
|
+
rules_storage.each do |rule_klass, _rule_instance|
|
|
72
|
+
remove_class(rule_klass)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def remove_class(klass)
|
|
77
|
+
Object.send(:remove_const, klass.name.to_sym)
|
|
78
|
+
end
|
|
79
|
+
end
|