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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffa6ee8fff6415c7271eb4a38172148881cdd8866d0279e59abffd452b54f591
4
- data.tar.gz: a8e077bf276d9a6a3a05f345d09085561ccb5f06996b7c7a8a01f4082cf329b5
3
+ metadata.gz: 738d61388a1488705037348a76d42d69b4b92a83cdd6753a2d14d061768302a1
4
+ data.tar.gz: 9dcc97a76fa682b76ff8910e4146da62eff304c3a7f511343e31d7b8ed222c36
5
5
  SHA512:
6
- metadata.gz: d4e616146039004e697d7a2b3fc3ef89eaaaaf11779af4744a605d2102b967b8b4b9856147a1b111db5c801fde833ca19c0c5ba067a246977a14c14d7f9c2f54
7
- data.tar.gz: 1e883e3cbfc21fc4e7135acc9b23dc4db7e05b2a4671cfe6d618f04b5df4d672c7ac8f76487ae34cce503fd2fdc51e2a0952dffbc66964943c85b8136a0dd509
6
+ metadata.gz: 9961fcdafa85dca7ef1a91641c4867fa348cdeb20a30abda8e8a91ab3a33c9d3ae456d57a2761615c127bf656b793c8bda94e7cc21371a42bd413e57336babfc
7
+ data.tar.gz: 7a570f8a7836ff4a3404525d41323697850c46c4dae447512ab517acf036799afd7c14a57028f374a5cea8aa8b489d134e32a3bd8278563039242d33546a8f0a
@@ -1,25 +1,25 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- actionpack (6.0.3.1)
5
- actionview (= 6.0.3.1)
6
- activesupport (= 6.0.3.1)
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.1)
12
- activesupport (= 6.0.3.1)
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.1)
18
- activesupport (= 6.0.3.1)
19
- activerecord (6.0.3.1)
20
- activemodel (= 6.0.3.1)
21
- activesupport (= 6.0.3.1)
22
- activesupport (6.0.3.1)
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.2)
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.5.0)
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.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.7.3
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
@@ -4,3 +4,6 @@ require "patterns/service"
4
4
  require "patterns/collection"
5
5
  require "patterns/calculation"
6
6
  require "patterns/form"
7
+ require "patterns/rule"
8
+ require "patterns/ruleset"
9
+ require "patterns/strong_ruleset"
@@ -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.7.3 ruby lib
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.7.3"
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-06-12"
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 = "2.7.6.2".freeze
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
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
59
- s.add_runtime_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
60
- s.add_runtime_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
61
- s.add_runtime_dependency(%q<virtus>.freeze, [">= 0"])
62
- s.add_runtime_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
63
- s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
64
- s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
65
- s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
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.7.3
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-06-12 00:00:00.000000000 Z
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
- rubyforge_project:
165
- rubygems_version: 2.7.6.2
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: []