README.md in prezzo-0.5.1 vs README.md in prezzo-1.0.0.pre.rc

- old
+ new

@@ -21,218 +21,226 @@ $ gem install prezzo ``` ## Usage -### Prezzo::Context +### The calculation context +`Prezzo::Context` is a source of data for your calculators. It receives a hash +of params and makes them available to calculators. -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. - e.g.: ```ruby -module Uber - class Context - include Prezzo::Context - CATEGORIES = ["UberX", "UberXL", "UberBlack"].freeze +context = Prezzo::Context.new(category: "Expensive", distance: 10.0) +``` - validations do - required(:category).filled(included_in?: CATEGORIES) - required(:distance).filled(:float?) - required(:total_cars).filled(:int?) - required(:available_cars).filled(:int?) - end - end -end +### Simple calculators -context = Uber::Context.new(category: "UberBlack", ...) +`Prezzo::Calculator` is the top level concept to describe calculations. Define +a `formula` method that describe the calculation you want to perform and call +the `calculate` method. -# when valid -context.valid? -#=> true +e.g.: -# when invalid -context.valid? -#=> false +```ruby +require "prezzo" -context.errors -# { distance: ["must be a float"]} +class StaticCalculator + include Prezzo::Calculator + + def formula + 2 * 5 + end +end + +StaticCalculator.new.calculate +#=> 10 ``` -### Prezzo::Calculator +### Accessing context params -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. +Use the `param` dsl to create methods that read data from the context. e.g.: ```ruby require "prezzo" -module Uber - class PricePerDistanceCalculator - include Prezzo::Calculator +class Multiplier + include Prezzo::Calculator - def calculate - price_per_kilometer * distance - end + param :arg1 + param :arg2 - def price_per_kilometer - 1.30 - end - - def distance - context.fetch(:distance) - end + def formula + arg1 * arg2 end end -context = Uber::Context.new(distance: 10.0) -Uber::PricePerDistanceCalculator.new(context).calculate +context = Prezzo::Context.new(arg1: 2, arg2: 10.0) +Multiplier.new(context).calculate #=> 20.0 ``` -**Context Validation** +### Default params -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. +The `param` dsl accepts a default value: -### Prezzo::Composable - -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. - e.g.: ```ruby require "prezzo" -module Uber - class RidePriceCalculator - include Prezzo::Calculator - include Prezzo::Composable +class OptionalCalculator + include Prezzo::Calculator - composed_by base_fare: BaseFareCalculator, - price_per_distance: PricePerDistanceCalculator, + param :optional, default: 10.0 - def calculate - base_fare + price_per_distance - end + def formula + optional * 3 end end -context = Uber::Context.new(distance: 10.0) -Uber::RidePriceCalculator.new(context).calculate -#=> 47.3 +context = Prezzo::Context.new(arg1: 2, arg2: 10.0) +OptionalCalculator.new(context).calculate +#=> 30.0 ``` -### Prezzo::Explainable +### Nested context params -The `Prezzo::Explainable` module is an abstraction that provides a nice way of representing how the price was composed. +The `param` dsl can take a block to access nested data in the context. e.g.: ```ruby require "prezzo" -module Uber - class RidePriceCalculator - include Prezzo::Calculator - include Prezzo::Composable - include Prezzo::Explainable +class NestedCalculator + include Prezzo::Calculator - composed_by base_fare: BaseFareCalculator, - price_per_distance: PricePerDistanceCalculator, - explain_with :base_fare, :price_per_distance + param :level1 do + param :level2 + end - def calculate - base_fare + price_per_distance - end + def formula + level1.level2 * 2 end end -context = Uber::Context.new(distance: 10.0) -Uber::RidePriceCalculator.new(context).explain -#=> { total: 25.6, components: { base_fare: 4.3, price_per_distance: 21.3 } } +context = Prezzo::Context.new(level1: { level2: 10.0 }) +NestedCalculator.new(context).calculate +#=> 20.0 ``` -#### Multiline `explain_with` +### Composing calculators -`explain_with` can be splitted into several lines. +Calculators provide the `component` dsl method to make it easy to compose +calculators. Each calculator will be defined as a method in the calculator. +They will receive the whole context on instantiation. +e.g.: + ```ruby -class RidePriceCalculator - include Prezzo::Explainable +require "prezzo" - explain_with :base_fare - explain_with :price_per_distance +class ComposedCalculator + include Prezzo::Calculator + + component :calculator1, StaticCalculator + component :calculator2, Multiplier + + def formula + calculator1 + calculator2 + end end + +context = Prezzo::Context.new(arg1: 2, arg2: 10.0) +ComposedCalculator.new(context).calculate +#=> 30.0 ``` -#### ` explain_with` with the `recursive: false` option +### Restricting the context +You can restrict the context of each calculator by providing a third argument +to `components`: + +e.g.: + ```ruby -class FooCalculator +require "prezzo" + +class RestrictedCalculator include Prezzo::Calculator - include Prezzo::Explainable - explain_with :bar, :baz + component :calculator1, Multiplier, :side1 + component :calculator2, Multiplier, :side2 - def calculate - bar + baz + def formula + calculator1 + calculator2 end +end - def bar - 10 - end +context = Prezzo::Context.new(side1: { arg1: 2, arg2: 10.0 }, side2: { arg1: 3, arg2: 20.0 }) +RestrictedCalculator.new(context).calculate +#=> 80.0 +``` - def baz - 20 - end -end +### Explanations -class QuxCalculator - include Prezzo::Calculator - include Prezzo::Composable - include Prezzo::Explainable +The `explain` method provides a nice way of representing how the price was +composed. It will include all params and components defined in the calculator. - composed_by foo: FooCalculator +e.g.: - explain_with :foo, recursive: false +```ruby +require "prezzo" - def calculate - foo + 5 +class ExplainableCalculator + include Prezzo::Calculator + + param :value + component :calculator1, StaticCalculator + component :calculator2, Multiplier + + def formula + value + calculator1 + calculator2 end end + +context = Prezzo::Context.new(value: 3, arg1: 2, arg2: 10.0) +ExplainableCalculator.new(context).explain +#=> { total: 33.0, context: { value: 3 }, components: { calculator1: { ... }, calculator2: { ... } } } ``` -`QuxCalculator#explain` now produces +### Transient values +Intermediate calculation values that you would like to appear on the +explanation can be defined with the `transient` dsl: + ```ruby -{ - total: 35, - components: { - foo: 30 - } -} -``` +require "prezzo" -but not +class TransientCalculator + include Prezzo::Calculator -```ruby -{ - total: 35, - components: { - foo: { - total: 30, - components: { - bar: 10, - baz: 20 - } - } - } -} -``` + param :arg1 + param :arg2 + param :arg3 -Check the full [Uber pricing](/spec/integration/uber_pricing_spec.rb) for more complete example with many calculators and factors. + transient :intermediate do + arg1 + arg2 + end + + def formula + arg3 * intermediate + end +end + +context = Prezzo::Context.new(arg1: 1, arg2: 2, arg3: 3) +TransientCalculator.new(context).explain +#=> { total: 9, context: { arg1: 1, arg2: 2, arg3: 3 }, transients: { intermediate: 3 } } +``` ## Development After checking out the repo, run `make` to install dependencies. Then, run `make spec` to run the tests. You can also run `make console` for an interactive prompt that will allow you to experiment.