rails-patterns 0.7.3 → 0.11.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: bbe1da3d0914915666930f116e9d74b0c20cd5f75184f3d4e5304231fdb98784
4
+ data.tar.gz: 60f796148cfcb353aa5071936ecf9203bd8895183feccc14a80baaa31705eeef
5
5
  SHA512:
6
- metadata.gz: d4e616146039004e697d7a2b3fc3ef89eaaaaf11779af4744a605d2102b967b8b4b9856147a1b111db5c801fde833ca19c0c5ba067a246977a14c14d7f9c2f54
7
- data.tar.gz: 1e883e3cbfc21fc4e7135acc9b23dc4db7e05b2a4671cfe6d618f04b5df4d672c7ac8f76487ae34cce503fd2fdc51e2a0952dffbc66964943c85b8136a0dd509
6
+ metadata.gz: 4c53d800f009d26c834a88d672a6f6461cd88d16b3e13d086d1da40d20c88e6f457a878f40b297a22ffab921081863f6c29002b032fefb02d8fd890ba7a51f69
7
+ data.tar.gz: e3a6563d3a5e6c081491be1e4d2839d6d5a7f17d5f5af2980ffac2732fc9cd15bd00976ca68f99322e3d6ba559d70bbab56c2168880dc89c03ed29f1c39511e5
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ gem "ruby2_keywords"
11
11
  group :development do
12
12
  gem "rspec"
13
13
  gem "bundler", "~> 2.0"
14
- gem "juwelier", "~> 2.1.0"
14
+ gem "juwelier"
15
15
  end
16
16
 
17
17
  group "test" do
data/Gemfile.lock CHANGED
@@ -1,134 +1,156 @@
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)
7
- rack (~> 2.0, >= 2.0.8)
4
+ actionpack (6.1.4.1)
5
+ actionview (= 6.1.4.1)
6
+ activesupport (= 6.1.4.1)
7
+ rack (~> 2.0, >= 2.0.9)
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.1.4.1)
12
+ activesupport (= 6.1.4.1)
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.1.4.1)
18
+ activesupport (= 6.1.4.1)
19
+ activerecord (6.1.4.1)
20
+ activemodel (= 6.1.4.1)
21
+ activesupport (= 6.1.4.1)
22
+ activesupport (6.1.4.1)
23
23
  concurrent-ruby (~> 1.0, >= 1.0.2)
24
- i18n (>= 0.7, < 2)
25
- minitest (~> 5.1)
26
- tzinfo (~> 1.1)
27
- zeitwerk (~> 2.2, >= 2.2.2)
28
- addressable (2.7.0)
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ tzinfo (~> 2.0)
27
+ zeitwerk (~> 2.3)
28
+ addressable (2.8.0)
29
29
  public_suffix (>= 2.0.2, < 5.0)
30
30
  axiom-types (0.1.1)
31
31
  descendants_tracker (~> 0.0.4)
32
32
  ice_nine (~> 0.11.0)
33
33
  thread_safe (~> 0.3, >= 0.3.1)
34
34
  builder (3.2.4)
35
- coderay (1.1.2)
35
+ coderay (1.1.3)
36
36
  coercible (1.0.0)
37
37
  descendants_tracker (~> 0.0.1)
38
- concurrent-ruby (1.1.6)
38
+ concurrent-ruby (1.1.9)
39
39
  crass (1.0.6)
40
40
  descendants_tracker (0.0.4)
41
41
  thread_safe (~> 0.3, >= 0.3.1)
42
- diff-lcs (1.3)
43
- equalizer (0.0.11)
44
- erubi (1.9.0)
45
- faraday (0.17.3)
42
+ diff-lcs (1.4.4)
43
+ erubi (1.10.0)
44
+ faraday (1.8.0)
45
+ faraday-em_http (~> 1.0)
46
+ faraday-em_synchrony (~> 1.0)
47
+ faraday-excon (~> 1.1)
48
+ faraday-httpclient (~> 1.0.1)
49
+ faraday-net_http (~> 1.0)
50
+ faraday-net_http_persistent (~> 1.1)
51
+ faraday-patron (~> 1.0)
52
+ faraday-rack (~> 1.0)
46
53
  multipart-post (>= 1.2, < 3)
47
- git (1.7.0)
54
+ ruby2_keywords (>= 0.0.4)
55
+ faraday-em_http (1.0.0)
56
+ faraday-em_synchrony (1.0.0)
57
+ faraday-excon (1.1.0)
58
+ faraday-httpclient (1.0.1)
59
+ faraday-net_http (1.0.1)
60
+ faraday-net_http_persistent (1.2.0)
61
+ faraday-patron (1.0.0)
62
+ faraday-rack (1.0.0)
63
+ git (1.9.1)
48
64
  rchardet (~> 1.8)
49
- github_api (0.18.2)
65
+ github_api (0.19.0)
50
66
  addressable (~> 2.4)
51
67
  descendants_tracker (~> 0.0.4)
52
- faraday (~> 0.8)
68
+ faraday (>= 0.8, < 2)
53
69
  hashie (~> 3.5, >= 3.5.2)
54
70
  oauth2 (~> 1.0)
55
71
  hashie (3.6.0)
56
72
  highline (2.0.3)
57
- i18n (1.8.2)
73
+ i18n (1.8.11)
58
74
  concurrent-ruby (~> 1.0)
59
75
  ice_nine (0.11.2)
60
- juwelier (2.1.3)
76
+ juwelier (2.4.9)
61
77
  builder
62
- bundler (>= 1.13)
63
- git (>= 1.2.5)
78
+ bundler
79
+ git
64
80
  github_api
65
- highline (>= 1.6.15)
66
- nokogiri (>= 1.5.10)
81
+ highline
82
+ kamelcase (~> 0)
83
+ nokogiri
84
+ psych
67
85
  rake
68
86
  rdoc
69
- semver
70
- jwt (2.2.1)
71
- loofah (2.5.0)
87
+ semver2
88
+ jwt (2.3.0)
89
+ kamelcase (0.0.2)
90
+ semver2 (~> 3)
91
+ loofah (2.13.0)
72
92
  crass (~> 1.0.2)
73
93
  nokogiri (>= 1.5.9)
74
- method_source (0.9.0)
75
- mini_portile2 (2.4.0)
76
- minitest (5.14.1)
77
- multi_json (1.14.1)
94
+ method_source (1.0.0)
95
+ mini_portile2 (2.6.1)
96
+ minitest (5.14.4)
97
+ multi_json (1.15.0)
78
98
  multi_xml (0.6.0)
79
99
  multipart-post (2.1.1)
80
- nokogiri (1.10.9)
81
- mini_portile2 (~> 2.4.0)
82
- oauth2 (1.4.4)
100
+ nokogiri (1.12.5)
101
+ mini_portile2 (~> 2.6.1)
102
+ racc (~> 1.4)
103
+ oauth2 (1.4.7)
83
104
  faraday (>= 0.8, < 2.0)
84
105
  jwt (>= 1.0, < 3.0)
85
106
  multi_json (~> 1.3)
86
107
  multi_xml (~> 0.5)
87
108
  rack (>= 1.2, < 3)
88
- pry (0.11.3)
89
- coderay (~> 1.1.0)
90
- method_source (~> 0.9.0)
91
- pry-rails (0.3.6)
109
+ pry (0.14.1)
110
+ coderay (~> 1.1)
111
+ method_source (~> 1.0)
112
+ pry-rails (0.3.9)
92
113
  pry (>= 0.10.4)
93
- public_suffix (4.0.5)
94
- rack (2.2.2)
114
+ psych (4.0.2)
115
+ public_suffix (4.0.6)
116
+ racc (1.6.0)
117
+ rack (2.2.3)
95
118
  rack-test (1.1.0)
96
119
  rack (>= 1.0, < 3)
97
120
  rails-dom-testing (2.0.3)
98
121
  activesupport (>= 4.2.0)
99
122
  nokogiri (>= 1.6)
100
- rails-html-sanitizer (1.3.0)
123
+ rails-html-sanitizer (1.4.2)
101
124
  loofah (~> 2.3)
102
- rake (13.0.1)
125
+ rake (13.0.6)
103
126
  rchardet (1.8.0)
104
- rdoc (6.2.1)
105
- redis (4.1.4)
106
- rspec (3.7.0)
107
- rspec-core (~> 3.7.0)
108
- rspec-expectations (~> 3.7.0)
109
- rspec-mocks (~> 3.7.0)
110
- rspec-core (3.7.1)
111
- rspec-support (~> 3.7.0)
112
- rspec-expectations (3.7.0)
127
+ rdoc (6.3.3)
128
+ redis (4.5.1)
129
+ rspec (3.10.0)
130
+ rspec-core (~> 3.10.0)
131
+ rspec-expectations (~> 3.10.0)
132
+ rspec-mocks (~> 3.10.0)
133
+ rspec-core (3.10.1)
134
+ rspec-support (~> 3.10.0)
135
+ rspec-expectations (3.10.1)
113
136
  diff-lcs (>= 1.2.0, < 2.0)
114
- rspec-support (~> 3.7.0)
115
- rspec-mocks (3.7.0)
137
+ rspec-support (~> 3.10.0)
138
+ rspec-mocks (3.10.2)
116
139
  diff-lcs (>= 1.2.0, < 2.0)
117
- rspec-support (~> 3.7.0)
118
- rspec-support (3.7.1)
119
- rspec_junit_formatter (0.3.0)
140
+ rspec-support (~> 3.10.0)
141
+ rspec-support (3.10.3)
142
+ rspec_junit_formatter (0.4.1)
120
143
  rspec-core (>= 2, < 4, != 2.12.0)
121
- ruby2_keywords (0.0.2)
122
- semver (1.0.1)
144
+ ruby2_keywords (0.0.5)
145
+ semver2 (3.4.2)
123
146
  thread_safe (0.3.6)
124
- tzinfo (1.2.7)
125
- thread_safe (~> 0.1)
126
- virtus (1.0.5)
147
+ tzinfo (2.0.4)
148
+ concurrent-ruby (~> 1.0)
149
+ virtus (2.0.0)
127
150
  axiom-types (~> 0.1)
128
151
  coercible (~> 1.0)
129
152
  descendants_tracker (~> 0.0, >= 0.0.3)
130
- equalizer (~> 0.0, >= 0.0.9)
131
- zeitwerk (2.3.0)
153
+ zeitwerk (2.5.1)
132
154
 
133
155
  PLATFORMS
134
156
  ruby
@@ -137,7 +159,7 @@ DEPENDENCIES
137
159
  actionpack (>= 4.2.6)
138
160
  activerecord (>= 4.2.6)
139
161
  bundler (~> 2.0)
140
- juwelier (~> 2.1.0)
162
+ juwelier
141
163
  pry-rails
142
164
  redis
143
165
  rspec
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
 
@@ -23,11 +24,11 @@ gem "rails-patterns"
23
24
  Then `bundle install`
24
25
 
25
26
  ## Query
26
-
27
+
27
28
  ### When to use it
28
29
 
29
- One should consider using query objects pattern when in need to perform complex querying on active record relation.
30
- 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.
31
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.
32
33
  Also whenever a chain of scopes is to be used, one should consider using query object too.
33
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).
@@ -97,9 +98,9 @@ Service objects are also useful for handling processes involving multiple steps.
97
98
  * It is recommended for `#call` method to be the only public method of service object (besides state readers)
98
99
  * It is recommended to name service object classes after commands (e.g. `ActivateUser` instead of `UserActivation`)
99
100
 
100
- ### Other
101
+ ### Other
101
102
 
102
- 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).
103
104
 
104
105
  ### Examples
105
106
 
@@ -347,6 +348,94 @@ 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
+ * By default empty ruleset is satisfied.
373
+
374
+ #### Forcing rules
375
+
376
+ On some occasions there is a situation in which some condition should be overridable.
377
+ 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.
378
+ 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.
379
+
380
+ It might be good idea to test for `#forceable?` on the UI level to control visibility of such link/button.
381
+
382
+ 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.
383
+ If not, ruleset containing such rule (and the rule itself) would not be "forceable" and UI could reflect that by querying `#forceable?`.
384
+
385
+ #### Regular and strong rulesets
386
+
387
+ While regular `Ruleset` can be satisfied or forced if any of its rules in not applicable, the
388
+ `StrongRuleset` is not satisfied and not "forceable" if any of its rules is not applicable.
389
+
390
+ #### `#not_applicable?` vs `#applicable?`
391
+
392
+ It might be surprising that is is the negated version of the `#applicable?` predicate methods that is overridable.
393
+ However, from the actual usage perspective, it usually easier to conceptually define when condition makes no sense than other way around.
394
+
395
+ ### Examples
396
+
397
+ #### Declaration
398
+
399
+ ```ruby
400
+ class OrderIsSentRule < Patterns::Rule
401
+ def satisfied?
402
+ subject.sent?
403
+ end
404
+ end
405
+
406
+ class OrderIsPaidRule < Patterns::Rule
407
+ def satisfied?
408
+ subject.paid?
409
+ end
410
+
411
+ def forceable?
412
+ true
413
+ end
414
+ end
415
+
416
+ OrderCompletedNotificationRuleset = Class.new(Patterns::Ruleset)
417
+ OrderCompletedNotificationRuleset.
418
+ add_rule(:order_is_sent_rule).
419
+ add_rule(:order_is_paid_rule)
420
+ ```
421
+
422
+ #### Usage
423
+
424
+ ```ruby
425
+ OrderIsPaidRule.new(order).satisfied?
426
+ OrderCompletedNotificationRuleset.new(order).satisfied?
427
+
428
+ ResendOrderNotification.call(order) if OrderCompletedNotificationRuleset.new(order).satisfied?(force: true)
429
+ ```
430
+
350
431
  ## Further reading
351
432
 
352
433
  * [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)
434
+
435
+ ## About Selleo
436
+
437
+ ![selleo](https://raw.githubusercontent.com/Selleo/selleo-resources/master/public/github_footer.png)
438
+
439
+ 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.
440
+
441
+ 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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.3
1
+ 0.11.0
@@ -9,8 +9,8 @@ module Patterns
9
9
  @subject = args.first
10
10
  end
11
11
 
12
- def self.result(*args)
13
- new(*args).cached_result
12
+ def self.result(*args, &block)
13
+ new(*args).cached_result(&block)
14
14
  end
15
15
 
16
16
  class << self
@@ -22,12 +22,12 @@ module Patterns
22
22
  self.cache_expiry_every = period
23
23
  end
24
24
 
25
- def cached_result
25
+ def cached_result(&block)
26
26
  if cache_expiry_period.blank?
27
- result
27
+ result(&block)
28
28
  else
29
29
  Rails.cache.fetch(cache_key, expires_in: cache_expiry_period) do
30
- result
30
+ result(&block)
31
31
  end
32
32
  end
33
33
  end
@@ -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,67 @@
1
+ module Patterns
2
+ class Ruleset
3
+ class << self
4
+ attr_accessor :rule_names
5
+ end
6
+
7
+ def self.rules
8
+ (rule_names || []).map do |rule_name|
9
+ rule_name.to_s.classify.constantize
10
+ end
11
+ end
12
+
13
+ def self.add_rule(rule_name)
14
+ self.rule_names ||= []
15
+ self.rule_names << rule_name.to_sym
16
+ self
17
+ end
18
+
19
+ def initialize(subject = nil)
20
+ @rules = self.class.rules.map { |rule| rule.new(subject) }
21
+ end
22
+
23
+ def satisfied?(force: false)
24
+ rules.all? do |rule|
25
+ rule.satisfied? ||
26
+ rule.not_applicable? ||
27
+ (force && rule.forceable?)
28
+ end
29
+ end
30
+
31
+ def not_satisfied?
32
+ !satisfied?
33
+ end
34
+
35
+ def applicable?
36
+ !not_applicable?
37
+ end
38
+
39
+ def not_applicable?
40
+ rules.all?(&:not_applicable?)
41
+ end
42
+
43
+ def forceable?
44
+ rules.all? do |rule|
45
+ rule.forceable? ||
46
+ rule.not_applicable? ||
47
+ rule.satisfied?
48
+ end
49
+ end
50
+
51
+ def each(&block)
52
+ return enum_for(:each) unless block_given?
53
+
54
+ rules.each do |rule_or_ruleset|
55
+ if rule_or_ruleset.is_a?(Ruleset)
56
+ rule_or_ruleset.each(&block)
57
+ else
58
+ yield rule_or_ruleset
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :rules
66
+ end
67
+ end
@@ -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"
@@ -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.11.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.11.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 = "2022-02-09"
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,13 +46,16 @@ 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.0.8".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
@@ -62,7 +68,7 @@ Gem::Specification.new do |s|
62
68
  s.add_runtime_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
63
69
  s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
64
70
  s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
65
- s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
71
+ s.add_development_dependency(%q<juwelier>.freeze, [">= 0"])
66
72
  else
67
73
  s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
68
74
  s.add_dependency(%q<actionpack>.freeze, [">= 4.2.6"])
@@ -70,7 +76,7 @@ Gem::Specification.new do |s|
70
76
  s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
71
77
  s.add_dependency(%q<rspec>.freeze, [">= 0"])
72
78
  s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
73
- s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
79
+ s.add_dependency(%q<juwelier>.freeze, [">= 0"])
74
80
  end
75
81
  else
76
82
  s.add_dependency(%q<activerecord>.freeze, [">= 4.2.6"])
@@ -79,7 +85,7 @@ Gem::Specification.new do |s|
79
85
  s.add_dependency(%q<ruby2_keywords>.freeze, [">= 0"])
80
86
  s.add_dependency(%q<rspec>.freeze, [">= 0"])
81
87
  s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
82
- s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
88
+ s.add_dependency(%q<juwelier>.freeze, [">= 0"])
83
89
  end
84
90
  end
85
91
 
@@ -60,6 +60,18 @@ RSpec.describe Patterns::Calculation do
60
60
 
61
61
  expect(CustomCalculation.result(nil, arg_1: 20, arg_2: 30)).to eq([20, 30])
62
62
  end
63
+
64
+ it 'executes calculation with given block' do
65
+ CustomCalculation = Class.new(Patterns::Calculation) do
66
+ private
67
+
68
+ def result
69
+ yield(subject)
70
+ end
71
+ end
72
+
73
+ expect(CustomCalculation.result(5) { |a| a * 3 }).to eq(15)
74
+ end
63
75
  end
64
76
 
65
77
  describe "caching" do
@@ -0,0 +1,44 @@
1
+ RSpec.describe Patterns::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(Patterns::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(Patterns::Rule)
15
+ CustomRule = Class.new(Patterns::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(Patterns::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,255 @@
1
+ RSpec.describe Patterns::Ruleset do
2
+ describe '#forceable?' do
3
+ context 'all rules are forceable' do
4
+ it 'returns true' do
5
+ with_mocked_rules do |rules|
6
+ subject = double
7
+ rules << mock_rule(:rule_1, is_forceable: true)
8
+ rules << mock_rule(:rule_2, is_forceable: true)
9
+
10
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
11
+ custom_ruleset_klass.add_rule(:rule_1)
12
+ custom_ruleset_klass.add_rule(:rule_2)
13
+
14
+ expect(custom_ruleset_klass.new(subject).forceable?).to eq true
15
+ end
16
+ end
17
+ end
18
+
19
+ context 'at least one rule is not forceable' do
20
+ it 'returns false' do
21
+ with_mocked_rules do |rules|
22
+ subject = double
23
+ rules << mock_rule(:rule_1, is_forceable: false, is_satisfied: false, is_applicable: true)
24
+ rules << mock_rule(:rule_2, is_forceable: true)
25
+
26
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
27
+ custom_ruleset_klass.add_rule(:rule_1)
28
+ custom_ruleset_klass.add_rule(:rule_2)
29
+
30
+ expect(custom_ruleset_klass.new(subject).forceable?).to eq false
31
+ end
32
+ end
33
+
34
+ context 'and rule is satisfied' do
35
+ it 'returns true' do
36
+ with_mocked_rules do |rules|
37
+ subject = double
38
+ rules << mock_rule(
39
+ :rule_1,
40
+ is_forceable: false,
41
+ is_satisfied: true,
42
+ is_applicable: true
43
+ )
44
+ rules << mock_rule(:rule_2, is_forceable: true)
45
+
46
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
47
+ custom_ruleset_klass.add_rule(:rule_1)
48
+ custom_ruleset_klass.add_rule(:rule_2)
49
+
50
+ expect(custom_ruleset_klass.new(subject).forceable?).to eq true
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'and rule is not applicable' do
56
+ it 'returns true' do
57
+ with_mocked_rules do |rules|
58
+ subject = double
59
+ rules << mock_rule(
60
+ :rule_1,
61
+ is_forceable: false,
62
+ is_satisfied: false,
63
+ is_applicable: false
64
+ )
65
+ rules << mock_rule(:rule_2, is_forceable: true)
66
+
67
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
68
+ custom_ruleset_klass.add_rule(:rule_1)
69
+ custom_ruleset_klass.add_rule(:rule_2)
70
+
71
+ expect(custom_ruleset_klass.new(subject).forceable?).to eq true
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#not_applicable?' do
79
+ context 'all rules are not applicable' do
80
+ it 'returns true' do
81
+ with_mocked_rules do |rules|
82
+ subject = double
83
+ rules << mock_rule(:rule_1, is_applicable: false)
84
+ rules << mock_rule(:rule_2, is_applicable: false)
85
+
86
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
87
+ custom_ruleset_klass.add_rule(:rule_1)
88
+ custom_ruleset_klass.add_rule(:rule_2)
89
+
90
+ expect(custom_ruleset_klass.new(subject).not_applicable?).to eq true
91
+ end
92
+ end
93
+ end
94
+
95
+ context 'at least one rule is applicable' do
96
+ it 'returns false' do
97
+ with_mocked_rules do |rules|
98
+ subject = double
99
+ rules << mock_rule(:rule_1, is_applicable: false)
100
+ rules << mock_rule(:rule_2, is_applicable: true)
101
+
102
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
103
+ custom_ruleset_klass.add_rule(:rule_1)
104
+ custom_ruleset_klass.add_rule(:rule_2)
105
+
106
+ expect(custom_ruleset_klass.new(subject).not_applicable?).to eq false
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#satisfied?' do
113
+ context 'all rules are satisfied' do
114
+ it 'returns true' do
115
+ with_mocked_rules do |rules|
116
+ subject = double
117
+ rules << mock_rule(:rule_1)
118
+ rules << mock_rule(:rule_2)
119
+
120
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
121
+ custom_ruleset_klass.add_rule(:rule_1)
122
+ custom_ruleset_klass.add_rule(:rule_2)
123
+
124
+ expect(custom_ruleset_klass.new(subject).satisfied?).to eq true
125
+ end
126
+ end
127
+ end
128
+
129
+ context 'at least one rule is not satisfied' do
130
+ it 'returns false' do
131
+ with_mocked_rules do |rules|
132
+ subject = double
133
+ rules << mock_rule(:rule_1)
134
+ rules << mock_rule(:rule_2, is_satisfied: false)
135
+
136
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
137
+ custom_ruleset_klass.add_rule(:rule_1)
138
+ custom_ruleset_klass.add_rule(:rule_2)
139
+
140
+ expect(custom_ruleset_klass.new(subject).satisfied?).to eq false
141
+ end
142
+ end
143
+
144
+ context 'when rule is not applicable' do
145
+ it 'returns true' do
146
+ with_mocked_rules do |rules|
147
+ subject = double
148
+ rules << mock_rule(:rule_1)
149
+ rules << mock_rule(:rule_2, is_satisfied: false, is_applicable: false)
150
+
151
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
152
+ custom_ruleset_klass.add_rule(:rule_1)
153
+ custom_ruleset_klass.add_rule(:rule_2)
154
+
155
+ expect(custom_ruleset_klass.new(subject).satisfied?).to eq true
156
+ end
157
+ end
158
+ end
159
+
160
+ context 'when provided with force: true' do
161
+ context 'when rule is forceable' 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_forceable: true)
167
+
168
+ custom_ruleset_klass = Class.new(Patterns::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?(force: true)).to eq true
173
+ end
174
+ end
175
+ end
176
+
177
+ context 'when rule is not forceable' do
178
+ it 'returns false' do
179
+ with_mocked_rules do |rules|
180
+ subject = double
181
+ rules << mock_rule(:rule_1)
182
+ rules << mock_rule(:rule_2, is_satisfied: false, is_forceable: false)
183
+
184
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
185
+ custom_ruleset_klass.add_rule(:rule_1)
186
+ custom_ruleset_klass.add_rule(:rule_2)
187
+
188
+ expect(custom_ruleset_klass.new(subject).satisfied?(force: true)).to eq false
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'ruleset has no rules' do
196
+ it 'returns true' do
197
+ with_mocked_rules do |_rules|
198
+ subject = double
199
+
200
+ custom_ruleset_klass = Class.new(Patterns::Ruleset)
201
+
202
+ expect(custom_ruleset_klass.new(subject).satisfied?).to eq true
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ describe '#each' do
209
+ it 'yields all rules for ruleset' do
210
+ with_mocked_rules do |rules|
211
+ rules << (_, rule_1 = mock_rule(:rule_1))
212
+ rules << (_, rule_2 = mock_rule(:rule_2))
213
+ rules << (_, rule_3 = mock_rule(:rule_3))
214
+ custom_ruleset_klass_1 = Class.new(Patterns::Ruleset)
215
+ custom_ruleset_klass_1.add_rule(:rule_1)
216
+ custom_ruleset_klass_1.add_rule(:rule_2)
217
+ Ruleset2 = Class.new(Patterns::Ruleset)
218
+ Ruleset2.add_rule(:rule_3)
219
+ custom_ruleset_klass_1.add_rule(:ruleset_2)
220
+
221
+ ruleset = custom_ruleset_klass_1.new(double)
222
+
223
+ expect { |b| ruleset.each(&b) }.to yield_successive_args(rule_1, rule_2, rule_3)
224
+ ensure
225
+ remove_class(Ruleset2)
226
+ end
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ def mock_rule(rule_name, is_applicable: true, is_satisfied: true, is_forceable: true)
233
+ klass = Object.const_set(rule_name.to_s.classify, Class.new(Patterns::Rule))
234
+ rule = double(
235
+ not_applicable?: !is_applicable,
236
+ satisfied?: is_satisfied,
237
+ forceable?: is_forceable
238
+ )
239
+ allow(klass).to receive(:new).with(anything) { rule }
240
+ [klass, rule]
241
+ end
242
+
243
+ def with_mocked_rules
244
+ rules_storage = []
245
+ yield rules_storage
246
+ ensure
247
+ rules_storage.each do |rule_klass, _rule_instance|
248
+ remove_class(rule_klass)
249
+ end
250
+ end
251
+
252
+ def remove_class(klass)
253
+ Object.send(:remove_const, klass.name.to_sym)
254
+ end
255
+ end
@@ -0,0 +1,79 @@
1
+ RSpec.describe Patterns::StrongRuleset do
2
+ it 'inherites from Ruleset' do
3
+ custom_strong_ruleset_klass = Class.new(Patterns::StrongRuleset)
4
+ expect(custom_strong_ruleset_klass.ancestors).to include Patterns::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(Patterns::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(Patterns::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(Patterns::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(Patterns::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.11.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: 2022-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: juwelier
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 2.1.0
103
+ version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 2.1.0
110
+ version: '0'
111
111
  description: A collection of lightweight, standardized, rails-oriented patterns.
112
112
  email: b.kosmowski@selleo.com
113
113
  executables: []
@@ -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.0.8
171
+ signing_key:
167
172
  specification_version: 4
168
173
  summary: A collection of lightweight, standardized, rails-oriented patterns.
169
174
  test_files: []