Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
set event bus, on set callbacks, business logic relocator, whatever (ruby)
Ruby
branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
examples
lib
spec
CHANGELOG.txt
LICENSE.txt
README.rdoc
Rakefile
TODO.txt
volute.gemspec

README.rdoc

volute

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.

include Volute

When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attribute set method, upon setting the value of the attribute triggers a callback defined outside of the class.

require 'rubygems'
require 'volute' # gem install volute

class Light
  include Volute

  attr_accessor :colour
  attr_accessor :changed_at
end

volute :colour do
  object.changed_at = Time.now
end

l = Light.new
p l # => #<Light:0x10014c480>

l.colour = :blue
p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue>

There is a catch in this example, the volute will trigger for any class that includes Volute and which sees a change to its :colour attribute.

Those two classes would see their :colour hooked :

class Light
  include Volute

  attr_accessor :colour
  attr_accessor :changed_at
end

class Flower
  include Volute

  attr_accessor :colour
end

To make sure that only instances of Light will be affected, one could write :

volute Light do
  volute :colour do
    object.changed_at = Time.now
  end
end

Inside of a volute, these are the available 'variables' :

  • 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

l = Light.new
l.colour = :blue
l.colour = :red

would output :

Light.colour : nil --> :blue
Light.colour : :blue --> :red

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
end

volute Grant do
  volute :paid do
    puts "the :paid attribute of a Grant just changed"
  end
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

'state volutes'

“I want this volute to trigger when the patient has a sore_throat and the flu”, would translate to

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) :

volute Patient do
  volute :sore_throat => true, :fever => true do
    puts "it triggers"
  end
end

:not applies as well :

volute Patient do
  volute :not, :leg_broken do
    send_patient_back_home # we only treat broken legs
  end
end

Note that our sore_throat and flu example could be rewritten with Ruby ifs as :

volute Patient do
  if object.sore_throat == true && object.flu == true
    puts "it triggers"
  end
end

which isn't hairy at all.

Pointing to multiple values is OK :

volute Package do
  volute :delivered => true, :weight => [ '1kg', '2kg' ] do
    puts "delivered a package of 1 or 2 kg"
  end
end

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

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

For attributes whose values are strings, regular expressions may prove useful :

volute :location => /^Fort .+/ do
  puts "Somewhere in Fort ..."
end

There is an example that uses those 'state volutes' at 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.state = :off
  end
end

The key here is the call to

Volute.apply(object, attribute=nil, previous_value=nil, value=nil)

In fact, for classes that include Volute, this method is called for each attribute getting set.

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').

volute blocks, closures

TODO

volute management

TODO

examples

github.com/jmettraux/volute/tree/master/examples/

alternatives

states

rules

aspects

hooks and callbacks

author

John Mettraux - github.com/jmettraux/

feedback

  • IRC freenode #ruote

  • jmettraux@gmail.com

license

MIT, see LICENSE.txt

Something went wrong with that request. Please try again.