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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: acffaa9187d107e66c029e063f1d9cd5fbb2a3f16af7c2f747f166bc3bafab64
|
|
4
|
+
data.tar.gz: '014944374d20b4e6113c2946e25c963aaebd5841dad52cf960f85896c29930e5'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: def79504af62d2295806ab1a6e35261ad413d9205cfe36c41d4ae110068cfb5afe267aee4b194dbe2199616ca2ca976d6ffc63d5ca2166875b73d80c1e3fc9df
|
|
7
|
+
data.tar.gz: 84033ea6eb230ddf0436bda70889f4f62d46bb734d490635f564232b00bd199cdca595729e0eddae5055679e6575debcc19ed0385236e47b01b253510c86df8c
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
2
|
+
# They are provided by a third-party and are governed by
|
|
3
|
+
# separate terms of service, privacy policy, and support
|
|
4
|
+
# documentation.
|
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
|
7
|
+
|
|
8
|
+
name: Ruby
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [ master ]
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [ master ]
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
ruby: [ '2.5', '2.6', '2.7' ]
|
|
23
|
+
name: RSpec for Ruby version ${{ matrix.ruby }}
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v2
|
|
26
|
+
- uses: supercharge/redis-github-action@1.1.0
|
|
27
|
+
- name: Set up Ruby
|
|
28
|
+
uses: actions/setup-ruby@v1
|
|
29
|
+
with:
|
|
30
|
+
ruby-version: ${{ matrix.ruby }}
|
|
31
|
+
- run: gem install bundler
|
|
32
|
+
- run: bundle install
|
|
33
|
+
- run: bundle exec rspec
|
data/Gemfile
CHANGED
|
@@ -3,16 +3,19 @@ source "https://rubygems.org"
|
|
|
3
3
|
gem "activerecord", ">= 4.2.6"
|
|
4
4
|
gem "actionpack", ">= 4.2.6"
|
|
5
5
|
gem "virtus"
|
|
6
|
+
gem "ruby2_keywords"
|
|
6
7
|
|
|
7
8
|
# Add dependencies to develop your gem here.
|
|
8
9
|
# Include everything needed to run rake, tests, features, etc.
|
|
9
10
|
|
|
10
11
|
group :development do
|
|
11
12
|
gem "rspec"
|
|
12
|
-
gem "bundler", "~>
|
|
13
|
-
gem "juwelier"
|
|
13
|
+
gem "bundler", "~> 2.0"
|
|
14
|
+
gem "juwelier"
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
group "test" do
|
|
17
18
|
gem "pry-rails"
|
|
19
|
+
gem "rspec_junit_formatter"
|
|
20
|
+
gem "redis"
|
|
18
21
|
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,87 +1,95 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: https://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
actionpack (
|
|
5
|
-
actionview (=
|
|
6
|
-
activesupport (=
|
|
7
|
-
rack (~> 2.0)
|
|
4
|
+
actionpack (6.0.3.2)
|
|
5
|
+
actionview (= 6.0.3.2)
|
|
6
|
+
activesupport (= 6.0.3.2)
|
|
7
|
+
rack (~> 2.0, >= 2.0.8)
|
|
8
8
|
rack-test (>= 0.6.3)
|
|
9
9
|
rails-dom-testing (~> 2.0)
|
|
10
|
-
rails-html-sanitizer (~> 1.0, >= 1.0
|
|
11
|
-
actionview (
|
|
12
|
-
activesupport (=
|
|
10
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
|
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
|
-
rails-html-sanitizer (~> 1.
|
|
17
|
-
activemodel (
|
|
18
|
-
activesupport (=
|
|
19
|
-
activerecord (
|
|
20
|
-
activemodel (=
|
|
21
|
-
activesupport (=
|
|
22
|
-
|
|
23
|
-
activesupport (5.1.4)
|
|
16
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
|
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)
|
|
24
23
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
25
|
-
i18n (
|
|
24
|
+
i18n (>= 0.7, < 2)
|
|
26
25
|
minitest (~> 5.1)
|
|
27
26
|
tzinfo (~> 1.1)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
28
|
+
addressable (2.7.0)
|
|
29
|
+
public_suffix (>= 2.0.2, < 5.0)
|
|
31
30
|
axiom-types (0.1.1)
|
|
32
31
|
descendants_tracker (~> 0.0.4)
|
|
33
32
|
ice_nine (~> 0.11.0)
|
|
34
33
|
thread_safe (~> 0.3, >= 0.3.1)
|
|
35
|
-
builder (3.2.
|
|
34
|
+
builder (3.2.4)
|
|
36
35
|
coderay (1.1.2)
|
|
37
36
|
coercible (1.0.0)
|
|
38
37
|
descendants_tracker (~> 0.0.1)
|
|
39
|
-
concurrent-ruby (1.
|
|
40
|
-
crass (1.0.
|
|
38
|
+
concurrent-ruby (1.1.6)
|
|
39
|
+
crass (1.0.6)
|
|
41
40
|
descendants_tracker (0.0.4)
|
|
42
41
|
thread_safe (~> 0.3, >= 0.3.1)
|
|
43
42
|
diff-lcs (1.3)
|
|
44
43
|
equalizer (0.0.11)
|
|
45
|
-
erubi (1.
|
|
46
|
-
faraday (
|
|
44
|
+
erubi (1.9.0)
|
|
45
|
+
faraday (1.3.0)
|
|
46
|
+
faraday-net_http (~> 1.0)
|
|
47
47
|
multipart-post (>= 1.2, < 3)
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
ruby2_keywords
|
|
49
|
+
faraday-net_http (1.0.1)
|
|
50
|
+
git (1.8.1)
|
|
51
|
+
rchardet (~> 1.8)
|
|
52
|
+
github_api (0.19.0)
|
|
50
53
|
addressable (~> 2.4)
|
|
51
54
|
descendants_tracker (~> 0.0.4)
|
|
52
|
-
faraday (
|
|
55
|
+
faraday (>= 0.8, < 2)
|
|
53
56
|
hashie (~> 3.5, >= 3.5.2)
|
|
54
57
|
oauth2 (~> 1.0)
|
|
55
|
-
hashie (3.
|
|
56
|
-
highline (
|
|
57
|
-
i18n (
|
|
58
|
+
hashie (3.6.0)
|
|
59
|
+
highline (2.0.3)
|
|
60
|
+
i18n (1.8.3)
|
|
58
61
|
concurrent-ruby (~> 1.0)
|
|
59
62
|
ice_nine (0.11.2)
|
|
60
|
-
juwelier (2.
|
|
63
|
+
juwelier (2.4.9)
|
|
61
64
|
builder
|
|
62
|
-
bundler
|
|
63
|
-
git
|
|
65
|
+
bundler
|
|
66
|
+
git
|
|
64
67
|
github_api
|
|
65
|
-
highline
|
|
66
|
-
|
|
68
|
+
highline
|
|
69
|
+
kamelcase (~> 0)
|
|
70
|
+
nokogiri
|
|
71
|
+
psych
|
|
67
72
|
rake
|
|
68
73
|
rdoc
|
|
69
|
-
|
|
70
|
-
jwt (
|
|
71
|
-
|
|
74
|
+
semver2
|
|
75
|
+
jwt (2.2.3)
|
|
76
|
+
kamelcase (0.0.2)
|
|
77
|
+
semver2 (~> 3)
|
|
78
|
+
loofah (2.6.0)
|
|
72
79
|
crass (~> 1.0.2)
|
|
73
80
|
nokogiri (>= 1.5.9)
|
|
74
81
|
method_source (0.9.0)
|
|
75
|
-
mini_portile2 (2.3
|
|
76
|
-
minitest (5.
|
|
77
|
-
multi_json (1.
|
|
82
|
+
mini_portile2 (2.5.3)
|
|
83
|
+
minitest (5.14.1)
|
|
84
|
+
multi_json (1.15.0)
|
|
78
85
|
multi_xml (0.6.0)
|
|
79
|
-
multipart-post (2.
|
|
80
|
-
nokogiri (1.
|
|
81
|
-
mini_portile2 (~> 2.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
multipart-post (2.1.1)
|
|
87
|
+
nokogiri (1.11.7)
|
|
88
|
+
mini_portile2 (~> 2.5.0)
|
|
89
|
+
racc (~> 1.4)
|
|
90
|
+
oauth2 (1.4.7)
|
|
91
|
+
faraday (>= 0.8, < 2.0)
|
|
92
|
+
jwt (>= 1.0, < 3.0)
|
|
85
93
|
multi_json (~> 1.3)
|
|
86
94
|
multi_xml (~> 0.5)
|
|
87
95
|
rack (>= 1.2, < 3)
|
|
@@ -90,17 +98,21 @@ GEM
|
|
|
90
98
|
method_source (~> 0.9.0)
|
|
91
99
|
pry-rails (0.3.6)
|
|
92
100
|
pry (>= 0.10.4)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
psych (4.0.1)
|
|
102
|
+
public_suffix (4.0.6)
|
|
103
|
+
racc (1.5.2)
|
|
104
|
+
rack (2.2.3)
|
|
105
|
+
rack-test (1.1.0)
|
|
96
106
|
rack (>= 1.0, < 3)
|
|
97
107
|
rails-dom-testing (2.0.3)
|
|
98
108
|
activesupport (>= 4.2.0)
|
|
99
109
|
nokogiri (>= 1.6)
|
|
100
|
-
rails-html-sanitizer (1.0
|
|
101
|
-
loofah (~> 2.
|
|
102
|
-
rake (
|
|
103
|
-
|
|
110
|
+
rails-html-sanitizer (1.3.0)
|
|
111
|
+
loofah (~> 2.3)
|
|
112
|
+
rake (13.0.3)
|
|
113
|
+
rchardet (1.8.0)
|
|
114
|
+
rdoc (6.3.1)
|
|
115
|
+
redis (4.1.4)
|
|
104
116
|
rspec (3.7.0)
|
|
105
117
|
rspec-core (~> 3.7.0)
|
|
106
118
|
rspec-expectations (~> 3.7.0)
|
|
@@ -113,16 +125,20 @@ GEM
|
|
|
113
125
|
rspec-mocks (3.7.0)
|
|
114
126
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
115
127
|
rspec-support (~> 3.7.0)
|
|
116
|
-
rspec-support (3.7.
|
|
117
|
-
|
|
128
|
+
rspec-support (3.7.1)
|
|
129
|
+
rspec_junit_formatter (0.3.0)
|
|
130
|
+
rspec-core (>= 2, < 4, != 2.12.0)
|
|
131
|
+
ruby2_keywords (0.0.2)
|
|
132
|
+
semver2 (3.4.2)
|
|
118
133
|
thread_safe (0.3.6)
|
|
119
|
-
tzinfo (1.2.
|
|
134
|
+
tzinfo (1.2.7)
|
|
120
135
|
thread_safe (~> 0.1)
|
|
121
136
|
virtus (1.0.5)
|
|
122
137
|
axiom-types (~> 0.1)
|
|
123
138
|
coercible (~> 1.0)
|
|
124
139
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
|
125
140
|
equalizer (~> 0.0, >= 0.0.9)
|
|
141
|
+
zeitwerk (2.3.0)
|
|
126
142
|
|
|
127
143
|
PLATFORMS
|
|
128
144
|
ruby
|
|
@@ -130,11 +146,14 @@ PLATFORMS
|
|
|
130
146
|
DEPENDENCIES
|
|
131
147
|
actionpack (>= 4.2.6)
|
|
132
148
|
activerecord (>= 4.2.6)
|
|
133
|
-
bundler (~>
|
|
134
|
-
juwelier
|
|
149
|
+
bundler (~> 2.0)
|
|
150
|
+
juwelier
|
|
135
151
|
pry-rails
|
|
152
|
+
redis
|
|
136
153
|
rspec
|
|
154
|
+
rspec_junit_formatter
|
|
155
|
+
ruby2_keywords
|
|
137
156
|
virtus
|
|
138
157
|
|
|
139
158
|
BUNDLED WITH
|
|
140
|
-
1.
|
|
159
|
+
2.1.4
|
data/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# Pattern
|
|
2
4
|
|
|
3
|
-
A collection of lightweight, standardized, rails-oriented patterns.
|
|
5
|
+
A collection of lightweight, standardized, rails-oriented patterns used by [RubyOnRails Developers @ Selleo](https://selleo.com/ruby-on-rails)
|
|
4
6
|
|
|
5
7
|
- [Query - complex querying on active record relation](#query)
|
|
6
8
|
- [Service - useful for handling processes involving multiple steps](#service)
|
|
7
9
|
- [Collection - when in need to add a method that relates to the collection as whole](#collection)
|
|
8
10
|
- [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)
|
|
9
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)
|
|
10
13
|
|
|
11
14
|
## Installation
|
|
12
15
|
|
|
@@ -21,13 +24,14 @@ gem "rails-patterns"
|
|
|
21
24
|
Then `bundle install`
|
|
22
25
|
|
|
23
26
|
## Query
|
|
24
|
-
|
|
27
|
+
|
|
25
28
|
### When to use it
|
|
26
29
|
|
|
27
|
-
One should consider using query objects pattern when in need to perform complex querying on active record relation.
|
|
28
|
-
Usually one should avoid using scopes for such purpose.
|
|
30
|
+
One should consider using query objects pattern when in need to perform complex querying on active record relation.
|
|
31
|
+
Usually one should avoid using scopes for such purpose.
|
|
29
32
|
As a rule of thumb, if scope interacts with more than one column and/or joins in other tables, it should be moved to query object.
|
|
30
33
|
Also whenever a chain of scopes is to be used, one should consider using query object too.
|
|
34
|
+
Some more information on using query objects can be found in [this article](https://medium.com/@blazejkosmowski/essential-rubyonrails-patterns-part-2-query-objects-4b253f4f4539).
|
|
31
35
|
|
|
32
36
|
### Assumptions and rules
|
|
33
37
|
|
|
@@ -74,7 +78,7 @@ RecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.t
|
|
|
74
78
|
RecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)
|
|
75
79
|
|
|
76
80
|
class User < ApplicationRecord
|
|
77
|
-
scope :
|
|
81
|
+
scope :recently_activated, RecentlyActivatedUsersQuery
|
|
78
82
|
end
|
|
79
83
|
```
|
|
80
84
|
|
|
@@ -83,7 +87,7 @@ end
|
|
|
83
87
|
### When to use it
|
|
84
88
|
|
|
85
89
|
Service objects are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).
|
|
86
|
-
Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring. In many cases service object can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object).
|
|
90
|
+
Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring. In many cases service object can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using services can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1).
|
|
87
91
|
|
|
88
92
|
### Assumptions and rules
|
|
89
93
|
|
|
@@ -94,9 +98,9 @@ Service objects are also useful for handling processes involving multiple steps.
|
|
|
94
98
|
* It is recommended for `#call` method to be the only public method of service object (besides state readers)
|
|
95
99
|
* It is recommended to name service object classes after commands (e.g. `ActivateUser` instead of `UserActivation`)
|
|
96
100
|
|
|
97
|
-
### Other
|
|
101
|
+
### Other
|
|
98
102
|
|
|
99
|
-
A bit higher level of abstraction is provided by [business_process gem](https://github.com/Selleo/business_process).
|
|
103
|
+
A bit higher level of abstraction is provided by [business_process gem](https://github.com/Selleo/business_process).
|
|
100
104
|
|
|
101
105
|
### Examples
|
|
102
106
|
|
|
@@ -188,7 +192,7 @@ Form objects, just like service objects, are commonly used to mitigate problems
|
|
|
188
192
|
Form objects can also be used as replacement for `ActionController::StrongParameters` strategy, as all writable attributes are re-defined within each form.
|
|
189
193
|
Finally form objects can be used as wrappers for virtual (with no model representation) or composite (saving multiple models at once) resources.
|
|
190
194
|
In the latter case this may act as replacement for `ActiveRecord::NestedAttributes`.
|
|
191
|
-
In some cases FormObject can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object).
|
|
195
|
+
In some cases FormObject can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using form objects can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-form-objects-b199aada6ec9).
|
|
192
196
|
|
|
193
197
|
### Assumptions and rules
|
|
194
198
|
|
|
@@ -344,6 +348,93 @@ TotalCurrentRevenue.calculate
|
|
|
344
348
|
AverageDailyRevenue.result
|
|
345
349
|
```
|
|
346
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 < Patterns::Rule
|
|
400
|
+
def satisfied?
|
|
401
|
+
subject.sent?
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
class OrderIsPaidRule < Patterns::Rule
|
|
406
|
+
def satisfied?
|
|
407
|
+
subject.paid?
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def forceable?
|
|
411
|
+
true
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
OrderCompletedNotificationRuleset = Class.new(Patterns::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
|
+
|
|
347
430
|
## Further reading
|
|
348
431
|
|
|
349
432
|
* [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)
|
|
433
|
+
|
|
434
|
+
## About Selleo
|
|
435
|
+
|
|
436
|
+

|
|
437
|
+
|
|
438
|
+
Software development teams with an entrepreneurial sense of ownership at their core delivering great digital products and building culture people want to belong to. We are a community of engaged co-workers passionate about crafting impactful web solutions which transform the way our clients do business.
|
|
439
|
+
|
|
440
|
+
All names and logos for [Selleo](https://selleo.com/about) are trademark of Selleo Labs Sp. z o.o. (formerly Selleo Sp. z o.o. Sp.k.)
|
data/Rakefile
CHANGED
|
@@ -21,6 +21,7 @@ Juwelier::Tasks.new do |gem|
|
|
|
21
21
|
gem.description = "A collection of lightweight, standardized, rails-oriented patterns."
|
|
22
22
|
gem.email = "b.kosmowski@selleo.com"
|
|
23
23
|
gem.authors = ["Stevo"]
|
|
24
|
+
gem.required_ruby_version = ">= 2.5.0"
|
|
24
25
|
|
|
25
26
|
# dependencies defined in Gemfile
|
|
26
27
|
end
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
data/lib/patterns/calculation.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
1
3
|
module Patterns
|
|
2
4
|
class Calculation
|
|
3
5
|
class_attribute :cache_expiry_every
|
|
@@ -7,8 +9,8 @@ module Patterns
|
|
|
7
9
|
@subject = args.first
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
def self.result(*args)
|
|
11
|
-
new(*args).cached_result
|
|
12
|
+
def self.result(*args, &block)
|
|
13
|
+
new(*args).cached_result(&block)
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
class << self
|
|
@@ -20,9 +22,13 @@ module Patterns
|
|
|
20
22
|
self.cache_expiry_every = period
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def cached_result
|
|
24
|
-
|
|
25
|
-
result
|
|
25
|
+
def cached_result(&block)
|
|
26
|
+
if cache_expiry_period.blank?
|
|
27
|
+
result(&block)
|
|
28
|
+
else
|
|
29
|
+
Rails.cache.fetch(cache_key, expires_in: cache_expiry_period) do
|
|
30
|
+
result(&block)
|
|
31
|
+
end
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
|
|
@@ -35,7 +41,15 @@ module Patterns
|
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def cache_key
|
|
38
|
-
"#{self.class.name}_#{
|
|
44
|
+
"#{self.class.name}_#{hash_of(subject, options)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.hash_of(*args)
|
|
48
|
+
Digest::SHA1.hexdigest(args.map(&:to_s).join(':'))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hash_of(*args)
|
|
52
|
+
self.class.hash_of(*args)
|
|
39
53
|
end
|
|
40
54
|
|
|
41
55
|
def cache_expiry_period
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Patterns
|
|
2
|
+
class Rule
|
|
3
|
+
def initialize(subject)
|
|
4
|
+
@subject = subject
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def satisfied?
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def not_applicable?
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def applicable?
|
|
16
|
+
!not_applicable?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def forceable?
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :subject
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Patterns
|
|
2
|
+
class Ruleset
|
|
3
|
+
class EmptyRuleset < StandardError; end
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :rule_names
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.rules
|
|
10
|
+
(rule_names || []).map do |rule_name|
|
|
11
|
+
rule_name.to_s.classify.constantize
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.add_rule(rule_name)
|
|
16
|
+
self.rule_names ||= []
|
|
17
|
+
self.rule_names << rule_name.to_sym
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(subject = nil)
|
|
22
|
+
raise EmptyRuleset if self.class.rules.empty?
|
|
23
|
+
|
|
24
|
+
@rules = self.class.rules.map { |rule| rule.new(subject) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def satisfied?(force: false)
|
|
28
|
+
rules.all? do |rule|
|
|
29
|
+
rule.satisfied? ||
|
|
30
|
+
rule.not_applicable? ||
|
|
31
|
+
(force && rule.forceable?)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def not_satisfied?
|
|
36
|
+
!satisfied?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def applicable?
|
|
40
|
+
!not_applicable?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def not_applicable?
|
|
44
|
+
rules.all?(&:not_applicable?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def forceable?
|
|
48
|
+
rules.all? do |rule|
|
|
49
|
+
rule.forceable? ||
|
|
50
|
+
rule.not_applicable? ||
|
|
51
|
+
rule.satisfied?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each(&block)
|
|
56
|
+
return enum_for(:each) unless block_given?
|
|
57
|
+
|
|
58
|
+
rules.each do |rule_or_ruleset|
|
|
59
|
+
if rule_or_ruleset.is_a?(Ruleset)
|
|
60
|
+
rule_or_ruleset.each(&block)
|
|
61
|
+
else
|
|
62
|
+
yield rule_or_ruleset
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
attr_reader :rules
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/patterns/service.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
require 'ruby2_keywords'
|
|
2
|
+
|
|
1
3
|
module Patterns
|
|
2
4
|
class Service
|
|
3
5
|
attr_reader :result
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
service
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
class << self
|
|
8
|
+
ruby2_keywords def call(*args)
|
|
9
|
+
new(*args).tap do |service|
|
|
10
|
+
service.instance_variable_set(
|
|
11
|
+
"@result",
|
|
12
|
+
service.call
|
|
13
|
+
)
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# StrongRuleset is not satisfied and not forceable if any of rules is not applicable
|
|
2
|
+
|
|
3
|
+
module Patterns
|
|
4
|
+
class StrongRuleset < Ruleset
|
|
5
|
+
def satisfied?(force: false)
|
|
6
|
+
rules.all? do |rule|
|
|
7
|
+
(rule.applicable? && rule.satisfied?) || (force && rule.forceable?)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def not_applicable?
|
|
12
|
+
rules.any?(&:not_applicable?)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def forceable?
|
|
16
|
+
rules.all? do |rule|
|
|
17
|
+
(rule.applicable? && rule.forceable?) || rule.satisfied?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|