rails-patterns 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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: []