brule 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86b2ca827bc1365eda3e1c2be029086dcbd7777bd70a2ead29469c502d171a13
4
- data.tar.gz: bc97f46ef11b350b664e6278b9978d75ed85f703681e0e7e896aafe853f55888
3
+ metadata.gz: 669938141fcd377863b801be38726843c5f1ed81737754e76178923298794541
4
+ data.tar.gz: aa7e4e0b3c4a84f0ab2c449518097e3519d8ede931f350119946d7beeed11ea0
5
5
  SHA512:
6
- metadata.gz: 3369d8945f69fe5334338550207943dd4a200bcbf3ddae031cbed2fa1101dbecacb5f06c38d9999d80d9fbd0d2c5f08da9ce5ad5e3bcec6ea3ddef844c5b31ff
7
- data.tar.gz: 5d74c2c6bb63d900b8cff010d5a8266dc7226ab614820c827f808d3c0a6370c297f8e3188f9d9bdde3186cb280dd627aac2dda253cd643ab09c82899dd0bd87c
6
+ metadata.gz: 2e68b2077b6c133989d430b4b8226aea8b06dda3264b0e92892ec2684265350fcc969a9737d290cf32683c50cedee0699331a35c28830cd00b01872272aaf32f
7
+ data.tar.gz: 44da1bdbecb77ae14b17230997ce3b3b5a47c623ebf86a44836bd8a245f2f12644d152edb2f50f47834e99eed3f5cd2c7dbc072319dab7b9403891cfac16adce
data/README.md CHANGED
@@ -7,6 +7,8 @@ business rules. It helps when:
7
7
  * You want to make your tests independent from your production data.
8
8
  * You want to introspect the computation of a result.
9
9
 
10
+ ![Test](https://github.com/nicoolas25/brule-rb/workflows/Test/badge.svg?branch=master)
11
+
10
12
  ## How does it work
11
13
 
12
14
  The idea is very similar to function composition or Rack's middlewares. It is a
@@ -21,17 +23,20 @@ doing that, the engine apply its _rules_.
21
23
 
22
24
  Each rule have two methods: `#trigger?` and `#apply`. `#apply` runs only when
23
25
  `trigger?` is true. `#apply` writes stuff to the context for other rules and
24
- for the engine to produce the result.
26
+ for the engine to produce the result. Rules can be initialized with a
27
+ _configuration_.
25
28
 
26
29
  ![Rule](https://github.com/nicoolas25/brule-rb/blob/master/docs/img/rule.png?raw=true)
27
30
 
31
+
28
32
  A typical usage for this kind of engine is to use it to compute the price of a
29
33
  service or a good. But, this is not limited to that use-case as an engine
30
34
  would be able to produce any kind of results.
31
35
 
32
36
  ## How does it look
33
37
 
34
- Here is an example from the [Elephant Carpaccio][elephant] kata. The specs are:
38
+ [Here](https://nicoolas25.github.io/brule-rb/examples/elephant_carpaccio.html)
39
+ is an example from the [Elephant Carpaccio][elephant] kata. The specs are:
35
40
 
36
41
  > Accept 3 inputs from the user:
37
42
  >
@@ -42,116 +47,19 @@ Here is an example from the [Elephant Carpaccio][elephant] kata. The specs are:
42
47
  > Output the total price. Give a discount based on the total price, add state
43
48
  > tax based on the state and the discounted price.
44
49
 
45
- ```ruby
46
- require "brule"
47
-
48
- module Pricing
49
- class Engine < Brule::Engine
50
- def result
51
- context.fetch(:price)
52
- end
53
- end
54
-
55
- class OrderTotal < Brule::Rule
56
- context_reader :unit_price, :item_count
57
- context_writer :price
58
-
59
- def apply
60
- self.price = unit_price * item_count
61
- end
62
- end
63
-
64
- class Discount < Brule::Rule
65
- config_reader :rates
66
- context_accessor :price, :discount_rate, :discount_amount
67
-
68
- def trigger?
69
- !applicable_discount.nil?
70
- end
71
-
72
- def apply
73
- self.discount_rate = applicable_discount.last
74
- self.discount_amount = (price * discount_rate).ceil
75
- self.price = price - discount_amount
76
- end
77
-
78
- private
79
-
80
- def applicable_discount
81
- rates
82
- .sort_by { |order_value, _| order_value * -1 }
83
- .find { |order_value, _| order_value <= context.fetch(:price) }
84
- end
85
- end
86
-
87
- class StateTax < Brule::Rule
88
- config_reader :rates
89
- context_reader :state
90
- context_accessor :price, :state_tax
91
-
92
- def apply
93
- tax_rate = rates.fetch(state)
94
- self.state_tax = (price * tax_rate).ceil
95
- self.price = price + state_tax
96
- end
97
- end
98
- end
99
-
100
- require "bigdecimal"
101
- require "bigdecimal/util"
50
+ [This](https://nicoolas25.github.io/brule-rb/examples/elephant_carpaccio.html)
51
+ is the smallest example and the _best_ one to refer to. More examples are
52
+ available:
102
53
 
103
- engine = Pricing::Engine.new(
104
- rules: [
105
- Pricing::OrderTotal.new,
106
- Pricing::Discount.new(
107
- rates: [
108
- [ 1_000_00, "0.03".to_d ],
109
- [ 5_000_00, "0.05".to_d ],
110
- [ 7_000_00, "0.07".to_d ],
111
- [ 10_000_00, "0.10".to_d ],
112
- [ 50_000_00, "0.15".to_d ],
113
- ],
114
- ),
115
- Pricing::StateTax.new(
116
- rates: {
117
- "UT" => "0.0685".to_d,
118
- "NV" => "0.0800".to_d,
119
- "TX" => "0.0625".to_d,
120
- "AL" => "0.0400".to_d,
121
- "CA" => "0.0825".to_d,
122
- },
123
- ),
124
- ],
125
- )
126
-
127
- result = engine.call(
128
- item_count: 100,
129
- unit_price: 100_00,
130
- state: "NV",
131
- )
132
-
133
- # Access the main result
134
- result # => 9_720_00 ($9,720.00)
135
-
136
- # Access the context
137
- engine.context.fetch_values(
138
- :discount_rate, # => 0.1 (10%)
139
- :discount_amount, # => 1_000_00 ($1,000.00)
140
- :state_tax, # => 720_00 ($720.00)
141
- )
142
-
143
- # Access the history
144
- engine.history(key: :price) # => [
145
- # => [:initial, nil],
146
- # => [#<struct Pricing::OrderTotal ...>, 10_000_00],
147
- # => [#<struct Pricing::Discount ...>, 9_000_00],
148
- # => [#<struct Pricing::StateTax ...>, 9_720_00],
149
- # => ]
150
- ```
54
+ - [Elephant Carpaccio](https://nicoolas25.github.io/brule-rb/examples/elephant_carpaccio.html)
55
+ - [Car rental](https://nicoolas25.github.io/brule-rb/examples/car_rental.html)
56
+ - [Studio rental](https://nicoolas25.github.io/brule-rb/examples/studio_rental.html)
57
+ - [Promo code](https://nicoolas25.github.io/brule-rb/examples/promo_code.html) (WIP)
151
58
 
152
59
  ## What does it bring to the table
153
60
 
154
- If you compare this approach with a simple method like this one:
61
+ If you compare [this approach](https://nicoolas25.github.io/brule-rb/examples/elephant_carpaccio.html)
62
+ with a simple method like this one:
155
63
 
156
64
  ```ruby
157
65
  require "bigdecimal"
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Brule
2
4
  class Engine
3
- attr_reader :context
5
+ attr_reader :context, :rules
4
6
 
5
7
  def initialize(rules:)
6
8
  @rules = rules
@@ -19,6 +21,19 @@ module Brule
19
21
  @history.map { |tag, content| [tag, content.fetch(key, nil)] }
20
22
  end
21
23
 
24
+ def to_hash
25
+ { 'engine_class' => self.class.name, 'rules' => @rules.map(&:to_hash) }
26
+ end
27
+
28
+ def self.from_hash(hash)
29
+ engine_class = Object.const_get(hash.fetch('engine_class'))
30
+ rules = hash.fetch('rules').map do |rule_hash|
31
+ rule_class = Object.const_get(rule_hash.fetch('rule_class'))
32
+ rule_class.from_hash(rule_hash)
33
+ end
34
+ engine_class.new(rules: rules)
35
+ end
36
+
22
37
  private
23
38
 
24
39
  def snapshot!(tag:)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Brule
2
4
  Rule = Struct.new(:config) do
3
5
  attr_accessor :context
@@ -14,26 +16,38 @@ module Brule
14
16
  self
15
17
  end
16
18
 
17
- def self.context_reader(*symbols)
18
- symbols.each do |symbol|
19
- define_method(symbol) { context.fetch(symbol) }
19
+ def to_hash
20
+ config_hash = block_given? ? yield(config) : config&.to_hash
21
+ { 'rule_class' => self.class.name, 'config' => config_hash }
22
+ end
23
+
24
+ def self.from_hash(hash)
25
+ raise ArgumentError unless hash.fetch('rule_class') == name
26
+
27
+ config_hash = hash.fetch('config')
28
+ new(block_given? ? yield(config_hash) : config_hash)
29
+ end
30
+
31
+ def self.context_reader(*keys)
32
+ keys.each do |key|
33
+ define_method(key) { context.fetch(key) }
20
34
  end
21
35
  end
22
36
 
23
- def self.context_writer(*symbols)
24
- symbols.each do |symbol|
25
- define_method("#{symbol}=") { |value| context[symbol] = value }
37
+ def self.context_writer(*keys)
38
+ keys.each do |key|
39
+ define_method("#{key}=") { |value| context[key] = value }
26
40
  end
27
41
  end
28
42
 
29
- def self.context_accessor(*symbols)
30
- context_reader(*symbols)
31
- context_writer(*symbols)
43
+ def self.context_accessor(*keys)
44
+ context_reader(*keys)
45
+ context_writer(*keys)
32
46
  end
33
47
 
34
- def self.config_reader(*symbols)
35
- symbols.each do |symbol|
36
- define_method(symbol) { config.fetch(symbol) }
48
+ def self.config_reader(*keys)
49
+ keys.each do |key|
50
+ define_method(key) { config.fetch(key) }
37
51
  end
38
52
  end
39
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Brule
2
4
  module Utils
3
5
  autoload :Either, 'brule/utils/either'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
 
3
5
  module Brule
@@ -10,7 +12,26 @@ module Brule
10
12
 
11
13
  def_delegators :only_match, :apply, :trigger?, :to_tag
12
14
 
13
- config_reader :rules
15
+ config_reader 'rules'
16
+
17
+ def to_hash
18
+ super do |config|
19
+ config.to_hash.tap do |config_hash|
20
+ config_hash['rules'] = config.fetch('rules').map(&:to_hash)
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.from_hash(*)
26
+ super do |config_hash|
27
+ config_hash.tap do |config|
28
+ config['rules'] = config_hash.fetch('rules').map do |rule_hash|
29
+ rule_class = Object.const_get(rule_hash.fetch('rule_class'))
30
+ rule_class.from_hash(rule_hash)
31
+ end
32
+ end
33
+ end
34
+ end
14
35
 
15
36
  private
16
37
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brule
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Zermati
@@ -38,6 +38,62 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.32'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.32'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.18'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.18'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov-lcov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.4'
41
97
  description:
42
98
  email: ''
43
99
  executables: []