Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Adding files

  • Loading branch information...
commit 63c2730d343b06cad1322040205bd22ddb32e96e 0 parents
@avdi authored
4 History.txt
@@ -0,0 +1,4 @@
+== 0.0.1 2008-11-05
+
+* 1 major enhancement:
+ * Initial release
18 Manifest.txt
@@ -0,0 +1,18 @@
+History.txt
+Manifest.txt
+PostInstall.txt
+README.rdoc
+Rakefile
+TODO
+assertions.rb
+assertions_spec.rb
+lib/alter_ego.rb
+script/console
+script/destroy
+script/generate
+spec/spec.opts
+spec/spec_helper.rb
+spec/alter_ego_spec.rb
+alter_ego.rb
+alter_ego_spec.rb
+tasks/rspec.rake
7 PostInstall.txt
@@ -0,0 +1,7 @@
+
+For more information on states, see http://alter-ego.rubyforge.org
+
+NOTE: Change this information in PostInstall.txt
+You can also delete it if you don't want it.
+
+
149 README.rdoc
@@ -0,0 +1,149 @@
+= states
+
+* FIX (url)
+
+== DESCRIPTION:
+
+AlterEgo is a Ruby implementation of the State pattern as described by the Gang
+of Four. It differs from other Ruby state machine libraries in that it focuses
+on providing polymorphic behavior based on object state. In effect, it makes it
+easy to give an object different personalities depending on the state it is in.
+
+== SYNOPSIS:
+
+ class TrafficLight
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle!
+ end
+
+ state :caution do
+ handle :color do
+ "yellow"
+ end
+ transition :to => :stop, :on => :cycle!
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle!
+ end
+ end
+
+ light = TrafficLight.new
+ light.color # => "green"
+ light.cycle!
+ light.color # => "yellow"
+ light.cycle!
+ light.color # => "red"
+ light.cycle!
+ light.color # => "green"
+
+
+== FEATURES:
+
+* Implemented as a module which can be included in any Ruby class.
+* Fully tested with literate RSpec
+* Guard clauses may be defined for each transition.
+* Enter/exit actions may be defined for each state.
+* For more advanced scenarios, arbitrary "request filters" may be
+ defined with full control over which requests are filtered.
+* Uses dynamic module generation and delegation instead of method
+ rewriting.
+* Pervasive contract-checking catches mistakes in library usage
+ early.
+* Storing and reading current state is completely customizable,
+ making it easier to add AlterEgo to legacy classes.
+
+== DETAILS:
+
+AlterEgo differs from other Finite State Machine implementations in
+Ruby in that where other libraries focus on describing a set of
+valid state transitions, AlterEgo focuses on varying _behavior_ based
+on state. In other words, it provides state-based polymorphism.
+
+AlterEgo draws heavily on the State Pattern as published in the book
+Design Patterns[1]. A summary of the pattern can be
+found on Wikipedia[http://en.wikipedia.org/wiki/State_pattern].
+Because AlterEgo uses the terminology set forth in the State Pattern,
+it is useful to have some familiarity with the pattern in order to
+understand the library.
+
+link://State_Design_Pattern_UML_Class_Diagram.png
+
+In the State Pattern, states of an object are represented as
+discrete objects. At any given time an object with state-based
+behavior has-a state object. The object with state-based behavior
+delegates certain method calls to its current state. In this way,
+the implementation of those methods can vary with the state of the
+object. Or in certain states some methods may not be supported at
+all.
+
+The AlterEgo library provides both an object model for manually
+setting up explicit state classes, and a concise DSL built on top
+of that model which simplifies building classes with state-based
+behavior.
+
+This file only scratches the surface of AlterEgo
+functionality. For complete tutorial documentation, see the file
+spec/states_spec.rb. It contains the annotated specification,
+written in the style of a step-by-step tutorial.
+
+=== Terminology:
+
+[Context] The *context* is the class which should have state-based
+ behavior. In the example above, the +TrafficLight+ class
+ is the context.
+[State] Each *state* the *context* might exist in is represented by a
+ class, In the example given above, the available states
+ are +caution+, +stop+, and +proceed+.
+[Request] A *request* refers to a message (method) sent to the
+ *context*. In the example given above, the supported
+ requests are +color+ and +cycle+.
+[Handler] A *handler* is a method on a *state* which implements a
+ *request* for that state. For instance, in the example
+ above, when the +TrafficLight+ is in the +caution+ state,
+ the handler for the request +color+ returns "yellow".
+
+=== Footnotes:
+
+1. Gamma, Erich; Richard Helm, Ralph Johnson, John M. Vlissides (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 395. ISBN 0201633612.
+
+== REQUIREMENTS:
+
+* ActiveSupport
+
+== INSTALL:
+
+* sudo gem install states
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2008 Avdi Grimm
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 Rakefile
@@ -0,0 +1,32 @@
+%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
+require File.dirname(__FILE__) + '/lib/alter_ego'
+
+# Generate all the Rake tasks
+# Run 'rake -T' to see list of generated tasks (from gem root directory)
+$hoe = Hoe.new('alter-ego', AlterEgo::VERSION) do |p|
+ p.developer('Avdi Grimm', 'avdi@avdi.org')
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
+ p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
+ p.rubyforge_name = p.name # TODO this is default value
+ # p.extra_deps = [
+ # ['activesupport','>= 2.0.2'],
+ # ]
+ p.extra_dev_deps = [
+ ['newgem', ">= #{::Newgem::VERSION}"]
+ ]
+
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
+ p.rsync_args = '-av --delete --ignore-errors'
+end
+
+require 'newgem/tasks' # load /tasks/*.rake
+Dir['tasks/**/*.rake'].each { |t| load t }
+
+# TODO - want other tests/tasks run by default? Add them to the list
+# task :default => [:spec, :features]
+
+task :docs do |task|
+ cp "State_Design_Pattern_UML_Class_Diagram.png", "doc"
+end
BIN  State_Design_Pattern_UML_Class_Diagram.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 TODO
@@ -0,0 +1,10 @@
+* TODO Factor assertions out into separate library
+* TODO Remove ActiveSupport dependency
+* TODO Nested state support with history
+* TODO Verify full methods/respond_to? integration
+* TODO Composite state support
+ E.g. a Traffic Light could have both green/yellow/red state and "in
+ service"/out of service" state at the same time
+* TODO More natural/Rubyish DSL
+ It would be good to have plain-old "def" work as expected inside of "state"
+ blocks instead of requiring "handle".
2  config/website.yml
@@ -0,0 +1,2 @@
+host: avdi@rubyforge.org
+remote_dir: /var/www/gforge-projects/alter-ego
381 lib/alter_ego.rb
@@ -0,0 +1,381 @@
+$:.unshift(File.dirname(__FILE__)) unless
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+
+require File.join(File.dirname(__FILE__), 'assertions')
+require 'forwardable'
+require 'singleton'
+require 'rubygems'
+require 'activesupport'
+
+module AlterEgo
+ VERSION = '1.0.0'
+
+ include Assertions
+
+ class StateError < RuntimeError
+ end
+ class InvalidDefinitionError < StateError
+ end
+ class InvalidTransitionError < StateError
+ end
+ class InvalidRequestError < StateError
+ end
+ class WrongStateError < StateError
+ end
+
+ RequestFilter = Struct.new("RequestFilter",
+ :state,
+ :request,
+ :new_state,
+ :action)
+ class RequestFilter
+ def ===(other)
+ result = (matches?(self.state, other.state) and
+ matches?(self.request, other.request) and
+ matches?(self.new_state, other.new_state))
+ result
+ end
+
+ def matches?(lhs, rhs)
+ if rhs.respond_to?(:include?)
+ rhs.include?(lhs)
+ else
+ rhs == lhs
+ end
+ end
+ end
+
+ class AnyMatcher
+ include Singleton
+
+ def ===(other)
+ self == other
+ end
+
+ def ==(other)
+ true
+ end
+ end
+
+ class NotNilMatcher
+ include Singleton
+
+ def ===(other)
+ self == other
+ end
+
+ def ==(other)
+ not other.nil?
+ end
+ end
+
+ class State
+ include Assertions
+ extend Assertions
+
+ def self.transition(options, &trans_action)
+ options.assert_valid_keys(:to, :on, :if)
+ assert_keys(options, :to)
+ guard = options[:if]
+ to_state = options[:to]
+ request = options[:on]
+ if request
+ handle(request) do
+ transition_to(to_state, request)
+ end
+ end
+ valid_transitions << to_state unless valid_transitions.include?(to_state)
+ if guard
+ method = guard.kind_of?(Symbol) ? guard : nil
+ block = guard.kind_of?(Proc) ? guard : nil
+ predicate = AlterEgo.proc_from_symbol_or_block(method, &block)
+ guard_proc = proc do
+ result = instance_eval(&predicate)
+ throw :cancel unless result
+ end
+ add_request_filter(request, to_state, guard_proc)
+ end
+ if trans_action
+ add_request_filter(request, to_state, trans_action)
+ end
+ end
+
+ def self.identifier
+ self
+ end
+
+ def self.valid_transitions
+ (@valid_transitions ||= [])
+ end
+
+ def self.handled_requests
+ public_instance_methods(false)
+ end
+
+ def self.request_filters
+ (@request_filters ||= [])
+ end
+
+ def self.handle(request, method = nil, &block)
+ define_contextual_method_from_symbol_or_block(request, method, &block)
+ end
+
+ def self.on_enter(method = nil, &block)
+ assert(method.nil? ^ block.nil?)
+ define_contextual_method_from_symbol_or_block(:on_enter, method, &block)
+ end
+
+ def self.on_exit(method = nil, &block)
+ assert(method.nil? ^ block.nil?)
+ define_contextual_method_from_symbol_or_block(:on_exit, method, &block)
+ end
+
+ def valid_transitions
+ self.class.valid_transitions
+ end
+
+ def to_s
+ "<State:#{identifier}>"
+ end
+
+ def identifier
+ self.class.identifier
+ end
+
+ def ==(other)
+ (self.identifier == other) or super(other)
+ end
+
+ def can_handle_request?(request)
+ return true if respond_to?(request)
+ return false
+ end
+
+ def transition_to(context, request, new_state, *args)
+ return true if context.state == new_state
+ new_state_obj = context.state_for_identifier(new_state)
+ unless new_state_obj
+ raise(InvalidTransitionError,
+ "Context #{context.inspect} has no state '#{new_state}' defined")
+ end
+
+ continue = context.execute_request_filters(self.class.identifier,
+ request,
+ new_state)
+ return false unless continue
+
+ if (not valid_transitions.empty?) and (not valid_transitions.include?(new_state))
+ raise(InvalidTransitionError,
+ "Not allowed to transition from #{self.identifier} to #{new_state}")
+ end
+
+ on_exit(context)
+ new_state_obj.on_enter(context)
+ context.state=(new_state)
+ assert(new_state == context.state)
+ true
+ end
+
+ protected
+
+ def on_exit(context)
+ end
+
+ def on_enter(context)
+ end
+
+ private
+
+ def self.add_request_filter(request_pattern, new_state_pattern, action)
+ new_filter = RequestFilter.new(identifier,
+ request_pattern,
+ new_state_pattern,
+ action)
+ self.request_filters << new_filter
+ end
+
+ def self.define_contextual_method_from_symbol_or_block(name,
+ symbol,
+ &block)
+ if symbol
+ define_method(name) do |context, *args|
+ context.send(symbol, *args)
+ end
+ elsif block
+ define_method(name) do |context, *args|
+ context.send(:instance_eval, &block)
+ end
+ end
+ end
+end
+
+ module ClassMethods
+ def state(identifier, options={}, &block)
+ if states.has_key?(identifier)
+ raise InvalidDefinitionError, "State #{identifier.inspect} already defined"
+ end
+ new_state = Class.new(State)
+ new_state_eigenclass = class << new_state; self; end
+ new_state_eigenclass.send(:define_method, :identifier) { identifier }
+ new_state.instance_eval(&block) if block
+
+ add_state(new_state, identifier, options)
+ end
+
+ def request_filter(options, &block)
+ options.assert_valid_keys(:state, :request, :new_state, :action)
+ options = {
+ :state => not_nil,
+ :request => not_nil,
+ :new_state => nil
+ }.merge(options)
+ add_request_filter(options[:state],
+ options[:request],
+ options[:new_state],
+ AlterEgo.proc_from_symbol_or_block(options[:method], &block))
+ end
+
+ def all_handled_requests
+ methods = @state_proxy.public_instance_methods(false)
+ methods -= ["identifier", "on_enter", "on_exit"]
+ methods.map{|m| m.to_sym}
+ end
+
+ def states
+ (@states ||= {})
+ end
+
+ def states=(value)
+ @states = value
+ end
+
+ def add_state(new_state, identifier=new_state.identifier, options = {})
+ options.assert_valid_keys(:default)
+
+ self.states[identifier] = new_state.new
+
+ if options[:default]
+ if @default_state
+ raise InvalidDefinitionError, "Cannot have more than one default state"
+ end
+ @default_state = identifier
+ end
+
+ new_requests = (new_state.handled_requests - all_handled_requests)
+ new_requests.each do |request|
+ @state_proxy.send(:define_method, request) do |*args|
+ args.unshift(self)
+ begin
+ continue = execute_request_filters(current_state.identifier,
+ request,
+ nil)
+ return false unless continue
+ current_state.send(request, *args)
+ rescue NoMethodError => error
+ if error.name.to_s == request.to_s
+ raise WrongStateError,
+ "Request '#{request}' not supported by state #{current_state}"
+ else
+ raise
+ end
+ end
+ end
+ end
+
+ self.request_filters += new_state.request_filters
+ end
+
+ def add_request_filter(state_pattern, request_pattern, new_state_pattern, action)
+ @request_filters << RequestFilter.new(state_pattern,
+ request_pattern,
+ new_state_pattern,
+ action)
+ end
+
+ def default_state
+ @default_state
+ end
+
+ def request_filters
+ (@request_filters ||= [])
+ end
+
+ protected
+
+ def request_filters=(value)
+ @request_filters = value
+ end
+
+ def any
+ AlterEgo::AnyMatcher.instance
+ end
+
+ def not_nil
+ AlterEgo::NotNilMatcher.instance
+ end
+
+ end
+
+ def self.append_features(klass)
+ # Give the other module my instance methods at the class level
+ klass.extend(ClassMethods)
+ klass.extend(Forwardable)
+
+ state_proxy = Module.new
+ klass.instance_variable_set :@state_proxy, state_proxy
+ klass.send(:include, state_proxy)
+
+ super(klass)
+ end
+
+ def current_state
+ state_id = self.state
+ state_id ? self.class.states[state_id] : nil
+ end
+
+ def state
+ result = (@state || self.class.default_state)
+ assert(result.nil? || self.class.states.keys.include?(result))
+ result
+ end
+
+ def state=(identifier)
+ @state = identifier
+ end
+
+ def state_for_identifier(identifier)
+ self.class.states[identifier]
+ end
+
+ def transition_to(new_state, request=nil, *args)
+ current_state.transition_to(self, request, new_state, *args)
+ end
+
+ def all_handled_requests
+ self.class.all_handled_requests
+ end
+
+ def execute_request_filters(state, request, new_state)
+ pattern = RequestFilter.new(state, request, new_state)
+ self.class.request_filters.grep(pattern) do |filter|
+ result = catch(:cancel) do
+ self.instance_eval(&filter.action)
+ true
+ end
+ return false unless result
+ end
+ true
+ end
+
+ def self.proc_from_symbol_or_block(symbol = nil, &block)
+ if symbol then
+ proc do
+ self.send(symbol)
+ end
+ elsif block then
+ block
+ else raise "Should never get here"
+ end
+ end
+
+end
63 lib/assertions.rb
@@ -0,0 +1,63 @@
+class AssertionFailureError < Exception
+end
+
+module Assertions
+
+ # Assert that no +values+ are nil or false. Returns the last value.
+ def assert(*values, &block)
+ iterate_and_return_last(values, block) do |v|
+ raise_assertion_error unless v
+ end
+ end
+
+ # The opposite of #assert.
+ def deny(*values)
+ assert(*values.map{ |v| !v})
+ assert(yield(*values)) if block_given?
+ values.last
+ end
+
+ # Assert that no +values+ are nil. Returns the last value.
+ def assert_exists(*values, &block)
+ iterate_and_return_last(values, block) { |value| deny(value.nil?) }
+ end
+
+ # Assert that +values+ are collections that contain at least one element.
+ # Returns the last value.
+ def assert_one_or_more(*values, &block)
+ iterate_and_return_last(values, block) do |value|
+ assert_exists(value)
+ deny(value.kind_of?(String))
+ deny(value.empty?)
+ end
+ end
+
+ def assert_keys(hash, *keys)
+ assert_exists(hash)
+ assert(hash.respond_to?(:[]))
+ values = keys.inject([]) { |vals, k| vals << assert_exists(hash[k]) }
+ assert(yield(*values)) if block_given?
+ hash
+ end
+
+ private
+
+ def iterate_and_return_last(values, block = nil)
+ values.each { |v| yield(v) }
+ if block
+ raise_assertion_error unless block.call(*values)
+ end
+ values.last
+ end
+
+ def raise_assertion_error
+ error = AssertionFailureError.new
+ backtrace = caller
+ trimmed_backtrace = []
+ trimmed_backtrace.unshift(backtrace.pop) until
+ backtrace.last.include?(__FILE__)
+ error.set_backtrace(trimmed_backtrace)
+ raise error
+ end
+
+end
10 script/console
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+# File: script/console
+irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
+
+libs = " -r irb/completion"
+# Perhaps use a console_lib to store any extra methods I may want available in the cosole
+# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
+libs << " -r #{File.dirname(__FILE__) + '/../lib/alter_ego.rb'}"
+puts "Loading AlterEgo gem"
+exec "#{irb} #{libs} --simple-prompt"
14 script/destroy
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+begin
+ require 'rubigen'
+rescue LoadError
+ require 'rubygems'
+ require 'rubigen'
+end
+require 'rubigen/scripts/destroy'
+
+ARGV.shift if ['--help', '-h'].include?(ARGV[0])
+RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
+RubiGen::Scripts::Destroy.new.run(ARGV)
14 script/generate
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+begin
+ require 'rubigen'
+rescue LoadError
+ require 'rubygems'
+ require 'rubigen'
+end
+require 'rubigen/scripts/generate'
+
+ARGV.shift if ['--help', '-h'].include?(ARGV[0])
+RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
+RubiGen::Scripts::Generate.new.run(ARGV)
71 script/txt2html
@@ -0,0 +1,71 @@
+#!/usr/bin/env ruby
+
+load File.dirname(__FILE__) + "/../Rakefile"
+require 'rubyforge'
+require 'redcloth'
+require 'syntax/convertors/html'
+require 'erb'
+
+download = "http://rubyforge.org/projects/#{$hoe.rubyforge_name}"
+version = $hoe.version
+
+def rubyforge_project_id
+ RubyForge.new.configure.autoconfig["group_ids"][$hoe.rubyforge_name]
+end
+
+class Fixnum
+ def ordinal
+ # teens
+ return 'th' if (10..19).include?(self % 100)
+ # others
+ case self % 10
+ when 1: return 'st'
+ when 2: return 'nd'
+ when 3: return 'rd'
+ else return 'th'
+ end
+ end
+end
+
+class Time
+ def pretty
+ return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
+ end
+end
+
+def convert_syntax(syntax, source)
+ return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
+end
+
+if ARGV.length >= 1
+ src, template = ARGV
+ template ||= File.join(File.dirname(__FILE__), '/../website/template.html.erb')
+else
+ puts("Usage: #{File.split($0).last} source.txt [template.html.erb] > output.html")
+ exit!
+end
+
+template = ERB.new(File.open(template).read)
+
+title = nil
+body = nil
+File.open(src) do |fsrc|
+ title_text = fsrc.readline
+ body_text_template = fsrc.read
+ body_text = ERB.new(body_text_template).result(binding)
+ syntax_items = []
+ body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
+ ident = syntax_items.length
+ element, syntax, source = $1, $2, $3
+ syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
+ "syntax-temp-#{ident}"
+ }
+ title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
+ body = RedCloth.new(body_text).to_html
+ body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
+end
+stat = File.stat(src)
+created = stat.ctime
+modified = stat.mtime
+
+$stdout << template.result(binding)
1,051 spec/alter_ego_spec.rb
@@ -0,0 +1,1051 @@
+require 'ostruct'
+require File.expand_path('spec_helper', File.dirname(__FILE__))
+
+# let's define a traffic light class with three states: proceed, caution, and
+# stop. We'll leave the DSL for later, and use old-school class definitions to
+# start out.
+
+class TrafficLightWithClassicStates
+ include AlterEgo
+
+ class ProceedState < State
+ end
+
+ class CautionState < State
+ end
+
+ class StopState < State
+ end
+
+ add_state(ProceedState)
+ add_state(CautionState)
+ add_state(StopState)
+
+end
+
+describe TrafficLightWithClassicStates do
+ before :each do
+ @it = TrafficLightWithClassicStates
+ end
+
+ it "should have the specified states" do
+ @it.states.values.should include(TrafficLightWithClassicStates::ProceedState)
+ @it.states.values.should include(TrafficLightWithClassicStates::CautionState)
+ @it.states.values.should include(TrafficLightWithClassicStates::StopState)
+ end
+end
+
+# Before we go any further, we'll define some identifiers for our states. This
+# will make them easier to work with.
+
+class TrafficLightWithIdentifiers
+ include AlterEgo
+
+ class ProceedState < State
+ def self.identifier; :proceed; end
+ end
+
+ class CautionState < State
+ def self.identifier; :caution; end
+ end
+
+ class StopState < State
+ def self.identifier; :stop; end
+ end
+
+ add_state(ProceedState)
+ add_state(CautionState)
+ add_state(StopState)
+
+ def initialize(starting_state = :proceed)
+ self.state=(starting_state)
+ end
+
+ def cycle
+ case current_state.identifier
+ when :proceed then transition_to(:caution)
+ when :caution then transition_to(:stop)
+ when :stop then transition_to(:proceed)
+ else raise "Should never get here"
+ end
+ end
+end
+
+describe "a green light", :shared => true do
+ it "should be in 'proceed' state" do
+ @it.current_state.should == :proceed
+ end
+
+ it "should change to the caution (yellow) state on cycle" do
+ @it.cycle
+ @it.current_state.should == :caution
+ end
+end
+
+describe "a yellow light", :shared => true do
+ it "should be in 'caution' state" do
+ @it.current_state.should == :caution
+ end
+
+ it "should change to stop (red) on cycle" do
+ @it.cycle
+ @it.current_state.should == :stop
+ end
+end
+
+describe "a red light", :shared => true do
+ it "should be in 'stop' state" do
+ @it.current_state.should == :stop
+ end
+
+ it "should change to proceed (green) on cycle" do
+ @it.cycle
+ @it.current_state.should == :proceed
+ end
+end
+
+describe TrafficLightWithIdentifiers, "by default" do
+ before :each do
+ @it = TrafficLightWithIdentifiers.new
+ end
+
+ it_should_behave_like "a green light"
+end
+
+describe TrafficLightWithIdentifiers, "when yellow" do
+ before :each do
+ @it = TrafficLightWithIdentifiers.new(:caution)
+ end
+
+ it_should_behave_like "a yellow light"
+end
+
+describe TrafficLightWithIdentifiers, "when red" do
+ before :each do
+ @it = TrafficLightWithIdentifiers.new(:stop)
+ end
+
+ it_should_behave_like "a red light"
+end
+
+# Being able to go from one state to another isn't that big a deal. Let's add
+# some state-specific behaviour.
+
+class TrafficLightWithColors
+ include AlterEgo
+
+ class ProceedState < State
+ def self.identifier
+ :proceed
+ end
+ def color(traffic_light)
+ "green"
+ end
+ end
+
+ class CautionState < State
+ def self.identifier
+ :caution
+ end
+ def color(traffic_light)
+ "yellow"
+ end
+ end
+
+ class StopState < State
+ def self.identifier
+ :stop
+ end
+ def color(traffic_light)
+ "red"
+ end
+ end
+
+ add_state(ProceedState)
+ add_state(CautionState)
+ add_state(StopState)
+
+ def initialize(starting_state = :proceed)
+ self.state=(starting_state)
+ end
+
+ def cycle
+ case current_state.identifier
+ when :proceed then transition_to(:caution)
+ when :caution then transition_to(:stop)
+ when :stop then transition_to(:proceed)
+ else raise "Should never get here"
+ end
+ end
+end
+
+describe "a green light with color", :shared => true do
+ it_should_behave_like "a green light"
+ it "should have color green" do
+ @it.color.should == "green"
+ end
+end
+
+describe "a yellow light with color", :shared => true do
+ it_should_behave_like "a yellow light"
+ it "should have color yellow" do
+ @it.color.should == "yellow"
+ end
+end
+
+describe "a red light with color", :shared => true do
+ it_should_behave_like "a red light"
+ it "should have color red" do
+ @it.color.should == "red"
+ end
+end
+
+describe TrafficLightWithColors, "when green" do
+ before :each do
+ @it = TrafficLightWithColors.new(:proceed)
+ end
+ it_should_behave_like "a green light with color"
+end
+
+describe TrafficLightWithColors, "when yellow" do
+ before :each do
+ @it = TrafficLightWithColors.new(:caution)
+ end
+ it_should_behave_like "a yellow light with color"
+end
+
+describe TrafficLightWithColors, "when red" do
+ before :each do
+ @it = TrafficLightWithColors.new(:stop)
+ end
+ it_should_behave_like "a red light with color"
+end
+
+# This is all very verbose. Now that we have a feel for the object model, let's
+# introduce the DSL syntax. Notice that the identifier becomes an argument to
+# the 'state' declaration, and the #color methods become "handlers". Also note
+# there is no longer any need for an explicit #add_state call.
+
+class TrafficLightDescribedByDsl
+ include AlterEgo
+
+ state :proceed do
+ handle :color do
+ "green"
+ end
+ end
+
+ state :caution do
+ handle :color do
+ "yellow"
+ end
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ end
+
+ def initialize(starting_state = :proceed)
+ self.state=(starting_state)
+ end
+
+ def cycle
+ case current_state.identifier
+ when :proceed then transition_to(:caution)
+ when :caution then transition_to(:stop)
+ when :stop then transition_to(:proceed)
+ else raise "Should never get here"
+ end
+ end
+end
+
+describe TrafficLightDescribedByDsl, "when green" do
+ before :each do
+ @it = TrafficLightDescribedByDsl.new(:proceed)
+ end
+
+ it_should_behave_like "a green light with color"
+end
+
+describe TrafficLightDescribedByDsl, "when yellow" do
+ before :each do
+ @it = TrafficLightDescribedByDsl.new(:caution)
+ end
+
+ it_should_behave_like "a yellow light with color"
+end
+
+describe TrafficLightDescribedByDsl, "when red" do
+ before :each do
+ @it = TrafficLightDescribedByDsl.new(:stop)
+ end
+
+ it_should_behave_like "a red light with color"
+end
+
+# Let's redefine #cycle to be just another handler. Note that when defined
+# with the 'handler' syntax, handler blocks are executed in the context of the
+# context object, that is, the object which has a state.
+
+class TrafficLightWithCycleHandler
+ include AlterEgo
+
+ state :proceed do
+ handle :color do
+ "green"
+ end
+ handle :cycle do
+ transition_to(:caution)
+ end
+ end
+
+ state :caution do
+ handle :color do
+ "yellow"
+ end
+ handle :cycle do
+ transition_to(:stop)
+ end
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ handle :cycle do
+ transition_to(:proceed)
+ end
+ end
+
+ def initialize(starting_state = :proceed)
+ self.state=(starting_state)
+ end
+end
+
+describe TrafficLightWithCycleHandler, "when green" do
+ before :each do
+ @it = TrafficLightWithCycleHandler.new(:proceed)
+ end
+
+ it_should_behave_like "a green light with color"
+end
+
+describe TrafficLightWithCycleHandler, "when yellow" do
+ before :each do
+ @it = TrafficLightWithCycleHandler.new(:caution)
+ end
+
+ it_should_behave_like "a yellow light with color"
+end
+
+describe TrafficLightWithCycleHandler, "when red" do
+ before :each do
+ @it = TrafficLightWithCycleHandler.new(:stop)
+ end
+
+ it_should_behave_like "a red light with color"
+end
+
+# In fact, the pattern of a handler which executes a state transition is common
+# enough that there is a special syntax for it. Let's convert to using that
+# syntax.
+
+# While we're at it, we'll also add a :default keyword to the :green state, and
+# eliminate the initializer.
+
+class TrafficLightWithTransitions
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle
+ end
+
+ state :caution do
+ handle :color do
+ "yellow"
+ end
+ transition :to => :stop, :on => :cycle
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle
+ end
+end
+
+describe TrafficLightWithTransitions, "by default" do
+ before :each do
+ @it = TrafficLightWithTransitions.new
+ end
+
+ it "should be in the green state" do
+ @it.current_state.should == :proceed
+ end
+end
+
+describe TrafficLightWithTransitions, "when green" do
+ before :each do
+ @it = TrafficLightWithTransitions.new
+ end
+
+ it_should_behave_like "a green light with color"
+end
+
+describe TrafficLightWithTransitions, "when yellow" do
+ before :each do
+ @it = TrafficLightWithTransitions.new
+ @it.cycle
+ end
+
+ it_should_behave_like "a yellow light with color"
+end
+
+describe TrafficLightWithTransitions, "when red" do
+ before :each do
+ @it = TrafficLightWithTransitions.new
+ @it.cycle
+ @it.cycle
+ end
+
+ it_should_behave_like "a red light with color"
+end
+
+# It is possible to have only some of the states handle a given request. If the
+# method is called while the object is in a state which doesn't handle it, a
+# WrongStateError will be raised.
+#
+# Let's add a #seconds_till_red method to our traffic light, so that it can show
+# a countdown letting motorists know exactly how long they have until the light
+# turns red. Let's say for the sake of example that it will only be valid to
+# call this method when the light is yellow.
+
+class TrafficLightWithRedCountdown
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle
+ end
+
+ state :caution do
+ handle :color do
+ "yellow"
+ end
+ handle :seconds_till_red do
+ # ...
+ end
+ transition :to => :stop, :on => :cycle
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle
+ end
+end
+
+describe TrafficLightWithRedCountdown, "that is green" do
+ before :each do
+ @it = TrafficLightWithRedCountdown.new
+ end
+
+ it "should raise an error if #seconds_till_red is called" do
+ lambda do
+ @it.seconds_till_red
+ end.should raise_error(AlterEgo::WrongStateError)
+ end
+end
+
+describe TrafficLightWithRedCountdown, "that is yellow" do
+ before :each do
+ @it = TrafficLightWithRedCountdown.new
+ @it.cycle
+ end
+
+ it "should raise an error if #seconds_till_red is called" do
+ lambda do
+ @it.seconds_till_red
+ end.should_not raise_error
+ end
+end
+
+# It is possible to get a list of currently handled requests, as well as a list
+# of all possible requests supported in any state.
+
+describe TrafficLightWithRedCountdown do
+ before :each do
+ @it = TrafficLightWithRedCountdown.new
+ end
+
+ it "should know what requests are supported by states" do
+ @it.all_handled_requests.should include(:cycle, :color, :seconds_till_red)
+ end
+end
+
+# The customer has decided the traffic light must sound an audible alert
+# while in the yellow state, in order to warn vision-impaired pedestrians.
+#
+# In order to accomodate this requirement, we will use on_enter and on_exit
+# handlers to switch an alarm on and off.
+
+class TrafficLightWithAlarm
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle
+ end
+
+ state :caution do
+ on_enter do
+ turn_on_alarm
+ end
+ on_exit do
+ turn_off_alarm
+ end
+ handle :color do
+ "yellow"
+ end
+ handle :seconds_till_red do
+ # ...
+ end
+ transition :to => :stop, :on => :cycle
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle
+ end
+
+ def initialize(hardware_controller)
+ @hardware_controller = hardware_controller
+ end
+
+ def turn_on_alarm
+ @hardware_controller.alarm_enabled = true
+ end
+
+ def turn_off_alarm
+ @hardware_controller.alarm_enabled = false
+ end
+end
+
+describe TrafficLightWithAlarm do
+ it "should not include on_enter or on_exit in list of handlers" do
+ TrafficLightWithAlarm.all_handled_requests.should_not include(:on_enter)
+ TrafficLightWithAlarm.all_handled_requests.should_not include(:on_exit)
+ end
+end
+
+describe TrafficLightWithAlarm, "when green" do
+ before :each do
+ @hardware_controller = mock("Hardware Controller")
+ @it = TrafficLightWithAlarm.new(@hardware_controller)
+ end
+
+ it "should enable alarm on transition to yellow" do
+ @hardware_controller.should_receive(:alarm_enabled=).
+ with(true)
+ @it.cycle
+ end
+end
+
+describe TrafficLightWithAlarm, "when yellow" do
+ before :each do
+ @hardware_controller = stub("Hardware Controller", :alarm_enabled= => nil)
+ @it = TrafficLightWithAlarm.new(@hardware_controller)
+ @it.cycle
+ end
+
+ it "should disable alarm on transition to yellow" do
+ @hardware_controller.should_receive(:alarm_enabled=).
+ with(false)
+ @it.cycle
+ end
+end
+
+# For safety reasons, the light should not allow transitions faster than every
+# twenty seconds. We'll add state guards to ensure this constraint is observed.
+# We'll also add a generic state change action for all states to restart the timer
+# each time the state changes.
+
+class TrafficLightWithGuards
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle, :if => :min_time_elapsed?
+ end
+
+ state :caution do
+ on_enter do
+ turn_on_alarm
+ end
+ on_exit do
+ turn_off_alarm
+ end
+ handle :color do
+ "yellow"
+ end
+ handle :seconds_till_red do
+ # ...
+ end
+ transition :to => :stop, :on => :cycle, :if => :min_time_elapsed?
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+
+ # Just to demonstrate that it is possible, we use a proc here instead of a
+ # symbol
+ transition(:to => :proceed, :on => :cycle, :if => proc { min_time_elapsed? })
+ end
+
+ # On state change
+ request_filter :state => any, :request => any, :new_state => not_nil do
+ @hardware_controller.restart_timer
+ end
+
+ def initialize(hardware_controller)
+ @hardware_controller = hardware_controller
+ end
+
+ def turn_on_alarm
+ @hardware_controller.alarm_enabled = true
+ end
+
+ def turn_off_alarm
+ @hardware_controller.alarm_enabled = false
+ end
+
+ def min_time_elapsed?
+ @hardware_controller.time_elapsed >= 20
+ end
+end
+
+describe TrafficLightWithGuards, "that is green" do
+ before :each do
+ @hardware_controller = stub("Hardware Controller",
+ :time_elapsed => 21,
+ :restart_timer => nil,
+ :alarm_enabled= => nil)
+ @it = TrafficLightWithGuards.new(@hardware_controller)
+ end
+
+ it "should check the hardware controller's #time_elapsed on cycle" do
+ @hardware_controller.should_receive(:time_elapsed).and_return(19)
+ @it.cycle
+ end
+
+ it "should fail to cycle if elapsed time < 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(19)
+ @it.cycle.should be_false
+ @it.current_state.should == :proceed
+ end
+
+ it "should cycle if elapsed time >= 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(20)
+ @it.cycle.should be_true
+ @it.current_state.should == :caution
+ end
+
+ it "should restart the timer on state change" do
+ @hardware_controller.should_receive(:restart_timer)
+ @it.cycle
+ end
+
+ it "should restart the timer on state change" do
+ @hardware_controller.should_receive(:restart_timer)
+ @it.cycle
+ end
+end
+
+describe TrafficLightWithGuards, "that is yellow" do
+ before :each do
+ @hardware_controller = stub("Hardware Controller",
+ :time_elapsed => 21,
+ :restart_timer => nil,
+ :alarm_enabled= => nil)
+ @it = TrafficLightWithGuards.new(@hardware_controller)
+ @it.cycle
+ end
+
+ it "should check the hardware controller's #time_elapsed on cycle" do
+ @hardware_controller.should_receive(:time_elapsed).and_return(19)
+ @it.cycle
+ end
+
+ it "should fail to cycle if elapsed time < 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(19)
+ @it.cycle.should be_false
+ end
+
+ it "should remain in :caution state if elapsed time < 20" do
+ @hardware_controller.stub!(:time_elapsed).and_return(19)
+ @it.cycle
+ @it.current_state.should == :caution
+ end
+
+ it "should cycle if elapsed time >= 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(20)
+ @it.cycle.should be_true
+ end
+
+ it "should cycle to :stop state if elapsed time >= 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(20)
+ @it.cycle
+ @it.current_state.should == :stop
+ end
+
+ it "should restart the timer on state change" do
+
+ @hardware_controller.should_receive(:restart_timer)
+ @it.cycle
+ end
+end
+
+describe TrafficLightWithGuards, "that is red" do
+ before :each do
+ @hardware_controller = stub("Hardware Controller",
+ :time_elapsed => 21,
+ :restart_timer => nil,
+ :alarm_enabled= => nil)
+ @it = TrafficLightWithGuards.new(@hardware_controller)
+ @it.cycle
+ @it.cycle
+ end
+
+ it "should fail to cycle if elapsed time < 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(19)
+ @it.cycle.should be_false
+ @it.current_state.should == :stop
+ end
+
+ it "should cycle if elapsed time >= 20 seconds" do
+ @hardware_controller.stub!(:time_elapsed).and_return(20)
+ @it.cycle.should be_true
+ @it.current_state.should == :proceed
+ end
+
+end
+
+# The traffic light controller actually stores it's current state as three
+# discrete booleans, one for each light which should be either on or off. We'll
+# customize the state saving and loading methods in order to support this
+# arrangement.
+
+class TrafficLightWithCustomStorage
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle
+ end
+
+ state :caution do
+ on_enter do
+ turn_on_alarm
+ end
+ on_exit do
+ turn_off_alarm
+ end
+ handle :color do
+ "yellow"
+ end
+ handle :seconds_till_red do
+ # ...
+ end
+ transition :to => :stop, :on => :cycle
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle
+ end
+
+ def initialize(hardware_controller)
+ @hardware_controller = hardware_controller
+ end
+
+ def turn_on_alarm
+ @hardware_controller.alarm_enabled = true
+ end
+
+ def turn_off_alarm
+ @hardware_controller.alarm_enabled = false
+ end
+
+ def state
+ gyr = [
+ @hardware_controller.green,
+ @hardware_controller.yellow,
+ @hardware_controller.red
+ ]
+
+ case gyr
+ when [true, false, false] then :proceed
+ when [false, true, false] then :caution
+ when [false, false, true] then :stop
+ else raise "Invalid state!"
+ end
+ end
+
+ def state=(value)
+ gyr = case value
+ when :proceed then [true, false, false]
+ when :caution then [false, true, false]
+ when :stop then [false, false, true]
+ end
+ @hardware_controller.green = gyr[0]
+ @hardware_controller.yellow = gyr[1]
+ @hardware_controller.red = gyr[2]
+ end
+end
+
+
+describe TrafficLightWithCustomStorage, "that is green" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => true,
+ :yellow => false,
+ :red => false)
+ @it = TrafficLightWithCustomStorage.new(@hardware_controller)
+ end
+
+ it "should be in the proceed state" do
+ @it.current_state.should == :proceed
+ end
+
+ it "should set lights for yellow on cycle" do
+ @it.cycle
+ @hardware_controller.green.should be_false
+ @hardware_controller.yellow.should be_true
+ @hardware_controller.red.should be_false
+ end
+end
+
+describe TrafficLightWithCustomStorage, "that is yellow" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => false,
+ :yellow => true,
+ :red => false)
+ @it = TrafficLightWithCustomStorage.new(@hardware_controller)
+ end
+
+ it "should be in the caution state" do
+ @it.current_state.should == :caution
+ end
+
+ it "should set lights for red on cycle" do
+ @it.cycle
+ @hardware_controller.green.should be_false
+ @hardware_controller.yellow.should be_false
+ @hardware_controller.red.should be_true
+ end
+
+end
+
+describe TrafficLightWithCustomStorage, "that is red" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => false,
+ :yellow => false,
+ :red => true)
+ @it = TrafficLightWithCustomStorage.new(@hardware_controller)
+ end
+
+ it "should be in the stop state" do
+ @it.current_state.should == :stop
+ end
+
+ it "should set lights for green on cycle" do
+ @it.cycle
+ @hardware_controller.green.should be_true
+ @hardware_controller.yellow.should be_false
+ @hardware_controller.red.should be_false
+ end
+
+end
+
+# In order to integrate with a pedestrian traffic light, our light needs to send
+# a signal whenever it changes. We'll add a transition action to handle this.
+#
+# It also needs to flash a strobe when transitioning to yellow or red. We'll
+# use a request filter and a state matching pattern to accomplish this.
+
+class TrafficLightWithTransAction
+ include AlterEgo
+
+ state :proceed, :default => true do
+ handle :color do
+ "green"
+ end
+ transition :to => :caution, :on => :cycle do
+ @hardware_controller.notify(:yellow)
+ end
+ end
+
+ state :caution do
+ on_enter do
+ turn_on_alarm
+ end
+ on_exit do
+ turn_off_alarm
+ end
+ handle :color do
+ "yellow"
+ end
+ handle :seconds_till_red do
+ # ...
+ end
+ transition :to => :stop, :on => :cycle do
+ @hardware_controller.notify(:red)
+ end
+ end
+
+ state :stop do
+ handle :color do
+ "red"
+ end
+ transition :to => :proceed, :on => :cycle do
+ @hardware_controller.notify(:green)
+ end
+ end
+
+ request_filter :state => any,
+ :request => any,
+ :new_state => [:caution, :stop] do
+ @hardware_controller.flash_strobe
+ end
+
+ def initialize(hardware_controller)
+ @hardware_controller = hardware_controller
+ end
+
+ def turn_on_alarm
+ @hardware_controller.alarm_enabled = true
+ end
+
+ def turn_off_alarm
+ @hardware_controller.alarm_enabled = false
+ end
+
+ def state
+ gyr = [
+ @hardware_controller.green,
+ @hardware_controller.yellow,
+ @hardware_controller.red
+ ]
+
+ case gyr
+ when [true, false, false] then :proceed
+ when [false, true, false] then :caution
+ when [false, false, true] then :stop
+ else raise "Invalid state!"
+ end
+ end
+
+ def state=(value)
+ gyr = case value
+ when :proceed then [true, false, false]
+ when :caution then [false, true, false]
+ when :stop then [false, false, true]
+ end
+ @hardware_controller.green = gyr[0]
+ @hardware_controller.yellow = gyr[1]
+ @hardware_controller.red = gyr[2]
+ end
+end
+
+describe TrafficLightWithTransAction, "that is green" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => true,
+ :yellow => false,
+ :red => false)
+ @hardware_controller.stub!(:notify)
+ @it = TrafficLightWithTransAction.new(@hardware_controller)
+ end
+
+ it "should notify that it has turned yellow on cycle" do
+ @hardware_controller.should_receive(:notify).with(:yellow)
+ @it.cycle
+ end
+
+ it "should flash strobe on cycle to yellow" do
+ @hardware_controller.should_receive(:flash_strobe)
+ @it.cycle
+ end
+end
+
+
+describe TrafficLightWithTransAction, "that is yellow" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => false,
+ :yellow => true,
+ :red => false)
+ @hardware_controller.stub!(:notify)
+ @it = TrafficLightWithTransAction.new(@hardware_controller)
+ end
+
+ it "should notify that it has turned red on cycle" do
+ @hardware_controller.should_receive(:notify).with(:red)
+ @it.cycle
+ end
+
+ it "should flash strobe on cycle to red" do
+ @hardware_controller.should_receive(:flash_strobe)
+ @it.cycle
+ end
+end
+
+describe TrafficLightWithTransAction, "that is red" do
+ before :each do
+ @hardware_controller = OpenStruct.new( :time_elapsed => 21,
+ :restart_timer => nil,
+ :green => false,
+ :yellow => false,
+ :red => true)
+ @hardware_controller.stub!(:notify)
+ @it = TrafficLightWithTransAction.new(@hardware_controller)
+ end
+
+ it "should notify that it has turned green on cycle" do
+ @hardware_controller.should_receive(:notify).with(:green)
+ @it.cycle
+ end
+
+ it "should not flash strobe on cycle to green" do
+ @hardware_controller.should_not_receive(:flash_strobe)
+ end
+
+end
284 spec/assertions_spec.rb
@@ -0,0 +1,284 @@
+require File.expand_path('spec_helper', File.dirname(__FILE__))
+
+module AssertionsSpecHelper
+ def do_success(&block)
+ do_assertion(@success_values.first, &block)
+ end
+ def do_failure(&block)
+ do_assertion(@failure_values.first, &block)
+ end
+end
+
+describe "any assertion", :shared => true do
+ it "on failure, should raise an exception with trimmed backtrace" do
+ begin
+ do_failure
+ rescue AssertionFailureError => e
+ e.backtrace.first.should include(__FILE__)
+ end
+ end
+end
+
+describe "a basic assertion", :shared => true do
+ it "should raise AssertionFailureError when it fails" do
+ @failure_values.each do |value|
+ lambda do
+ do_assertion(value)
+ end.should raise_error(AssertionFailureError)
+ end
+ end
+
+ it "should not raise an exception when it succeeds" do
+ @success_values.each do |value|
+ lambda do
+ do_assertion(value)
+ end.should_not raise_error
+ end
+ end
+
+ it "should return its argument on success" do
+ @success_values.each do |value|
+ do_assertion(value).should equal(value)
+ end
+ end
+
+end
+
+describe "an assertion taking a block", :shared => true do
+ it "should fail if the block result is not true" do
+ lambda do
+ do_success { false }
+ end.should raise_error(AssertionFailureError)
+ end
+end
+
+describe "an assertion yielding passed values", :shared => true do
+
+ it_should_behave_like "an assertion taking a block"
+
+ it "should yield values that pass the test" do
+ @success_values.each do |value|
+ did_yield = false
+ do_assertion(value) do |yielded_value|
+ did_yield = true
+ yielded_value.should equal(value)
+ true
+ end
+ did_yield.should be_true
+ end
+ end
+
+ it "should not yield values that fail the test" do
+ @failure_values.each do |value|
+ did_yield = false
+ lambda do
+ do_assertion(value) do |yielded_value|
+ did_yield = true
+ end
+ end.should raise_error(AssertionFailureError)
+ did_yield.should be_false
+ end
+ end
+
+end
+
+describe "an assertion taking multiple values", :shared => true do
+ def gather_arguments(values)
+ args = []
+ # gather 3 arguments
+ 3.times do |i|
+ args[i] = values[i % values.size]
+ end
+ args
+ end
+
+ it "should not raise error if all values succeed test" do
+ values = gather_arguments(@success_values)
+ lambda { do_assertion(*values) }.should_not raise_error
+ end
+
+ it "should raise error if all values fail test" do
+ values = gather_arguments(@failure_values)
+ lambda { do_assertion(*values) }.should raise_error(AssertionFailureError)
+ end
+
+ it "should raise error if one values fails test" do
+ values = gather_arguments(@success_values)
+ values[1] = @failure_values.first
+ lambda { do_assertion(*values) }.should raise_error(AssertionFailureError)
+ end
+
+ it "should return the last argument on success" do
+ values = gather_arguments(@success_values)
+ do_assertion(*values).should equal(values.last)
+ end
+
+end
+
+describe Assertions, "#assert" do
+
+ include Assertions
+ include AssertionsSpecHelper
+
+ def do_assertion(*args, &block)
+ assert(*args, &block)
+ end
+
+ before :each do
+ @success_values = [true, "", 0]
+ @failure_values = [false, nil]
+ end
+
+ it_should_behave_like "any assertion"
+ it_should_behave_like "a basic assertion"
+ it_should_behave_like "an assertion taking multiple values"
+ it_should_behave_like "an assertion yielding passed values"
+end
+
+describe Assertions, "#assert_exists" do
+
+ include Assertions
+ include AssertionsSpecHelper
+
+ def do_assertion(*args, &block)
+ assert_exists(*args, &block)
+ end
+
+ before :each do
+ @success_values = ["foo", 123, false]
+ @failure_values = [nil]
+ end
+
+ it_should_behave_like "any assertion"
+ it_should_behave_like "a basic assertion"
+ it_should_behave_like "an assertion taking multiple values"
+ it_should_behave_like "an assertion yielding passed values"
+
+end
+
+describe Assertions, "#assert_one_or_more" do
+
+ include Assertions
+ include AssertionsSpecHelper
+
+ def do_assertion(*args, &block)
+ assert_one_or_more(*args, &block)
+ end
+
+ before :each do
+ @success_values = [[1], {:foo => :bar }]
+ @failure_values = [nil, [], "foo"]
+ end
+
+ it_should_behave_like "any assertion"
+ it_should_behave_like "a basic assertion"
+ it_should_behave_like "an assertion taking multiple values"
+ it_should_behave_like "an assertion yielding passed values"
+end
+
+describe Assertions, "#deny" do
+
+ include Assertions
+ include AssertionsSpecHelper
+
+ def do_assertion(*args, &block)
+ deny(*args, &block)
+ end
+
+ before :each do
+ @success_values = [false, nil]
+ @failure_values = [true, "", 0]
+ end
+
+ it_should_behave_like "any assertion"
+ it_should_behave_like "a basic assertion"
+ it_should_behave_like "an assertion taking multiple values"
+ it_should_behave_like "an assertion yielding passed values"
+end
+
+describe Assertions, "#assert_keys" do
+
+ include Assertions
+
+ def do_assertion(*args, &block)
+ assert_keys(*args, &block)
+ end
+
+ def do_success(&block)
+ assert_keys({}, &block)
+ end
+
+ def do_failure(&block)
+ assert_keys({}, :foo, &block)
+ end
+
+ it_should_behave_like "any assertion"
+ it_should_behave_like "an assertion taking a block"
+
+ it "should fail if a specified key does not exist" do
+ lambda { assert_keys({}, :foo) }.should raise_error(AssertionFailureError)
+ end
+
+ it "should fail if a specified key is nil" do
+ lambda do
+ assert_keys({:foo => nil}, :foo)
+ end.should raise_error(AssertionFailureError)
+ end
+
+ it "should fail if any of the specified keys are nil" do
+ lambda do
+ assert_keys({:foo => true, :bar => nil}, :foo, :bar)
+ end.should raise_error(AssertionFailureError)
+ end
+
+ it "should fail if the given hash is nil" do
+ lambda do
+ assert_keys(nil)
+ end.should raise_error(AssertionFailureError)
+ end
+
+ it "should fail if given something unlike a hash" do
+ lambda do
+ assert_keys(true)
+ end.should raise_error(AssertionFailureError)
+ end
+
+ it "should succeed if no keys are given" do
+ lambda do
+ assert_keys({:foo => true, :bar => nil})
+ end.should_not raise_error
+ end
+
+ it "should yield key values if they all exist" do
+ did_yield = false
+ assert_keys({:foo => 23, :bar => 32}, :foo, :bar) do |x, y|
+ did_yield = true
+ x.should == 23
+ y.should == 32
+ true
+ end
+ did_yield.should be_true
+ end
+
+ it "should yield nothing if a key is missing" do
+ begin
+ did_yield = false
+ assert_keys({:foo => 23, :bar => 32}, :foo, :bar) do |x, y|
+ did_yield = true
+ end
+ rescue AssertionFailureError
+ did_yield.should be_false
+ end
+ end
+
+ it "should return the hash" do
+ @hash = { :buz => 42 }
+ assert_keys(@hash, :buz).should equal(@hash)
+ end
+end
+
+describe AssertionFailureError do
+ it "should derive from Exception" do
+ AssertionFailureError.superclass.should equal(Exception)
+ end
+end
1  spec/spec.opts
@@ -0,0 +1 @@
+--colour
10 spec/spec_helper.rb
@@ -0,0 +1,10 @@
+begin
+ require 'spec'
+rescue LoadError
+ require 'rubygems'
+ gem 'rspec'
+ require 'spec'
+end
+
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+require 'alter_ego'
21 tasks/rspec.rake
@@ -0,0 +1,21 @@
+begin
+ require 'spec'
+rescue LoadError
+ require 'rubygems'
+ require 'spec'
+end
+begin
+ require 'spec/rake/spectask'
+rescue LoadError
+ puts <<-EOS
+To use rspec for testing you must install rspec gem:
+ gem install rspec
+EOS
+ exit(0)
+end
+
+desc "Run the specs under spec/models"
+Spec::Rake::SpecTask.new do |t|
+ t.spec_opts = ['--options', "spec/spec.opts"]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+end
11 website/index.html
@@ -0,0 +1,11 @@
+<html>
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <title>alter-ego</title>
+
+ </head>
+ <body id="body">
+ <p>This page has not yet been created for RubyGem <code>alter-ego</code></p>
+ <p>To the developer: To generate it, update website/index.txt and run the rake task <code>website</code> to generate this <code>index.html</code> file.</p>
+ </body>
+</html>
81 website/index.txt
@@ -0,0 +1,81 @@
+h1. alter ego
+
+
+h2. What
+
+
+h2. Installing
+
+<pre syntax="ruby">sudo gem install alter-ego</pre>
+
+h2. The basics
+
+
+h2. Demonstration of usage
+
+
+
+h2. Forum
+
+"http://groups.google.com/group/alter-ego":http://groups.google.com/group/alter-ego
+
+TODO - create Google Group - alter-ego
+
+h2. How to submit patches
+
+Read the "8 steps for fixing other people's code":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/ and for section "8b: Submit patch to Google Groups":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/#8b-google-groups, use the Google Group above.
+
+TODO - pick SVN or Git instructions
+
+The trunk repository is <code>svn://rubyforge.org/var/svn/alter-ego/trunk</code> for anonymous access.
+
+OOOORRRR
+
+You can fetch the source from either:
+
+<% if rubyforge_project_id %>
+
+* rubyforge: "http://rubyforge.org/scm/?group_id=<%= rubyforge_project_id %>":http://rubyforge.org/scm/?group_id=<%= rubyforge_project_id %>
+
+<pre>git clone git://rubyforge.org/alter-ego.git</pre>
+
+<% else %>
+
+* rubyforge: MISSING IN ACTION
+
+TODO - You can not created a RubyForge project, OR have not run <code>rubyforge config</code>
+yet to refresh your local rubyforge data with this projects' id information.
+
+When you do this, this message will magically disappear!
+
+Or you can hack website/index.txt and make it all go away!!
+
+<% end %>
+
+* github: "http://github.com/GITHUB_USERNAME/alter-ego/tree/master":http://github.com/GITHUB_USERNAME/alter-ego/tree/master
+
+<pre>git clone git://github.com/GITHUB_USERNAME/alter-ego.git</pre>
+
+
+TODO - add "github_username: username" to ~/.rubyforge/user-config.yml and newgem will reuse it for future projects.
+
+
+* gitorious: "git://gitorious.org/alter-ego/mainline.git":git://gitorious.org/alter-ego/mainline.git
+
+<pre>git clone git://gitorious.org/alter-ego/mainline.git</pre>
+
+h3. Build and test instructions
+
+<pre>cd alter-ego
+rake test
+rake install_gem</pre>
+
+
+h2. License
+
+This code is free to use under the terms of the MIT license.
+
+h2. Contact
+
+Comments are welcome. Send an email to "TODO":mailto:TODO via the "forum":http://groups.google.com/group/alter-ego
+
285 website/javascripts/rounded_corners_lite.inc.js
@@ -0,0 +1,285 @@
+
+ /****************************************************************
+ * *
+ * curvyCorners *
+ * ------------ *
+ * *
+ * This script generates rounded corners for your divs. *
+ * *
+ * Version 1.2.9 *
+ * Copyright (c) 2006 Cameron Cooke *
+ * By: Cameron Cooke and Tim Hutchison. *
+ * *
+ * *
+ * Website: http://www.curvycorners.net *
+ * Email: info@totalinfinity.com *
+ * Forum: http://www.curvycorners.net/forum/ *
+ * *
+ * *
+ * This library is free software; you can redistribute *
+ * it and/or modify it under the terms of the GNU *
+ * Lesser General Public License as published by the *
+ * Free Software Foundation; either version 2.1 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * This library is distributed in the hope that it will *
+ * be useful, but WITHOUT ANY WARRANTY; without even the *
+ * implied warranty of MERCHANTABILITY or FITNESS FOR A *
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public *
+ * License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser *
+ * General Public License along with this library; *
+ * Inc., 59 Temple Place, Suite 330, Boston, *
+ * MA 02111-1307 USA *
+ * *
+ ****************************************************************/
+
+var isIE = navigator.userAgent.toLowerCase().indexOf("msie") > -1; var isMoz = document.implementation && document.implementation.createDocument; var isSafari = ((navigator.userAgent.toLowerCase().indexOf('safari')!=-1)&&(navigator.userAgent.toLowerCase().indexOf('mac')!=-1))?true:false; function curvyCorners()
+{ if(typeof(arguments[0]) != "object") throw newCurvyError("First parameter of curvyCorners() must be an object."); if(typeof(arguments[1]) != "object" && typeof(arguments[1]) != "string") throw newCurvyError("Second parameter of curvyCorners() must be an object or a class name."); if(typeof(arguments[1]) == "string")
+{ var startIndex = 0; var boxCol = getElementsByClass(arguments[1]);}
+else
+{ var startIndex = 1; var boxCol = arguments;}
+var curvyCornersCol = new Array(); if(arguments[0].validTags)
+var validElements = arguments[0].validTags; else
+var validElements = ["div"]; for(var i = startIndex, j = boxCol.length; i < j; i++)
+{ var currentTag = boxCol[i].tagName.toLowerCase(); if(inArray(validElements, currentTag) !== false)
+{ curvyCornersCol[curvyCornersCol.length] = new curvyObject(arguments[0], boxCol[i]);}
+}
+this.objects = curvyCornersCol; this.applyCornersToAll = function()
+{ for(var x = 0, k = this.objects.length; x < k; x++)
+{ this.objects[x].applyCorners();}
+}
+}
+function curvyObject()
+{ this.box = arguments[1]; this.settings = arguments[0]; this.topContainer = null; this.bottomContainer = null; this.masterCorners = new Array(); this.contentDIV = null; var boxHeight = get_style(this.box, "height", "height"); var boxWidth = get_style(this.box, "width", "width"); var borderWidth = get_style(this.box, "borderTopWidth", "border-top-width"); var borderColour = get_style(this.box, "borderTopColor", "border-top-color"); var boxColour = get_style(this.box, "backgroundColor", "background-color"); var backgroundImage = get_style(this.box, "backgroundImage", "background-image"); var boxPosition = get_style(this.box, "position", "position"); var boxPadding = get_style(this.box, "paddingTop", "padding-top"); this.boxHeight = parseInt(((boxHeight != "" && boxHeight != "auto" && boxHeight.indexOf("%") == -1)? boxHeight.substring(0, boxHeight.indexOf("px")) : this.box.scrollHeight)); this.boxWidth = parseInt(((boxWidth != "" && boxWidth != "auto" && boxWidth.indexOf("%") == -1)? boxWidth.substring(0, boxWidth.indexOf("px")) : this.box.scrollWidth)); this.borderWidth = parseInt(((borderWidth != "" && borderWidth.indexOf("px") !== -1)? borderWidth.slice(0, borderWidth.indexOf("px")) : 0)); this.boxColour = format_colour(boxColour); this.boxPadding = parseInt(((boxPadding != "" && boxPadding.indexOf("px") !== -1)? boxPadding.slice(0, boxPadding.indexOf("px")) : 0)); this.borderColour = format_colour(borderColour); this.borderString = this.borderWidth + "px" + " solid " + this.borderColour; this.backgroundImage = ((backgroundImage != "none")? backgroundImage : ""); this.boxContent = this.box.innerHTML; if(boxPosition != "absolute") this.box.style.position = "relative"; this.box.style.padding = "0px"; if(isIE && boxWidth == "auto" && boxHeight == "auto") this.box.style.width = "100%"; if(this.settings.autoPad == true && this.boxPadding > 0)
+this.box.innerHTML = ""; this.applyCorners = function()
+{ for(var t = 0; t < 2; t++)
+{ switch(t)
+{ case 0:
+if(this.settings.tl || this.settings.tr)
+{ var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var topMaxRadius = Math.max(this.settings.tl ? this.settings.tl.radius : 0, this.settings.tr ? this.settings.tr.radius : 0); newMainContainer.style.height = topMaxRadius + "px"; newMainContainer.style.top = 0 - topMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.topContainer = this.box.appendChild(newMainContainer);}
+break; case 1:
+if(this.settings.bl || this.settings.br)
+{ var newMainContainer = document.createElement("DIV"); newMainContainer.style.width = "100%"; newMainContainer.style.fontSize = "1px"; newMainContainer.style.overflow = "hidden"; newMainContainer.style.position = "absolute"; newMainContainer.style.paddingLeft = this.borderWidth + "px"; newMainContainer.style.paddingRight = this.borderWidth + "px"; var botMaxRadius = Math.max(this.settings.bl ? this.settings.bl.radius : 0, this.settings.br ? this.settings.br.radius : 0); newMainContainer.style.height = botMaxRadius + "px"; newMainContainer.style.bottom = 0 - botMaxRadius + "px"; newMainContainer.style.left = 0 - this.borderWidth + "px"; this.bottomContainer = this.box.appendChild(newMainContainer);}
+break;}
+}
+if(this.topContainer) this.box.style.borderTopWidth = "0px"; if(this.bottomContainer) this.box.style.borderBottomWidth = "0px"; var corners = ["tr", "tl", "br", "bl"]; for(var i in corners)
+{ if(i > -1 < 4)
+{ var cc = corners[i]; if(!this.settings[cc])
+{ if(((cc == "tr" || cc == "tl") && this.topContainer != null) || ((cc == "br" || cc == "bl") && this.bottomContainer != null))
+{ var newCorner = document.createElement("DIV"); newCorner.style.position = "relative"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; if(this.backgroundImage == "")
+newCorner.style.backgroundColor = this.boxColour; else
+newCorner.style.backgroundImage = this.backgroundImage; switch(cc)
+{ case "tl":
+newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.tr.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.left = -this.borderWidth + "px"; break; case "tr":
+newCorner.style.height = topMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.tl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderTop = this.borderString; newCorner.style.backgroundPosition = "-" + (topMaxRadius + this.borderWidth) + "px 0px"; newCorner.style.left = this.borderWidth + "px"; break; case "bl":
+newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginRight = this.settings.br.radius - (this.borderWidth*2) + "px"; newCorner.style.borderLeft = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = -this.borderWidth + "px"; newCorner.style.backgroundPosition = "-" + (this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break; case "br":
+newCorner.style.height = botMaxRadius - this.borderWidth + "px"; newCorner.style.marginLeft = this.settings.bl.radius - (this.borderWidth*2) + "px"; newCorner.style.borderRight = this.borderString; newCorner.style.borderBottom = this.borderString; newCorner.style.left = this.borderWidth + "px"
+newCorner.style.backgroundPosition = "-" + (botMaxRadius + this.borderWidth) + "px -" + (this.boxHeight + (botMaxRadius + this.borderWidth)) + "px"; break;}
+}
+}
+else
+{ if(this.masterCorners[this.settings[cc].radius])
+{ var newCorner = this.masterCorners[this.settings[cc].radius].cloneNode(true);}
+else
+{ var newCorner = document.createElement("DIV"); newCorner.style.height = this.settings[cc].radius + "px"; newCorner.style.width = this.settings[cc].radius + "px"; newCorner.style.position = "absolute"; newCorner.style.fontSize = "1px"; newCorner.style.overflow = "hidden"; var borderRadius = parseInt(this.settings[cc].radius - this.borderWidth); for(var intx = 0, j = this.settings[cc].radius; intx < j; intx++)
+{ if((intx +1) >= borderRadius)
+var y1 = -1; else
+var y1 = (Math.floor(Math.sqrt(Math.pow(borderRadius, 2) - Math.pow((intx+1), 2))) - 1); if(borderRadius != j)
+{ if((intx) >= borderRadius)
+var y2 = -1; else
+var y2 = Math.ceil(Math.sqrt(Math.pow(borderRadius,2) - Math.pow(intx, 2))); if((intx+1) >= j)
+var y3 = -1; else
+var y3 = (Math.floor(Math.sqrt(Math.pow(j ,2) - Math.pow((intx+1), 2))) - 1);}
+if((intx) >= j)
+var y4 = -1; else
+var y4 = Math.ceil(Math.sqrt(Math.pow(j ,2) - Math.pow(intx, 2))); if(y1 > -1) this.drawPixel(intx, 0, this.boxColour, 100, (y1+1), newCorner, -1, this.settings[cc].radius); if(borderRadius != j)
+{ for(var inty = (y1 + 1); inty < y2; inty++)
+{ if(this.settings.antiAlias)
+{ if(this.backgroundImage != "")
+{ var borderFract = (pixelFraction(intx, inty, borderRadius) * 100); if(borderFract < 30)
+{ this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, 0, this.settings[cc].radius);}
+else
+{ this.drawPixel(intx, inty, this.borderColour, 100, 1, newCorner, -1, this.settings[cc].radius);}
+}