aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
authorrick <technoweenie@gmail.com>2008-06-28 00:55:02 -0700
committerrick <technoweenie@gmail.com>2008-06-28 00:55:02 -0700
commitb9528ad3c5379896b00772cb44faf1db0fd882d7 (patch)
tree422ae423e519f744d51698d45a6fb931172b9cb2 /activemodel
parentb7c6ceff9a31cc478c4bc89d57980900a775fbed (diff)
downloadrails-b9528ad3c5379896b00772cb44faf1db0fd882d7.tar.gz
rails-b9528ad3c5379896b00772cb44faf1db0fd882d7.tar.bz2
rails-b9528ad3c5379896b00772cb44faf1db0fd882d7.zip
initial statemachine machine and state classes
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/lib/active_model/callbacks.rb2
-rw-r--r--activemodel/lib/active_model/state_machine.rb43
-rw-r--r--activemodel/lib/active_model/state_machine/machine.rb30
-rw-r--r--activemodel/lib/active_model/state_machine/state.rb40
-rw-r--r--activemodel/lib/active_model/validations.rb2
-rw-r--r--activemodel/test/observing_test.rb6
-rw-r--r--activemodel/test/state_machine/machine_test.rb28
-rw-r--r--activemodel/test/state_machine/state_test.rb73
-rw-r--r--activemodel/test/state_machine_test.rb335
-rw-r--r--activemodel/test/test_helper.rb18
10 files changed, 571 insertions, 6 deletions
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 1dd20156ec..c94f76109f 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,4 +1,4 @@
-require 'active_model/observing'
+require 'active_model/core'
module ActiveModel
module Callbacks
diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb
new file mode 100644
index 0000000000..bb038f6b7a
--- /dev/null
+++ b/activemodel/lib/active_model/state_machine.rb
@@ -0,0 +1,43 @@
+Dir[File.dirname(__FILE__) + "/state_machine/*.rb"].sort.each do |path|
+ filename = File.basename(path)
+ require "active_model/state_machine/#{filename}"
+end
+
+module ActiveModel
+ module StateMachine
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ module ClassMethods
+ def inherited(klass)
+ super
+ klass.state_machines = state_machines
+ end
+
+ def state_machines
+ @state_machines ||= {}
+ end
+
+ def state_machines=(value)
+ @state_machines = value ? value.dup : nil
+ end
+
+ def state_machine(name = nil, options = {}, &block)
+ if name.is_a?(Hash)
+ options = name
+ name = nil
+ end
+ name ||= :default
+ state_machines[name] ||= Machine.new(self, name)
+ block ? state_machines[name].update(options, &block) : state_machines[name]
+ end
+ end
+
+ def current_state(name = nil)
+ sm = self.class.state_machine(name)
+ ivar = "@#{sm.name}_current_state"
+ instance_variable_get(ivar) || instance_variable_set(ivar, sm.initial_state)
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb
new file mode 100644
index 0000000000..75ed8f8b65
--- /dev/null
+++ b/activemodel/lib/active_model/state_machine/machine.rb
@@ -0,0 +1,30 @@
+module ActiveModel
+ module StateMachine
+ class Machine
+ attr_accessor :initial_state, :states, :event
+ attr_reader :klass, :name
+
+ def initialize(klass, name)
+ @klass, @name, @states, @events = klass, name, [], {}
+ end
+
+ def states_for_select
+ states.map { |st| [st.display_name, st.name.to_s] }
+ end
+
+ def state(name, options = {})
+ @states << State.new(self, name, options)
+ end
+
+ def initial_state
+ @initial_state ||= (states.first ? states.first.name : nil)
+ end
+
+ def update(options = {}, &block)
+ @initial_state = options[:initial]
+ instance_eval(&block)
+ self
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/lib/active_model/state_machine/state.rb b/activemodel/lib/active_model/state_machine/state.rb
new file mode 100644
index 0000000000..5851a6fb79
--- /dev/null
+++ b/activemodel/lib/active_model/state_machine/state.rb
@@ -0,0 +1,40 @@
+module ActiveModel
+ module StateMachine
+ class State
+ attr_reader :name, :options
+
+ def initialize(machine, name, options={})
+ @machine, @name, @options, @display_name = machine, name, options, options.delete(:display)
+ machine.klass.send(:define_method, "#{name}?") do
+ current_state.to_s == name.to_s
+ end
+ end
+
+ def ==(state)
+ if state.is_a? Symbol
+ name == state
+ else
+ name == state.name
+ end
+ end
+
+ def call_action(action, record)
+ action = @options[action]
+ case action
+ when Symbol, String
+ record.send(action)
+ when Proc
+ action.call(record)
+ end
+ end
+
+ def display_name
+ @display_name ||= name.to_s.gsub(/_/, ' ').capitalize
+ end
+
+ def for_select
+ [display_name, name.to_s]
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 6b692e6a9a..7efe9901ca 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,4 +1,4 @@
-require 'active_model/observing'
+require 'active_model/core'
module ActiveModel
module Validations
diff --git a/activemodel/test/observing_test.rb b/activemodel/test/observing_test.rb
index 37291ae4c6..6e124de52f 100644
--- a/activemodel/test/observing_test.rb
+++ b/activemodel/test/observing_test.rb
@@ -1,4 +1,4 @@
-require File.join(File.dirname(__FILE__), 'test_helper')
+require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
class ObservedModel < ActiveModel::Base
class Observer
@@ -20,7 +20,7 @@ end
class Foo < ActiveModel::Base
end
-class ObservingTest < ActiveSupport::TestCase
+class ObservingTest < ActiveModel::TestCase
def setup
ObservedModel.observers.clear
end
@@ -67,7 +67,7 @@ class ObservingTest < ActiveSupport::TestCase
end
end
-class ObserverTest < ActiveSupport::TestCase
+class ObserverTest < ActiveModel::TestCase
def setup
ObservedModel.observers = :foo_observer
FooObserver.models = nil
diff --git a/activemodel/test/state_machine/machine_test.rb b/activemodel/test/state_machine/machine_test.rb
new file mode 100644
index 0000000000..34a4b384ce
--- /dev/null
+++ b/activemodel/test/state_machine/machine_test.rb
@@ -0,0 +1,28 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class MachineTestSubject
+ include ActiveModel::StateMachine
+
+ state_machine do
+ end
+
+ state_machine :initial => :foo do
+ end
+
+ state_machine :extra, :initial => :bar do
+ end
+end
+
+class StateMachineMachineTest < ActiveModel::TestCase
+ test "allows reuse of existing machines" do
+ assert_equal 2, MachineTestSubject.state_machines.size
+ end
+
+ test "sets #initial_state from :initial option" do
+ assert_equal :bar, MachineTestSubject.state_machine(:extra).initial_state
+ end
+
+ test "accesses non-default state machine" do
+ assert_kind_of ActiveModel::StateMachine::Machine, MachineTestSubject.state_machine(:extra)
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/state_machine/state_test.rb b/activemodel/test/state_machine/state_test.rb
new file mode 100644
index 0000000000..444435d271
--- /dev/null
+++ b/activemodel/test/state_machine/state_test.rb
@@ -0,0 +1,73 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class StateTestSubject
+ include ActiveModel::StateMachine
+
+ state_machine do
+ end
+end
+
+class StateTest < ActiveModel::TestCase
+ def setup
+ @name = :astate
+ @options = { :crazy_custom_key => 'key' }
+ @machine = StateTestSubject.state_machine
+ end
+
+ def new_state(options={})
+ ActiveModel::StateMachine::State.new(options.delete(:machine) || @machine, @name, @options.merge(options))
+ end
+
+ test 'sets the name' do
+ assert_equal :astate, new_state.name
+ end
+
+ test 'sets the display_name from name' do
+ assert_equal "Astate", new_state.display_name
+ end
+
+ test 'sets the display_name from options' do
+ assert_equal "A State", new_state(:display => "A State").display_name
+ end
+
+ test 'sets the options and expose them as options' do
+ assert_equal @options, new_state.options
+ end
+
+ test 'equals a symbol of the same name' do
+ assert_equal new_state, :astate
+ end
+
+ test 'equals a State of the same name' do
+ assert_equal new_state, new_state
+ end
+
+ uses_mocha 'state actions' do
+ test 'should send a message to the record for an action if the action is present as a symbol' do
+ state = new_state(:entering => :foo)
+
+ record = stub
+ record.expects(:foo)
+
+ state.call_action(:entering, record)
+ end
+
+ test 'should send a message to the record for an action if the action is present as a string' do
+ state = new_state(:entering => 'foo')
+
+ record = stub
+ record.expects(:foo)
+
+ state.call_action(:entering, record)
+ end
+
+ test 'should call a proc, passing in the record for an action if the action is present' do
+ state = new_state(:entering => Proc.new {|r| r.foobar})
+
+ record = stub
+ record.expects(:foobar)
+
+ state.call_action(:entering, record)
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/state_machine_test.rb b/activemodel/test/state_machine_test.rb
new file mode 100644
index 0000000000..e906744c77
--- /dev/null
+++ b/activemodel/test/state_machine_test.rb
@@ -0,0 +1,335 @@
+require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
+
+class StateMachineSubject
+ include ActiveModel::StateMachine
+
+ state_machine do
+ state :open, :exit => :exit
+ state :closed, :enter => :enter
+
+ #event :close, :success => :success_callback do
+ # transitions :to => :closed, :from => [:open]
+ #end
+ #
+ #event :null do
+ # transitions :to => :closed, :from => [:open], :guard => :always_false
+ #end
+ end
+
+ state_machine :bar do
+ state :read
+ state :ended
+
+ #event :foo do
+ # transitions :to => :ended, :from => [:read]
+ #end
+ end
+
+ def always_false
+ false
+ end
+
+ def success_callback
+ end
+
+ def enter
+ end
+ def exit
+ end
+end
+
+class StateMachineSubjectSubclass < StateMachineSubject
+end
+
+class StateMachineClassLevelTest < ActiveModel::TestCase
+ test 'defines a class level #state_machine method on its including class' do
+ assert StateMachineSubject.respond_to?(:state_machine)
+ end
+
+ test 'defines a class level #state_machines method on its including class' do
+ assert StateMachineSubject.respond_to?(:state_machines)
+ end
+
+ test 'class level #state_machine returns machine instance' do
+ assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machine
+ end
+
+ test 'class level #state_machine returns machine instance with given name' do
+ assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machine(:default)
+ end
+
+ test 'class level #state_machines returns hash of machine instances' do
+ assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machines[:default]
+ end
+
+ test "should return a select friendly array of states in the form of [['Friendly name', 'state_name']]" do
+ assert_equal [['Open', 'open'], ['Closed', 'closed']], StateMachineSubject.state_machine.states_for_select
+ end
+end
+
+class StateMachineInstanceLevelTest < ActiveModel::TestCase
+ def setup
+ @foo = StateMachineSubject.new
+ end
+
+ test 'defines an accessor for the current state' do
+ assert @foo.respond_to?(:current_state)
+ end
+
+ test 'defines a state querying instance method on including class' do
+ assert @foo.respond_to?(:open?)
+ end
+
+ #test 'should define an event! inance method' do
+ # assert @foo.respond_to?(:close!)
+ #end
+end
+
+class StateMachineInitialStatesTest < ActiveModel::TestCase
+ def setup
+ @foo = StateMachineSubject.new
+ end
+
+ test 'should set the initial state' do
+ assert_equal :open, @foo.current_state
+ end
+
+ #test '#open? should be initially true' do
+ # @foo.open?.should be_true
+ #end
+ #
+ #test '#closed? should be initially false' do
+ # @foo.closed?.should be_false
+ #end
+
+ test 'should use the first state defined if no initial state is given' do
+ assert_equal :read, @foo.current_state(:bar)
+ end
+end
+#
+#describe AASM, '- event firing with persistence' do
+# it 'should fire the Event' do
+# foo = Foo.new
+#
+# Foo.aasm_events[:close].should_receive(:fire).with(foo)
+# foo.close!
+# end
+#
+# it 'should update the current state' do
+# foo = Foo.new
+# foo.close!
+#
+# foo.aasm_current_state.should == :closed
+# end
+#
+# it 'should call the success callback if one was provided' do
+# foo = Foo.new
+#
+# foo.should_receive(:success_callback)
+#
+# foo.close!
+# end
+#
+# it 'should attempt to persist if aasm_write_state is defined' do
+# foo = Foo.new
+#
+# def foo.aasm_write_state
+# end
+#
+# foo.should_receive(:aasm_write_state)
+#
+# foo.close!
+# end
+#end
+#
+#describe AASM, '- event firing without persistence' do
+# it 'should fire the Event' do
+# foo = Foo.new
+#
+# Foo.aasm_events[:close].should_receive(:fire).with(foo)
+# foo.close
+# end
+#
+# it 'should update the current state' do
+# foo = Foo.new
+# foo.close
+#
+# foo.aasm_current_state.should == :closed
+# end
+#
+# it 'should attempt to persist if aasm_write_state is defined' do
+# foo = Foo.new
+#
+# def foo.aasm_write_state
+# end
+#
+# foo.should_receive(:aasm_write_state_without_persistence)
+#
+# foo.close
+# end
+#end
+#
+#describe AASM, '- persistence' do
+# it 'should read the state if it has not been set and aasm_read_state is defined' do
+# foo = Foo.new
+# def foo.aasm_read_state
+# end
+#
+# foo.should_receive(:aasm_read_state)
+#
+# foo.aasm_current_state
+# end
+#end
+#
+#describe AASM, '- getting events for a state' do
+# it '#aasm_events_for_current_state should use current state' do
+# foo = Foo.new
+# foo.should_receive(:aasm_current_state)
+# foo.aasm_events_for_current_state
+# end
+#
+# it '#aasm_events_for_current_state should use aasm_events_for_state' do
+# foo = Foo.new
+# foo.stub!(:aasm_current_state).and_return(:foo)
+# foo.should_receive(:aasm_events_for_state).with(:foo)
+# foo.aasm_events_for_current_state
+# end
+#end
+#
+#describe AASM, '- event callbacks' do
+# it 'should call aasm_event_fired if defined and successful for bang fire' do
+# foo = Foo.new
+# def foo.aasm_event_fired(from, to)
+# end
+#
+# foo.should_receive(:aasm_event_fired)
+#
+# foo.close!
+# end
+#
+# it 'should call aasm_event_fired if defined and successful for non-bang fire' do
+# foo = Foo.new
+# def foo.aasm_event_fired(from, to)
+# end
+#
+# foo.should_receive(:aasm_event_fired)
+#
+# foo.close
+# end
+#
+# it 'should call aasm_event_failed if defined and transition failed for bang fire' do
+# foo = Foo.new
+# def foo.aasm_event_failed(event)
+# end
+#
+# foo.should_receive(:aasm_event_failed)
+#
+# foo.null!
+# end
+#
+# it 'should call aasm_event_failed if defined and transition failed for non-bang fire' do
+# foo = Foo.new
+# def foo.aasm_event_failed(event)
+# end
+#
+# foo.should_receive(:aasm_event_failed)
+#
+# foo.null
+# end
+#end
+#
+#describe AASM, '- state actions' do
+# it "should call enter when entering state" do
+# foo = Foo.new
+# foo.should_receive(:enter)
+#
+# foo.close
+# end
+#
+# it "should call exit when exiting state" do
+# foo = Foo.new
+# foo.should_receive(:exit)
+#
+# foo.close
+# end
+#end
+#
+#
+class StateMachineInheritanceTest < ActiveModel::TestCase
+ test "should have the same states as it's parent" do
+ assert_equal StateMachineSubject.state_machine.states, StateMachineSubjectSubclass.state_machine.states
+ end
+
+ #test "should have the same events as it's parent" do
+ # StateMachineSubjectSubclass.aasm_events.should == Bar.aasm_events
+ #end
+end
+#
+#
+#class ChetanPatil
+# include AASM
+# aasm_initial_state :sleeping
+# aasm_state :sleeping
+# aasm_state :showering
+# aasm_state :working
+# aasm_state :dating
+#
+# aasm_event :wakeup do
+# transitions :from => :sleeping, :to => [:showering, :working]
+# end
+#
+# aasm_event :dress do
+# transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes
+# transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) }
+# end
+#
+# def wear_clothes(shirt_color, trouser_type)
+# end
+#end
+#
+#
+#describe ChetanPatil do
+# it 'should transition to specified next state (sleeping to showering)' do
+# cp = ChetanPatil.new
+# cp.wakeup! :showering
+#
+# cp.aasm_current_state.should == :showering
+# end
+#
+# it 'should transition to specified next state (sleeping to working)' do
+# cp = ChetanPatil.new
+# cp.wakeup! :working
+#
+# cp.aasm_current_state.should == :working
+# end
+#
+# it 'should transition to default (first or showering) state' do
+# cp = ChetanPatil.new
+# cp.wakeup!
+#
+# cp.aasm_current_state.should == :showering
+# end
+#
+# it 'should transition to default state when on_transition invoked' do
+# cp = ChetanPatil.new
+# cp.dress!(nil, 'purple', 'dressy')
+#
+# cp.aasm_current_state.should == :working
+# end
+#
+# it 'should call on_transition method with args' do
+# cp = ChetanPatil.new
+# cp.wakeup! :showering
+#
+# cp.should_receive(:wear_clothes).with('blue', 'jeans')
+# cp.dress! :working, 'blue', 'jeans'
+# end
+#
+# it 'should call on_transition proc' do
+# cp = ChetanPatil.new
+# cp.wakeup! :showering
+#
+# cp.should_receive(:wear_clothes).with('purple', 'slacks')
+# cp.dress!(:dating, 'purple', 'slacks')
+# end
+#end \ No newline at end of file
diff --git a/activemodel/test/test_helper.rb b/activemodel/test/test_helper.rb
index 8e608fa0bc..ccf93280ec 100644
--- a/activemodel/test/test_helper.rb
+++ b/activemodel/test/test_helper.rb
@@ -3,7 +3,8 @@ $:.unshift File.dirname(__FILE__)
require 'test/unit'
require 'active_model'
-require 'active_support/callbacks' # needed by ActiveSupport::TestCase
+require 'active_model/state_machine'
+require 'active_support/callbacks' # needed by ActiveModel::TestCase
require 'active_support/test_case'
def uses_gem(gem_name, test_name, version = '> 0')
@@ -21,3 +22,18 @@ unless defined? uses_mocha
uses_gem('mocha', test_name, '>= 0.5.5', &block)
end
end
+
+begin
+ require 'rubygems'
+ require 'ruby-debug'
+ Debugger.start
+rescue LoadError
+end
+
+ActiveSupport::TestCase.send :include, ActiveSupport::Testing::Default
+
+module ActiveModel
+ class TestCase < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Default
+ end
+end \ No newline at end of file