rails-patterns 0.6.0 → 0.10.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
- SHA1:
3
- metadata.gz: 0a30afb3f25ef523a75a54887fc9ff48f7567ac8
4
- data.tar.gz: b3d71af078c2287b6c1f6e16bc140d267879b49b
2
+ SHA256:
3
+ metadata.gz: acffaa9187d107e66c029e063f1d9cd5fbb2a3f16af7c2f747f166bc3bafab64
4
+ data.tar.gz: '014944374d20b4e6113c2946e25c963aaebd5841dad52cf960f85896c29930e5'
5
5
  SHA512:
6
- metadata.gz: 2bb36981deb575dd7e900bb41c14d89e230c60ea67896b87643ed332923cf13b32880599111c672d0591f01836fc81d2e87381337ec100d304fdf726e93f67ac
7
- data.tar.gz: debed57e679b131544e8b7c76f95944f0128570f71f3959187fbce79081a2848a9c2e8cfdbc257855df15f3d80b2c717c94d958428d0d23ad472825e1bf23d92
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", "~> 1.0"
13
- gem "juwelier", "~> 2.1.0"
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.1.4)
5
- actionview (= 5.1.4)
6
- activesupport (= 5.1.4)
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.2)
11
- actionview (5.1.4)
12
- activesupport (= 5.1.4)
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.0, >= 1.0.3)
17
- activemodel (5.1.4)
18
- activesupport (= 5.1.4)
19
- activerecord (5.1.4)
20
- activemodel (= 5.1.4)
21
- activesupport (= 5.1.4)
22
- arel (~> 8.0)
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 (~> 0.7)
24
+ i18n (>= 0.7, < 2)
26
25
  minitest (~> 5.1)
27
26
  tzinfo (~> 1.1)
28
- addressable (2.5.2)
29
- public_suffix (>= 2.0.2, < 4.0)
30
- arel (8.0.0)
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.3)
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.0.5)
40
- crass (1.0.3)
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.7.0)
46
- faraday (0.12.2)
44
+ erubi (1.9.0)
45
+ faraday (1.3.0)
46
+ faraday-net_http (~> 1.0)
47
47
  multipart-post (>= 1.2, < 3)
48
- git (1.3.0)
49
- github_api (0.18.2)
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 (~> 0.8)
55
+ faraday (>= 0.8, < 2)
53
56
  hashie (~> 3.5, >= 3.5.2)
54
57
  oauth2 (~> 1.0)
55
- hashie (3.5.7)
56
- highline (1.7.10)
57
- i18n (0.9.1)
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.1.3)
63
+ juwelier (2.4.9)
61
64
  builder
62
- bundler (>= 1.13)
63
- git (>= 1.2.5)
65
+ bundler
66
+ git
64
67
  github_api
65
- highline (>= 1.6.15)
66
- nokogiri (>= 1.5.10)
68
+ highline
69
+ kamelcase (~> 0)
70
+ nokogiri
71
+ psych
67
72
  rake
68
73
  rdoc
69
- semver
70
- jwt (1.5.6)
71
- loofah (2.1.1)
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.0)
76
- minitest (5.11.1)
77
- multi_json (1.13.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.0.0)
80
- nokogiri (1.8.1)
81
- mini_portile2 (~> 2.3.0)
82
- oauth2 (1.4.0)
83
- faraday (>= 0.8, < 0.13)
84
- jwt (~> 1.0)
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
- public_suffix (3.0.1)
94
- rack (2.0.3)
95
- rack-test (0.8.2)
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.3)
101
- loofah (~> 2.0)
102
- rake (12.3.0)
103
- rdoc (6.0.1)
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.0)
117
- semver (1.0.1)
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.4)
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 (~> 1.0)
134
- juwelier (~> 2.1.0)
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.15.1
159
+ 2.1.4
data/README.md CHANGED
@@ -1,12 +1,15 @@
1
+ ![](https://github.com/Selleo/pattern/workflows/Ruby/badge.svg)
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 :recenty_activated, RecentlyActivatedUsersQuery
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
+ ![selleo](https://raw.githubusercontent.com/Selleo/selleo-resources/master/public/github_footer.png)
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.6.0
1
+ 0.10.0
@@ -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
- Rails.cache.fetch(cache_key, expires_in: cache_expiry_period, force: cache_expiry_period.blank?) do
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}_#{[subject, options].hash}"
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
@@ -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
- def self.call(*args)
6
- new(*args).tap do |service|
7
- service.instance_variable_set(
8
- "@result",
9
- service.call
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
@@ -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"