README.rdoc in volute-0.1.0 vs README.rdoc in volute-0.1.1

- old
+ new

@@ -1,140 +1,397 @@ = volute -It could be a 'set event bus', or a 'business logic relocator'. +I wanted to write something about the state of multiple objects, I ended with something that feels like a subset of aspect oriented programming. It can be used to implement toy state machines, or dumb rule systems. -See examples/ and specs/ +== include Volute -== usage +When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attributer set method, upon setting the value of the attribute triggers a callback defined outside of the class. - gem install volute + require 'rubygems' + require 'volute' # gem install volute + class Light + include Volute -== example : equation + attr_accessor :colour + attr_accessor :changed_at + end - # license is MIT + volute :colour do + object.changed_at = Time.now + end - $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + l = Light.new + p l # => #<Light:0x10014c480> - require 'volute' + l.colour = :blue + p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue> - # - # our class +There is a catch in this example, the volute will trigger for any class that inccludes Volute and which sees a change to its :colour attribute. - class Equation +Those two classes would see their :colour hooked : + + class Light include Volute - attr_accessor :km, :h, :kph + attr_accessor :colour + attr_accessor :changed_at + end - def initialize - @km = 1.0 - @h = 1.0 - @kph = 1.0 + class Flower + include Volute + + attr_accessor :colour + end + +To make sure that only instance of Light will be concerned, one could write : + + volute Light do + volute :colour do + object.changed_at = Time.now end + end - def inspect +Inside of a volute, these are the available 'variables' : - "#{@km} km, #{@h} h, #{@kph} kph" +* object - the instance whose attribute has been set +* attribute - the attribute name whose value has been set +* previous_value - the previous value for the attribute +* value - the new value + +thus : + + volute Light do + volute :colour do + puts "#{object.class}.#{attribute} : #{previous_value.inspect} --> #{value.inspect}" end end - # - # a volute triggered for any 'set' operation on an attribute of book + l = Light.new + l.colour = :blue + l.colour = :red - volute Equation do +would output : - # object.vset(:state, x) - # is equivalent to - # object.instance_variable_set(:@state, x) + Light.colour : nil --> :blue + Light.colour : :blue --> :red - volute :km do - object.vset(:h, value / object.kph) + +== filters / guards + +A volute combines a list of arguments with a block of ruby code + + volute do + puts 'some attribute was set' + end + + volute Light do + puts 'some attribute of an instance of class Light was set' + end + + volute Light, Flower do + puts 'some attribute of an instance of class Light or Flower was set' + end + + volute :count do + puts 'the attribute :count of some instance got set' + end + + volute :count, :number do + puts 'the attribute :count or :number of some instance got set' + end + + volute Light, :count do + puts 'some attribute of an instance of class Light was set' + puts 'OR' + puts 'the attribute :count of some instance got set' + end + +As soon as 1 argument matches, the Ruby block of the volute is executed. In other words, arg0 OR arg1 OR ... OR argN + +If you need for an AND, read on to "nesting volutes". + +Filtering on attributes who match a regular expression : + + class Invoice + include Volute + + attr_accessor :amount + attr_accessor :customer_name, :customer_id + end + + volute /^customer_/ do + puts "attribute :customer_name or :customer_id got modified" + end + + +== 'transition volutes' + +It's possible to filter based on the previous value and the new value (with :any as a wildcard) : + + volute 0 => 100 do + puts "some attribute went from 0 to 100" + end + + volute :any => 100 do + puts "some attribute was just set to 100" + end + + volute 0 => :any do + puts "some attribute was at 0 and just got changed" + end + +Multiple start and end values may be specified : + + volute [ 'FRA', 'ZRH' ] => :any do + puts "left FRA or ZRH" + end + + volute 'GVA' => [ 'SHA', 'NRT' ] do + puts "reached SHA or NRT from GVA" + end + +Regular expressions are OK : + + volute /^S..$/ => /^F..$/ do + puts "left S.. and reached F.." + end + + +== volute :not + +A volute may have :not has a first argument + + volute :not, Invoice do + puts "some instance that is not an invoice..." + end + + volute :not, :any => :delivered do + puts "a transition to something different than :delivered..." + end + + volute :not, Invoice, :paid do + puts "not an Invoice and not a variation of the :paid attribute..." + end + +Not Bob or Charlie, Nor Bob and neither Charlie. + + +== nesting volutes + +Whereas enumerating arguments for a single volute played like an OR, to achieve AND, one can nest volutes. + + volute Invoice do + volute :paid do + puts "the :paid attribute of an Invoice just changed" end - volute :h do - object.vset(:kph, object.km / value) + end + + volute Grant do + volute :paid do + puts "the :paid attribute of a Grant just changed" end - volute :kph do - object.vset(:h, object.km / value) + end + + +== 'guarding' inside of the volute block + +As long as one doesn't use a 'return' inside of a block, they're just ruby code... + + volute Patient do + if object.sore_throat == true && object.fever == true + puts "needs further investigation" + elsif object.fever == true + puts "only a small fever" end end - # - # trying - e = Equation.new - p e # => 1.0 km, 1.0 h, 1.0 kph +== 'state volutes' - e.kph = 10.0 - p e # => 1.0 km, 0.1 h, 10.0 kph +"I want this volute to trigger when the patient has a sore_throat and the flu", would translate to - e.km = 5.0 - p e # => 5.0 km, 0.5 h, 10.0 kph + volute Patient do + volute :sore_throat do + if value == true && object.fever == true + puts "it triggers" + end + end + volute :fever do + if value == true && object.sore_throat == true + puts "it triggers" + end + end + end +hairy isn't it ? There is a simpler way, it constitutes an exception to the "volute arguments join in an OR", but it reads well (I hope) : -== example : some kind of state-machine + volute Patient do + volute :sore_throat => true, :fever => true do + puts "it triggers" + end + end - # license is MIT - # - # a state-machine-ish example, inspired by the example at - # http://github.com/qoobaa/transitions +:not applies as well : - $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) + volute Patient do + volute :not, :leg_broken do + send_patient_back_home # we only treat broken legs + end + end - require 'volute' +Our sore_throat and flu example could be rewritten with Ruby ifs as : - # - # our class + volute Patient do + if object.sore_throat == true && object.flu == true + puts "it triggers" + end + end - class Book - include Volute +which isn't hairy at all. - attr_accessor :stock - attr_accessor :discontinued +Pointing to multiple values is OK : - attr_reader :state + volute Package do + volute :delivered => true, :weight => [ '1kg', '2kg' ] do + puts "delivered a package of 1 or 2 kg" + end + end - def initialize (stock) - @stock = stock - @discontinued = false - @state = :in_stock +Not mentioning an attribute implies its value doesn't matter when matching state, :any and :not_nil could prove useful though : + + volute Package do + volute :delivered => :not_nil do + puts "package entered delivery circuit" end end - # - # a volute triggered for any 'set' operation on an attribute of book + volute Item do + volute :weight => :any, :package => true do + # dropping ":weight => :any" would make sense, but sometimes, when + # tweaking volutes, a quick editing of :any to another value is + # almost effortless + end + end - volute Book do +For attributes whose values are strings, regular expressions may prove useful : - # object.volute_do_set(:state, x) - # is equivalent to - # object.instance_variable_set(:@state, x) + volute :location => /^Fort .+/ do + puts "Somewhere in Fort ..." + end - if object.stock <= 0 - object.volute_do_set( - :state, object.discontinued ? :discontinued : :out_of_stock) +There is an example that uses those 'state volutes' at http://github.com/jmettraux/volute/blob/master/examples/diagnosis.rb + + +== 'over' + +Each volute that matches sees its block called. In order to prevent further evaluations, the 'over' method can be called. + + volute Package do + + volute do + over if object.delivered + # prevent further volute evaluation if the package was delivered + end + + volute :location do + (object.comment ||= []) << value + end + end + + +== application of the volutes on demand + +Up until now, this readme focused on the scenario where volute application is triggered by a change in the state of an attribute (in a class that includes Volute). + +It is entirely OK to have classes that do not include Volute but are the object of a volute application : + + class Engine + attr_accessor :state + def turn_key! + @key_turned = true + Volute.apply(self, :key_turned) + end + def press_red_button! + Volute.apply(self) + end + end + + volute Engine do + if attribute == :key_turned + object.state = :running else - object.volute_do_set( - :state, :in_stock) + object.state = :off end end - # - # trying +The key here is the call to - emma = Book.new(10) + Volute.apply(object, attribute=nil, previous_value=nil, value=nil) - emma.stock = 2 - p emma.state # => :in_stock +In fact, for classes that include Volute, this method is called for each attribute getting set. - emma.stock = 0 - p emma.state # => :out_of_stock +This technique is also a key when building system where the volutes aren't called all the time but only right before their result should matter ('decision' versus 'reaction'). - emma.discontinued = true - p emma.state # => :discontinued + +== volute blocks, closures + +TODO + + +== volute management + +TODO + + +== examples + +http://github.com/jmettraux/volute/tree/master/examples/ + + +== alternatives + +states + +- there is a list of ruby state machines at the end of http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/ + +rules + +- http://github.com/codeaspects/ruleby +- http://rools.rubyforge.org/ +- ... + +aspects + +- http://github.com/gcao/aspect4r +- http://github.com/teejayvanslyke/gazer +- http://github.com/nakajima/aspectory +- http://github.com/matthewrudy/aspicious +- http://aquarium.rubyforge.org/ + +- http://github.com/search?type=Repositories&language=ruby&q=aspect&repo=&langOverride=&x=15&y=25&start_value=1 (github search) + +hooks and callbacks + +- http://github.com/apotonick/hooks +- http://github.com/avdi/hookr +- http://github.com/auser/backcall +- ... + + +== author + +John Mettraux - http://github.com/jmettraux/ + + +== feedback + +* IRC freenode #ruote +* jmettraux@gmail.com == license MIT, see LICENSE.txt