prezzo 0.5.1 → 1.0.0.pre.rc

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
  SHA1:
3
- metadata.gz: 0e9332660f2522054b623e1ba93eb6bce0d543d9
4
- data.tar.gz: 7abe8f70afd7380d704db3dc114cefc4a71e5328
3
+ metadata.gz: 4409631f03de4a6dff3ebfbb48f25a0e4e9dcac4
4
+ data.tar.gz: ff6c4ec10622aeb9e924cbbe6b4c9b95dbd4a12d
5
5
  SHA512:
6
- metadata.gz: aff27c50f0d088a92a1bb8bae5e077c3a662bccf045e4face9f48a4ca9c22bd2b0c64ddbb411695018d440a62fe1159dfc344698b3613ca783bffb148fe89415
7
- data.tar.gz: 480cb54b37ee78b7428908025b3d580b74c8cd82feed2d47d65fbc50391f6793ff8a5d6771da90d2dc392d22ad753940a7b8543866b173e64cc8bd63648b77af
6
+ metadata.gz: 40cb9eead217c8009abbc86225913d3d099dc603a98a4c421d832d021267986d3995bc44033aff88856ee41fb7b378ba4e80ca3127390bdcc514656eda4f81aa
7
+ data.tar.gz: 1201e28ad3e1aa9e2a76c238c4b702e41038a12aa4088560aee8ca79bde0df99a060930023ec4b8a5c531692baf1d7524927a29740eaf64189c7e1719cd26c81
data/README.md CHANGED
@@ -23,215 +23,223 @@ $ gem install prezzo
23
23
 
24
24
  ## Usage
25
25
 
26
- ### Prezzo::Context
26
+ ### The calculation context
27
27
 
28
-
29
- The `Prezzo::Context` is a source of data for your calculators. Basically, it receives a hash of params and it validates its content, in order to make the calculations safe.
28
+ `Prezzo::Context` is a source of data for your calculators. It receives a hash
29
+ of params and makes them available to calculators.
30
30
 
31
31
  e.g.:
32
32
 
33
33
  ```ruby
34
- module Uber
35
- class Context
36
- include Prezzo::Context
37
- CATEGORIES = ["UberX", "UberXL", "UberBlack"].freeze
38
-
39
- validations do
40
- required(:category).filled(included_in?: CATEGORIES)
41
- required(:distance).filled(:float?)
42
- required(:total_cars).filled(:int?)
43
- required(:available_cars).filled(:int?)
44
- end
45
- end
46
- end
34
+ context = Prezzo::Context.new(category: "Expensive", distance: 10.0)
35
+ ```
47
36
 
48
- context = Uber::Context.new(category: "UberBlack", ...)
37
+ ### Simple calculators
49
38
 
50
- # when valid
51
- context.valid?
52
- #=> true
39
+ `Prezzo::Calculator` is the top level concept to describe calculations. Define
40
+ a `formula` method that describe the calculation you want to perform and call
41
+ the `calculate` method.
53
42
 
54
- # when invalid
55
- context.valid?
56
- #=> false
43
+ e.g.:
44
+
45
+ ```ruby
46
+ require "prezzo"
57
47
 
58
- context.errors
59
- # { distance: ["must be a float"]}
48
+ class StaticCalculator
49
+ include Prezzo::Calculator
50
+
51
+ def formula
52
+ 2 * 5
53
+ end
54
+ end
55
+
56
+ StaticCalculator.new.calculate
57
+ #=> 10
60
58
  ```
61
59
 
62
- ### Prezzo::Calculator
60
+ ### Accessing context params
63
61
 
64
- The `Prezzo::Calculator` is a simple interface for injecting dependencies on your calculators and calculating the price. Basically, it makes it possible to receive the context, an Hash of parameters containing the necessary information to calculate your price or a Prezzo::Context.
62
+ Use the `param` dsl to create methods that read data from the context.
65
63
 
66
64
  e.g.:
67
65
 
68
66
  ```ruby
69
67
  require "prezzo"
70
68
 
71
- module Uber
72
- class PricePerDistanceCalculator
73
- include Prezzo::Calculator
74
-
75
- def calculate
76
- price_per_kilometer * distance
77
- end
69
+ class Multiplier
70
+ include Prezzo::Calculator
78
71
 
79
- def price_per_kilometer
80
- 1.30
81
- end
72
+ param :arg1
73
+ param :arg2
82
74
 
83
- def distance
84
- context.fetch(:distance)
85
- end
75
+ def formula
76
+ arg1 * arg2
86
77
  end
87
78
  end
88
79
 
89
- context = Uber::Context.new(distance: 10.0)
90
- Uber::PricePerDistanceCalculator.new(context).calculate
80
+ context = Prezzo::Context.new(arg1: 2, arg2: 10.0)
81
+ Multiplier.new(context).calculate
91
82
  #=> 20.0
92
83
  ```
93
84
 
94
- **Context Validation**
85
+ ### Default params
95
86
 
96
- If you initialize the context with a hash, it will skip the validation, however, any object that responds to `.valid?` will attempt a validation, and it will fail if valid? returns false.
97
-
98
- ### Prezzo::Composable
99
-
100
- The `Prezzo::Composable` module is an abstraction that provides a nice way of injecting other calculators define how the price will be composed with all of those calculators.
87
+ The `param` dsl accepts a default value:
101
88
 
102
89
  e.g.:
103
90
 
104
91
  ```ruby
105
92
  require "prezzo"
106
93
 
107
- module Uber
108
- class RidePriceCalculator
109
- include Prezzo::Calculator
110
- include Prezzo::Composable
94
+ class OptionalCalculator
95
+ include Prezzo::Calculator
111
96
 
112
- composed_by base_fare: BaseFareCalculator,
113
- price_per_distance: PricePerDistanceCalculator,
97
+ param :optional, default: 10.0
114
98
 
115
- def calculate
116
- base_fare + price_per_distance
117
- end
99
+ def formula
100
+ optional * 3
118
101
  end
119
102
  end
120
103
 
121
- context = Uber::Context.new(distance: 10.0)
122
- Uber::RidePriceCalculator.new(context).calculate
123
- #=> 47.3
104
+ context = Prezzo::Context.new(arg1: 2, arg2: 10.0)
105
+ OptionalCalculator.new(context).calculate
106
+ #=> 30.0
124
107
  ```
125
108
 
126
- ### Prezzo::Explainable
109
+ ### Nested context params
127
110
 
128
- The `Prezzo::Explainable` module is an abstraction that provides a nice way of representing how the price was composed.
111
+ The `param` dsl can take a block to access nested data in the context.
129
112
 
130
113
  e.g.:
131
114
 
132
115
  ```ruby
133
116
  require "prezzo"
134
117
 
135
- module Uber
136
- class RidePriceCalculator
137
- include Prezzo::Calculator
138
- include Prezzo::Composable
139
- include Prezzo::Explainable
118
+ class NestedCalculator
119
+ include Prezzo::Calculator
140
120
 
141
- composed_by base_fare: BaseFareCalculator,
142
- price_per_distance: PricePerDistanceCalculator,
143
- explain_with :base_fare, :price_per_distance
121
+ param :level1 do
122
+ param :level2
123
+ end
144
124
 
145
- def calculate
146
- base_fare + price_per_distance
147
- end
125
+ def formula
126
+ level1.level2 * 2
148
127
  end
149
128
  end
150
129
 
151
- context = Uber::Context.new(distance: 10.0)
152
- Uber::RidePriceCalculator.new(context).explain
153
- #=> { total: 25.6, components: { base_fare: 4.3, price_per_distance: 21.3 } }
130
+ context = Prezzo::Context.new(level1: { level2: 10.0 })
131
+ NestedCalculator.new(context).calculate
132
+ #=> 20.0
154
133
  ```
155
134
 
156
- #### Multiline `explain_with`
135
+ ### Composing calculators
157
136
 
158
- `explain_with` can be splitted into several lines.
137
+ Calculators provide the `component` dsl method to make it easy to compose
138
+ calculators. Each calculator will be defined as a method in the calculator.
139
+ They will receive the whole context on instantiation.
140
+
141
+ e.g.:
159
142
 
160
143
  ```ruby
161
- class RidePriceCalculator
162
- include Prezzo::Explainable
144
+ require "prezzo"
145
+
146
+ class ComposedCalculator
147
+ include Prezzo::Calculator
148
+
149
+ component :calculator1, StaticCalculator
150
+ component :calculator2, Multiplier
163
151
 
164
- explain_with :base_fare
165
- explain_with :price_per_distance
152
+ def formula
153
+ calculator1 + calculator2
154
+ end
166
155
  end
156
+
157
+ context = Prezzo::Context.new(arg1: 2, arg2: 10.0)
158
+ ComposedCalculator.new(context).calculate
159
+ #=> 30.0
167
160
  ```
168
161
 
169
- #### ` explain_with` with the `recursive: false` option
162
+ ### Restricting the context
163
+
164
+ You can restrict the context of each calculator by providing a third argument
165
+ to `components`:
166
+
167
+ e.g.:
170
168
 
171
169
  ```ruby
172
- class FooCalculator
170
+ require "prezzo"
171
+
172
+ class RestrictedCalculator
173
173
  include Prezzo::Calculator
174
- include Prezzo::Explainable
175
174
 
176
- explain_with :bar, :baz
175
+ component :calculator1, Multiplier, :side1
176
+ component :calculator2, Multiplier, :side2
177
177
 
178
- def calculate
179
- bar + baz
178
+ def formula
179
+ calculator1 + calculator2
180
180
  end
181
+ end
181
182
 
182
- def bar
183
- 10
184
- end
183
+ context = Prezzo::Context.new(side1: { arg1: 2, arg2: 10.0 }, side2: { arg1: 3, arg2: 20.0 })
184
+ RestrictedCalculator.new(context).calculate
185
+ #=> 80.0
186
+ ```
185
187
 
186
- def baz
187
- 20
188
- end
189
- end
188
+ ### Explanations
190
189
 
191
- class QuxCalculator
192
- include Prezzo::Calculator
193
- include Prezzo::Composable
194
- include Prezzo::Explainable
190
+ The `explain` method provides a nice way of representing how the price was
191
+ composed. It will include all params and components defined in the calculator.
195
192
 
196
- composed_by foo: FooCalculator
193
+ e.g.:
197
194
 
198
- explain_with :foo, recursive: false
195
+ ```ruby
196
+ require "prezzo"
197
+
198
+ class ExplainableCalculator
199
+ include Prezzo::Calculator
199
200
 
200
- def calculate
201
- foo + 5
201
+ param :value
202
+ component :calculator1, StaticCalculator
203
+ component :calculator2, Multiplier
204
+
205
+ def formula
206
+ value + calculator1 + calculator2
202
207
  end
203
208
  end
209
+
210
+ context = Prezzo::Context.new(value: 3, arg1: 2, arg2: 10.0)
211
+ ExplainableCalculator.new(context).explain
212
+ #=> { total: 33.0, context: { value: 3 }, components: { calculator1: { ... }, calculator2: { ... } } }
204
213
  ```
205
214
 
206
- `QuxCalculator#explain` now produces
215
+ ### Transient values
216
+
217
+ Intermediate calculation values that you would like to appear on the
218
+ explanation can be defined with the `transient` dsl:
207
219
 
208
220
  ```ruby
209
- {
210
- total: 35,
211
- components: {
212
- foo: 30
213
- }
214
- }
215
- ```
221
+ require "prezzo"
216
222
 
217
- but not
223
+ class TransientCalculator
224
+ include Prezzo::Calculator
218
225
 
219
- ```ruby
220
- {
221
- total: 35,
222
- components: {
223
- foo: {
224
- total: 30,
225
- components: {
226
- bar: 10,
227
- baz: 20
228
- }
229
- }
230
- }
231
- }
232
- ```
226
+ param :arg1
227
+ param :arg2
228
+ param :arg3
229
+
230
+ transient :intermediate do
231
+ arg1 + arg2
232
+ end
233
233
 
234
- Check the full [Uber pricing](/spec/integration/uber_pricing_spec.rb) for more complete example with many calculators and factors.
234
+ def formula
235
+ arg3 * intermediate
236
+ end
237
+ end
238
+
239
+ context = Prezzo::Context.new(arg1: 1, arg2: 2, arg3: 3)
240
+ TransientCalculator.new(context).explain
241
+ #=> { total: 9, context: { arg1: 1, arg2: 2, arg3: 3 }, transients: { intermediate: 3 } }
242
+ ```
235
243
 
236
244
  ## Development
237
245
 
@@ -2,7 +2,9 @@ require "prezzo/version"
2
2
 
3
3
  module Prezzo
4
4
  autoload :Calculator, "prezzo/calculator"
5
- autoload :Composable, "prezzo/composable"
5
+ autoload :ParamsDSL, "prezzo/params_dsl"
6
+ autoload :ComponentsDSL, "prezzo/components_dsl"
7
+ autoload :TransientDSL, "prezzo/transient_dsl"
6
8
  autoload :Context, "prezzo/context"
7
9
  autoload :Explainable, "prezzo/explainable"
8
10
  end
@@ -1,22 +1,28 @@
1
1
  module Prezzo
2
2
  module Calculator
3
+ def self.included(base)
4
+ base.class_eval do
5
+ base.include(ParamsDSL)
6
+ base.include(ComponentsDSL)
7
+ base.include(TransientDSL)
8
+ base.include(Explainable)
9
+ end
10
+ end
11
+
3
12
  def initialize(context = {})
4
- @context = validated!(context)
13
+ @context = context
5
14
  end
6
15
 
7
16
  def calculate
8
- raise "Calculate not implemented"
17
+ @total ||= formula
18
+ end
19
+
20
+ def formula
21
+ raise "Formula not implemented"
9
22
  end
10
23
 
11
24
  private
12
25
 
13
26
  attr_reader :context
14
-
15
- def validated!(context)
16
- raise "Empty Context" if context.nil?
17
- raise "Invalid Context" if context.respond_to?(:valid?) && !context.valid?
18
-
19
- context
20
- end
21
27
  end
22
28
  end
@@ -0,0 +1,46 @@
1
+ module Prezzo
2
+ module ComponentsDSL
3
+ def self.included(base)
4
+ base.class_eval do
5
+ base.extend(ClassMethods)
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def component(name, klass, sub_context_name = nil)
11
+ components << name
12
+
13
+ define_method(name) do
14
+ cached_components[name] ||=
15
+ begin
16
+ c = sub_context_name ? context.fetch(sub_context_name) : context
17
+
18
+ instance = klass.new(c)
19
+ instance.sub_context(sub_context_name) if sub_context_name
20
+ instance
21
+ end
22
+
23
+ cached_components[name].calculate
24
+ end
25
+ end
26
+
27
+ def components
28
+ @components ||= []
29
+ end
30
+ end
31
+
32
+ def compile_components
33
+ self.class.components.reduce({}) do |acc, name|
34
+ public_send(name) # force component cache
35
+ acc[name] = cached_components[name].explain
36
+ acc
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def cached_components
43
+ @cached_components ||= {}
44
+ end
45
+ end
46
+ end
@@ -1,37 +1,23 @@
1
- require "hanami-validations"
2
-
3
1
  module Prezzo
4
- module Context
5
- def self.included(base)
6
- base.class_eval do
7
- base.include(Hanami::Validations)
8
- end
9
- end
10
-
11
- def valid?
12
- validation.success?
13
- end
2
+ class Context
3
+ include ParamsDSL
14
4
 
15
- def errors
16
- validation.errors
5
+ def initialize(attributes)
6
+ @attributes = attributes
17
7
  end
18
8
 
19
9
  def fetch(key, default = nil)
20
- if default.nil?
21
- attributes.fetch(key)
10
+ value = @attributes.fetch(key, default)
11
+
12
+ if value.is_a?(Hash)
13
+ Class.new(Context).new(value)
22
14
  else
23
- attributes.fetch(key, default) || default
15
+ value
24
16
  end
25
17
  end
26
18
 
27
- def attributes
28
- validation.output
29
- end
30
-
31
- private
32
-
33
- def validation
34
- @_validation ||= validate
19
+ def context
20
+ self
35
21
  end
36
22
  end
37
23
  end
@@ -1,39 +1,18 @@
1
1
  module Prezzo
2
2
  module Explainable
3
- def self.included(base)
4
- base.class_eval do
5
- base.extend(ClassMethods)
6
- end
7
- end
8
-
9
- module ClassMethods
10
- def explain_with(*component_names, **options)
11
- @explained_components ||= {}
12
- component_names.each do |component_name|
13
- @explained_components[component_name] = options
14
- end
15
- end
16
-
17
- attr_reader :explained_components
18
- end
19
-
20
3
  def explain
21
- explained_components = self.class.explained_components || {}
22
-
23
4
  explanation = {
24
5
  total: calculate,
25
- components: {},
26
6
  }
27
7
 
28
- explained_components.each do |component, options|
29
- value = send(component)
30
- if self.class.respond_to?(:components) && self.class.components.include?(component)
31
- value = cached_components[component]
32
- end
33
- value = value.explain if value.respond_to?(:explain) && options.fetch(:recursive, true)
34
- value = value.calculate if value.respond_to?(:calculate)
35
- explanation[:components][component] = value
36
- end
8
+ components = compile_components
9
+ explanation[:components] = components unless components.empty?
10
+
11
+ context = compile_params
12
+ explanation[:context] = context unless context.empty?
13
+
14
+ transients = compile_transients
15
+ explanation[:transients] = transients unless transients.empty?
37
16
 
38
17
  explanation
39
18
  end
@@ -0,0 +1,55 @@
1
+ module Prezzo
2
+ module ParamsDSL
3
+ def self.included(base)
4
+ base.class_eval do
5
+ base.extend(ClassMethods)
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def param(name, options = {}, &block)
11
+ params << name
12
+
13
+ define_method(name) do
14
+ cached_params[name] ||=
15
+ begin
16
+ value = context.fetch(name, options[:default])
17
+ value.class.class_eval(&block) if block
18
+ value
19
+ end
20
+ end
21
+ end
22
+
23
+ def params
24
+ @params ||= []
25
+ end
26
+ end
27
+
28
+ def compile_params
29
+ params = self.class.params.reduce({}) do |acc, name|
30
+ value = public_send(name)
31
+ value = value.compile_params if value.respond_to?(:compile_params)
32
+ acc[name] = value
33
+ acc
34
+ end
35
+
36
+ if @sub_context_name
37
+ {
38
+ @sub_context_name => params,
39
+ }
40
+ else
41
+ params
42
+ end
43
+ end
44
+
45
+ def sub_context(name)
46
+ @sub_context_name = name
47
+ end
48
+
49
+ private
50
+
51
+ def cached_params
52
+ @cached_params ||= {}
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ module Prezzo
2
+ module TransientDSL
3
+ def self.included(base)
4
+ base.class_eval do
5
+ base.extend(ClassMethods)
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def transient(name)
11
+ transients << name
12
+
13
+ define_method(name) do
14
+ cached_transients[name] ||= yield
15
+ end
16
+ end
17
+
18
+ def transients
19
+ @transients ||= []
20
+ end
21
+ end
22
+
23
+ def compile_transients
24
+ self.class.transients.reduce({}) do |acc, name|
25
+ public_send(name) # force transient cache
26
+ acc[name] = cached_transients[name]
27
+ acc
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def cached_transients
34
+ @cached_transients ||= {}
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Prezzo
2
- VERSION = "0.5.1".freeze
2
+ VERSION = "1.0.0-rc".freeze
3
3
  end
@@ -17,7 +17,6 @@ Gem::Specification.new do |spec|
17
17
  end
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "hanami-validations", "~> 1.0"
21
20
  spec.add_development_dependency "bundler", "~> 1.13"
22
21
  spec.add_development_dependency "rake", "~> 10.0"
23
22
  spec.add_development_dependency "rspec", "~> 3.0"
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prezzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 1.0.0.pre.rc
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Boeira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-17 00:00:00.000000000 Z
11
+ date: 2017-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: hanami-validations
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -173,9 +159,11 @@ files:
173
159
  - bin/setup
174
160
  - lib/prezzo.rb
175
161
  - lib/prezzo/calculator.rb
176
- - lib/prezzo/composable.rb
162
+ - lib/prezzo/components_dsl.rb
177
163
  - lib/prezzo/context.rb
178
164
  - lib/prezzo/explainable.rb
165
+ - lib/prezzo/params_dsl.rb
166
+ - lib/prezzo/transient_dsl.rb
179
167
  - lib/prezzo/version.rb
180
168
  - prezzo.gemspec
181
169
  homepage: http://github.com/marceloboeira/prezzo
@@ -192,12 +180,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
180
  version: '0'
193
181
  required_rubygems_version: !ruby/object:Gem::Requirement
194
182
  requirements:
195
- - - ">="
183
+ - - ">"
196
184
  - !ruby/object:Gem::Version
197
- version: '0'
185
+ version: 1.3.1
198
186
  requirements: []
199
187
  rubyforge_project:
200
- rubygems_version: 2.6.11
188
+ rubygems_version: 2.6.8
201
189
  signing_key:
202
190
  specification_version: 4
203
191
  summary: Toolbox to create complex pricing models
@@ -1,33 +0,0 @@
1
- module Prezzo
2
- module Composable
3
- def self.included(base)
4
- base.class_eval do
5
- base.extend(ClassMethods)
6
- end
7
- end
8
-
9
- module ClassMethods
10
- def composed_by(options)
11
- options.each do |name, klass|
12
- components << name
13
-
14
- define_method(name) do
15
- cached_components[name] ||= klass.new(context)
16
-
17
- cached_components[name].calculate
18
- end
19
- end
20
- end
21
-
22
- def components
23
- @components ||= []
24
- end
25
- end
26
-
27
- private
28
-
29
- def cached_components
30
- @cached_components ||= {}
31
- end
32
- end
33
- end