rails-patterns 0.7.3 → 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 +4 -4
- data/Gemfile.lock +14 -14
- data/README.md +80 -0
- data/VERSION +1 -1
- data/lib/patterns/rule.rb +25 -0
- data/lib/patterns/ruleset.rb +69 -0
- data/lib/patterns/strong_ruleset.rb +19 -0
- data/lib/rails-patterns.rb +3 -0
- data/rails-patterns.gemspec +19 -21
- data/spec/patterns/rule_spec.rb +44 -0
- data/spec/patterns/ruleset_spec.rb +260 -0
- data/spec/patterns/strong_ruleset_spec.rb +79 -0
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 738d61388a1488705037348a76d42d69b4b92a83cdd6753a2d14d061768302a1
|
4
|
+
data.tar.gz: 9dcc97a76fa682b76ff8910e4146da62eff304c3a7f511343e31d7b8ed222c36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9961fcdafa85dca7ef1a91641c4867fa348cdeb20a30abda8e8a91ab3a33c9d3ae456d57a2761615c127bf656b793c8bda94e7cc21371a42bd413e57336babfc
|
7
|
+
data.tar.gz: 7a570f8a7836ff4a3404525d41323697850c46c4dae447512ab517acf036799afd7c14a57028f374a5cea8aa8b489d134e32a3bd8278563039242d33546a8f0a
|
data/Gemfile.lock
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
GEM
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
|
-
actionpack (6.0.3.
|
5
|
-
actionview (= 6.0.3.
|
6
|
-
activesupport (= 6.0.3.
|
4
|
+
actionpack (6.0.3.2)
|
5
|
+
actionview (= 6.0.3.2)
|
6
|
+
activesupport (= 6.0.3.2)
|
7
7
|
rack (~> 2.0, >= 2.0.8)
|
8
8
|
rack-test (>= 0.6.3)
|
9
9
|
rails-dom-testing (~> 2.0)
|
10
10
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
11
|
-
actionview (6.0.3.
|
12
|
-
activesupport (= 6.0.3.
|
11
|
+
actionview (6.0.3.2)
|
12
|
+
activesupport (= 6.0.3.2)
|
13
13
|
builder (~> 3.1)
|
14
14
|
erubi (~> 1.4)
|
15
15
|
rails-dom-testing (~> 2.0)
|
16
16
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
17
|
-
activemodel (6.0.3.
|
18
|
-
activesupport (= 6.0.3.
|
19
|
-
activerecord (6.0.3.
|
20
|
-
activemodel (= 6.0.3.
|
21
|
-
activesupport (= 6.0.3.
|
22
|
-
activesupport (6.0.3.
|
17
|
+
activemodel (6.0.3.2)
|
18
|
+
activesupport (= 6.0.3.2)
|
19
|
+
activerecord (6.0.3.2)
|
20
|
+
activemodel (= 6.0.3.2)
|
21
|
+
activesupport (= 6.0.3.2)
|
22
|
+
activesupport (6.0.3.2)
|
23
23
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
24
24
|
i18n (>= 0.7, < 2)
|
25
25
|
minitest (~> 5.1)
|
@@ -54,7 +54,7 @@ GEM
|
|
54
54
|
oauth2 (~> 1.0)
|
55
55
|
hashie (3.6.0)
|
56
56
|
highline (2.0.3)
|
57
|
-
i18n (1.8.
|
57
|
+
i18n (1.8.3)
|
58
58
|
concurrent-ruby (~> 1.0)
|
59
59
|
ice_nine (0.11.2)
|
60
60
|
juwelier (2.1.3)
|
@@ -68,7 +68,7 @@ GEM
|
|
68
68
|
rdoc
|
69
69
|
semver
|
70
70
|
jwt (2.2.1)
|
71
|
-
loofah (2.
|
71
|
+
loofah (2.6.0)
|
72
72
|
crass (~> 1.0.2)
|
73
73
|
nokogiri (>= 1.5.9)
|
74
74
|
method_source (0.9.0)
|
@@ -91,7 +91,7 @@ GEM
|
|
91
91
|
pry-rails (0.3.6)
|
92
92
|
pry (>= 0.10.4)
|
93
93
|
public_suffix (4.0.5)
|
94
|
-
rack (2.2.
|
94
|
+
rack (2.2.3)
|
95
95
|
rack-test (1.1.0)
|
96
96
|
rack (>= 1.0, < 3)
|
97
97
|
rails-dom-testing (2.0.3)
|
data/README.md
CHANGED
@@ -9,6 +9,7 @@ A collection of lightweight, standardized, rails-oriented patterns used by [Ruby
|
|
9
9
|
- [Collection - when in need to add a method that relates to the collection as whole](#collection)
|
10
10
|
- [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)
|
11
11
|
- [Calculation - when you need a place for calculating a simple value (numeric, array, hash) and/or cache it](#calculation)
|
12
|
+
- [Rule and Ruleset - when you need a place for conditional logic](#rule-and-ruleset)
|
12
13
|
|
13
14
|
## Installation
|
14
15
|
|
@@ -347,6 +348,85 @@ TotalCurrentRevenue.calculate
|
|
347
348
|
AverageDailyRevenue.result
|
348
349
|
```
|
349
350
|
|
351
|
+
## Rule and Ruleset
|
352
|
+
|
353
|
+
### When to use it
|
354
|
+
|
355
|
+
Rule objects provide a place for dislocating/extracting conditional logic.
|
356
|
+
|
357
|
+
Use it when:
|
358
|
+
- given complex condition is duplicated in multiple places in your codebase
|
359
|
+
- part of condition logic can be reused in some other place
|
360
|
+
- there is a need to instantiate condition itself for some reason (i.e. to represent it in the interface)
|
361
|
+
- responsibility of your class is blurred by complex conditional logic, and as a result...
|
362
|
+
- ...tests for your class require multiple condition branches / nested contexts
|
363
|
+
|
364
|
+
### Assumptions and rules
|
365
|
+
|
366
|
+
* Rule has `#satisfied?`, `#applicable?`, `#not_applicable?` and `#forceable?` methods available.
|
367
|
+
* Rule has to implement at least `#satisfied?` method. `#not_applicable?` and `#forceable?` are meant to be overridable.
|
368
|
+
* `#forceable?` makes sense in scenario where condition is capable of being force-satisfied regardless if its actually satisfied or not. Is `true` by default.
|
369
|
+
* Override `#not_applicable?` when method is applicable only under some specific conditions. Is `false` by default.
|
370
|
+
* Rule requires a subject as first argument.
|
371
|
+
* Multiple rules and rulesets can be combined into new ruleset as both share same interface and can be used interchangeably (composite pattern).
|
372
|
+
|
373
|
+
#### Forcing rules
|
374
|
+
|
375
|
+
On some occasions there is a situation in which some condition should be overridable.
|
376
|
+
Let's say we may want send shipping notification even though given order was not paid for and under regular circumstances such notification should not be sent.
|
377
|
+
In this case, while regular logic with some automated process would not trigger delivery, an action triggered by user from UI could do it, by passing `force: true` option to `#satisified?` methods.
|
378
|
+
|
379
|
+
It might be good idea to test for `#forceable?` on the UI level to control visibility of such link/button.
|
380
|
+
|
381
|
+
Overriding `#forceable` can be useful to prevent some edge cases, i.e. `ContactInformationProvidedRule` might check if customer for given order has provided any contact means by which a notification could be delivered.
|
382
|
+
If not, ruleset containing such rule (and the rule itself) would not be "forceable" and UI could reflect that by querying `#forceable?`.
|
383
|
+
|
384
|
+
#### Regular and strong rulesets
|
385
|
+
|
386
|
+
While regular `Ruleset` can be satisfied or forced if any of its rules in not applicable, the
|
387
|
+
`StrongRuleset` is not satisfied and not "forceable" if any of its rules is not applicable.
|
388
|
+
|
389
|
+
#### `#not_applicable?` vs `#applicable?`
|
390
|
+
|
391
|
+
It might be surprising that is is the negated version of the `#applicable?` predicate methods that is overridable.
|
392
|
+
However, from the actual usage perspective, it usually easier to conceptually define when condition makes no sense than other way around.
|
393
|
+
|
394
|
+
### Examples
|
395
|
+
|
396
|
+
#### Declaration
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
class OrderIsSentRule < Rule
|
400
|
+
def satisfied?
|
401
|
+
subject.sent?
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
class OrderIsPaidRule < Rule
|
406
|
+
def satisfied?
|
407
|
+
subject.paid?
|
408
|
+
end
|
409
|
+
|
410
|
+
def forceable?
|
411
|
+
true
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
OrderCompletedNotificationRuleset = Class.new(Ruleset)
|
416
|
+
OrderCompletedNotificationRuleset.
|
417
|
+
add_rule(:order_is_sent_rule).
|
418
|
+
add_rule(:order_is_paid_rule)
|
419
|
+
```
|
420
|
+
|
421
|
+
#### Usage
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
OrderIsPaidRule.new(order).satisfied?
|
425
|
+
OrderCompletedNotificationRuleset.new(order).satisfied?
|
426
|
+
|
427
|
+
ResendOrderNotification.call(order) if OrderCompletedNotificationRuleset.new(order).satisfied?(force: true)
|
428
|
+
```
|
429
|
+
|
350
430
|
## Further reading
|
351
431
|
|
352
432
|
* [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.0
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Rule
|
2
|
+
def initialize(subject)
|
3
|
+
@subject = subject
|
4
|
+
end
|
5
|
+
|
6
|
+
def satisfied?
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def not_applicable?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def applicable?
|
15
|
+
!not_applicable?
|
16
|
+
end
|
17
|
+
|
18
|
+
def forceable?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :subject
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Ruleset
|
2
|
+
class EmptyRuleset < StandardError; end
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :rule_names
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.rules
|
9
|
+
(rule_names || []).map do |rule_name|
|
10
|
+
rule_name.to_s.classify.constantize
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.add_rule(rule_name)
|
15
|
+
self.rule_names ||= []
|
16
|
+
self.rule_names << rule_name.to_sym
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(subject = nil)
|
21
|
+
raise EmptyRuleset if self.class.rules.empty?
|
22
|
+
|
23
|
+
@rules = self.class.rules.map { |rule| rule.new(subject) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def satisfied?(force: false)
|
27
|
+
rules.all? do |rule|
|
28
|
+
rule.satisfied? ||
|
29
|
+
rule.not_applicable? ||
|
30
|
+
(force && rule.forceable?)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def not_satisfied?
|
35
|
+
!satisfied?
|
36
|
+
end
|
37
|
+
|
38
|
+
def applicable?
|
39
|
+
!not_applicable?
|
40
|
+
end
|
41
|
+
|
42
|
+
def not_applicable?
|
43
|
+
rules.all?(&:not_applicable?)
|
44
|
+
end
|
45
|
+
|
46
|
+
def forceable?
|
47
|
+
rules.all? do |rule|
|
48
|
+
rule.forceable? ||
|
49
|
+
rule.not_applicable? ||
|
50
|
+
rule.satisfied?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def each(&block)
|
55
|
+
return enum_for(:each) unless block_given?
|
56
|
+
|
57
|
+
rules.each do |rule_or_ruleset|
|
58
|
+
if rule_or_ruleset.is_a?(Ruleset)
|
59
|
+
rule_or_ruleset.each(&block)
|
60
|
+
else
|
61
|
+
yield rule_or_ruleset
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :rules
|
69
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# StrongRuleset is not satisfied and not forceable if any of rules is not applicable
|
2
|
+
|
3
|
+
class StrongRuleset < Ruleset
|
4
|
+
def satisfied?(force: false)
|
5
|
+
rules.all? do |rule|
|
6
|
+
(rule.applicable? && rule.satisfied?) || (force && rule.forceable?)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def not_applicable?
|
11
|
+
rules.any?(&:not_applicable?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def forceable?
|
15
|
+
rules.all? do |rule|
|
16
|
+
(rule.applicable? && rule.forceable?) || rule.satisfied?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/rails-patterns.rb
CHANGED
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.8.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.8.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 = "2020-
|
14
|
+
s.date = "2020-09-01"
|
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 = [
|
@@ -33,7 +33,10 @@ Gem::Specification.new do |s|
|
|
33
33
|
"lib/patterns/collection.rb",
|
34
34
|
"lib/patterns/form.rb",
|
35
35
|
"lib/patterns/query.rb",
|
36
|
+
"lib/patterns/rule.rb",
|
37
|
+
"lib/patterns/ruleset.rb",
|
36
38
|
"lib/patterns/service.rb",
|
39
|
+
"lib/patterns/strong_ruleset.rb",
|
37
40
|
"lib/rails-patterns.rb",
|
38
41
|
"rails-patterns.gemspec",
|
39
42
|
"spec/helpers/custom_calculation.rb",
|
@@ -43,35 +46,30 @@ Gem::Specification.new do |s|
|
|
43
46
|
"spec/patterns/collection_spec.rb",
|
44
47
|
"spec/patterns/form_spec.rb",
|
45
48
|
"spec/patterns/query_spec.rb",
|
49
|
+
"spec/patterns/rule_spec.rb",
|
50
|
+
"spec/patterns/ruleset_spec.rb",
|
46
51
|
"spec/patterns/service_spec.rb",
|
52
|
+
"spec/patterns/strong_ruleset_spec.rb",
|
47
53
|
"spec/spec_helper.rb"
|
48
54
|
]
|
49
55
|
s.homepage = "http://github.com/selleo/pattern".freeze
|
50
56
|
s.licenses = ["MIT".freeze]
|
51
57
|
s.required_ruby_version = Gem::Requirement.new(">= 2.5.0".freeze)
|
52
|
-
s.rubygems_version = "
|
58
|
+
s.rubygems_version = "3.1.2".freeze
|
53
59
|
s.summary = "A collection of lightweight, standardized, rails-oriented patterns.".freeze
|
54
60
|
|
55
61
|
if s.respond_to? :specification_version then
|
56
62
|
s.specification_version = 4
|
63
|
+
end
|
57
64
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
68
|
-
s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
69
|
-
s.add_dependency(%q<virtus>.freeze, [">= 0"])
|
70
|
-
s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
|
71
|
-
s.add_dependency(%q<rspec>.freeze, [">= 0"])
|
72
|
-
s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
73
|
-
s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
74
|
-
end
|
65
|
+
if s.respond_to? :add_runtime_dependency then
|
66
|
+
s.add_runtime_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
67
|
+
s.add_runtime_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
68
|
+
s.add_runtime_dependency(%q<virtus>.freeze, [">= 0"])
|
69
|
+
s.add_runtime_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
|
70
|
+
s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
|
71
|
+
s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
72
|
+
s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
75
73
|
else
|
76
74
|
s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
|
77
75
|
s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
|
@@ -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
|
@@ -0,0 +1,260 @@
|
|
1
|
+
RSpec.describe Ruleset do
|
2
|
+
context 'when empty ruleset is initialized' do
|
3
|
+
it 'raises an error' do
|
4
|
+
empty_ruleset_klass = Class.new(Ruleset)
|
5
|
+
custom_ruleset_klass = Class.new(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 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(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(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(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(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(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(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(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(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(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(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(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(Ruleset)
|
220
|
+
custom_ruleset_klass_1.add_rule(:rule_1)
|
221
|
+
custom_ruleset_klass_1.add_rule(:rule_2)
|
222
|
+
Ruleset2 = Class.new(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(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
|
@@ -0,0 +1,79 @@
|
|
1
|
+
RSpec.describe StrongRuleset do
|
2
|
+
it 'inherites from Ruleset' do
|
3
|
+
custom_strong_ruleset_klass = Class.new(StrongRuleset)
|
4
|
+
expect(custom_strong_ruleset_klass.ancestors).to include 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(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(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(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(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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-patterns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stevo
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -130,7 +130,10 @@ files:
|
|
130
130
|
- lib/patterns/collection.rb
|
131
131
|
- lib/patterns/form.rb
|
132
132
|
- lib/patterns/query.rb
|
133
|
+
- lib/patterns/rule.rb
|
134
|
+
- lib/patterns/ruleset.rb
|
133
135
|
- lib/patterns/service.rb
|
136
|
+
- lib/patterns/strong_ruleset.rb
|
134
137
|
- lib/rails-patterns.rb
|
135
138
|
- rails-patterns.gemspec
|
136
139
|
- spec/helpers/custom_calculation.rb
|
@@ -140,13 +143,16 @@ files:
|
|
140
143
|
- spec/patterns/collection_spec.rb
|
141
144
|
- spec/patterns/form_spec.rb
|
142
145
|
- spec/patterns/query_spec.rb
|
146
|
+
- spec/patterns/rule_spec.rb
|
147
|
+
- spec/patterns/ruleset_spec.rb
|
143
148
|
- spec/patterns/service_spec.rb
|
149
|
+
- spec/patterns/strong_ruleset_spec.rb
|
144
150
|
- spec/spec_helper.rb
|
145
151
|
homepage: http://github.com/selleo/pattern
|
146
152
|
licenses:
|
147
153
|
- MIT
|
148
154
|
metadata: {}
|
149
|
-
post_install_message:
|
155
|
+
post_install_message:
|
150
156
|
rdoc_options: []
|
151
157
|
require_paths:
|
152
158
|
- lib
|
@@ -161,9 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
161
167
|
- !ruby/object:Gem::Version
|
162
168
|
version: '0'
|
163
169
|
requirements: []
|
164
|
-
|
165
|
-
|
166
|
-
signing_key:
|
170
|
+
rubygems_version: 3.1.2
|
171
|
+
signing_key:
|
167
172
|
specification_version: 4
|
168
173
|
summary: A collection of lightweight, standardized, rails-oriented patterns.
|
169
174
|
test_files: []
|