brule 0.4.0 → 0.4.1

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: 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: []