mirror of
https://github.com/aasm/aasm
synced 2023-03-27 23:22:41 -04:00
Import into git
This commit is contained in:
commit
f6a8a34f06
13 changed files with 453 additions and 0 deletions
20
MIT-LICENSE
Normal file
20
MIT-LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2006 Scott Barron
|
||||||
|
|
||||||
|
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.
|
45
Rakefile
Normal file
45
Rakefile
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
require 'rake'
|
||||||
|
require 'rake/testtask'
|
||||||
|
require 'rake/rdoctask'
|
||||||
|
require 'spec/rake/spectask'
|
||||||
|
|
||||||
|
desc 'Default: run unit tests.'
|
||||||
|
task :default => [:clean_db, :test]
|
||||||
|
|
||||||
|
desc 'Remove the stale db file'
|
||||||
|
task :clean_db do
|
||||||
|
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Test the acts as state machine plugin.'
|
||||||
|
Rake::TestTask.new(:test) do |t|
|
||||||
|
t.libs << 'lib'
|
||||||
|
t.pattern = 'test/**/*_test.rb'
|
||||||
|
t.verbose = true
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Generate documentation for the acts as state machine plugin.'
|
||||||
|
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||||
|
rdoc.rdoc_dir = 'rdoc'
|
||||||
|
rdoc.title = 'Acts As State Machine'
|
||||||
|
rdoc.options << '--line-numbers --inline-source'
|
||||||
|
rdoc.rdoc_files.include('README')
|
||||||
|
rdoc.rdoc_files.include('TODO')
|
||||||
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run all examples with RCov"
|
||||||
|
Spec::Rake::SpecTask.new('cruise') do |t|
|
||||||
|
t.spec_files = FileList['spec/*.rb']
|
||||||
|
t.rcov = true
|
||||||
|
t.rcov_opts = ['--exclude', 'spec']
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run all examples"
|
||||||
|
Spec::Rake::SpecTask.new('spec') do |t|
|
||||||
|
t.spec_files = FileList['spec/*.rb']
|
||||||
|
t.rcov = false
|
||||||
|
t.spec_opts = ['-cfs']
|
||||||
|
end
|
||||||
|
|
||||||
|
#task :default => [:cruise]
|
1
aasm.rb
Normal file
1
aasm.rb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
require File.join(File.dirname(__FILE__), 'lib', 'aasm')
|
32
lib/aasm.rb
Normal file
32
lib/aasm.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module AASM
|
||||||
|
def self.included(base) #:nodoc:
|
||||||
|
base.extend AASM::ClassMethods
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def aasm_initial_state
|
||||||
|
@aasm_initial_state
|
||||||
|
end
|
||||||
|
|
||||||
|
def aasm_initial_state=(state)
|
||||||
|
@aasm_initial_state = state
|
||||||
|
end
|
||||||
|
alias :initial_state :aasm_initial_state=
|
||||||
|
|
||||||
|
def state(name, options={})
|
||||||
|
define_method("#{name.to_s}?") do
|
||||||
|
current_state == name
|
||||||
|
end
|
||||||
|
self.aasm_initial_state = name unless self.aasm_initial_state
|
||||||
|
end
|
||||||
|
|
||||||
|
def event(name, options={}, &block)
|
||||||
|
define_method("#{name.to_s}!") do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_state
|
||||||
|
@aasm_current_state || self.class.aasm_initial_state
|
||||||
|
end
|
||||||
|
end
|
32
lib/event.rb
Normal file
32
lib/event.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require File.join(File.dirname(__FILE__), 'state_transition')
|
||||||
|
|
||||||
|
module AASM
|
||||||
|
module SupportingClasses
|
||||||
|
class Event
|
||||||
|
attr_reader :name
|
||||||
|
|
||||||
|
def initialize(name, &block)
|
||||||
|
@name = name.to_sym
|
||||||
|
@transitions = []
|
||||||
|
instance_eval(&block) if block
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_states(from)
|
||||||
|
@transitions.select { |t| t.from == from }
|
||||||
|
end
|
||||||
|
|
||||||
|
def fire(record)
|
||||||
|
next_states(record).each do |transition|
|
||||||
|
break true if transition.perform(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def transitions(trans_opts)
|
||||||
|
Array(trans_opts[:from]).each do |s|
|
||||||
|
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
lib/state.rb
Normal file
29
lib/state.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module AASM
|
||||||
|
module SupportingClasses
|
||||||
|
class State
|
||||||
|
attr_reader :name
|
||||||
|
|
||||||
|
def initialize(name, opts={})
|
||||||
|
@name, @opts = name, opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def entering(record)
|
||||||
|
enteract = @opts[:enter]
|
||||||
|
record.send(:run_transition_action, enteract) if enteract
|
||||||
|
end
|
||||||
|
|
||||||
|
def entered(record)
|
||||||
|
afteractions = @opts[:after]
|
||||||
|
return unless afteractions
|
||||||
|
Array(afteractions).each do |afteract|
|
||||||
|
record.send(:run_transition_action, afteract)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def exited(record)
|
||||||
|
exitact = @opts[:exit]
|
||||||
|
record.send(:run_transition_action, exitact) if exitact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
lib/state_factory.rb
Normal file
14
lib/state_factory.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module AASM
|
||||||
|
module SupportingClasses
|
||||||
|
class StateFactory
|
||||||
|
def self.create(name, opts={})
|
||||||
|
@states ||= {}
|
||||||
|
@states[name] ||= State.new(name, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.[](name)
|
||||||
|
@states[name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
lib/state_transition.rb
Normal file
40
lib/state_transition.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
module AASM
|
||||||
|
module SupportingClasses
|
||||||
|
class StateTransition
|
||||||
|
attr_reader :from, :to, :opts
|
||||||
|
|
||||||
|
def initialize(opts)
|
||||||
|
@from, @to, @guard = opts[:from], opts[:to], opts[:guard]
|
||||||
|
@opts = opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def guard(obj)
|
||||||
|
# TODO should probably not be using obj
|
||||||
|
@guard ? obj.send(:run_transition_action, @guard) : true
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(obj)
|
||||||
|
# TODO should probably not be using obj
|
||||||
|
return false unless guard(obj)
|
||||||
|
loopback = obj.current_state == to
|
||||||
|
# TODO Maybe State should be a factory?
|
||||||
|
# State[:open] => returns same instance of State.new(:open)
|
||||||
|
next_state = StateFactory[to]
|
||||||
|
old_state = StateFactory[obj.current_state]
|
||||||
|
old_state = states[obj.current_state]
|
||||||
|
|
||||||
|
next_state.entering(obj) unless loopback
|
||||||
|
|
||||||
|
obj.update_attribute(obj.class.state_column, to.to_s)
|
||||||
|
|
||||||
|
next_state.entered(obj) unless loopback
|
||||||
|
old_state.exited(obj) unless loopback
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(obj)
|
||||||
|
@from == obj.from && @to == obj.to
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
77
spec/aasm_spec.rb
Normal file
77
spec/aasm_spec.rb
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'aasm')
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state')
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state_factory')
|
||||||
|
|
||||||
|
class Foo
|
||||||
|
include AASM
|
||||||
|
initial_state :open
|
||||||
|
state :open
|
||||||
|
state :closed
|
||||||
|
|
||||||
|
event :close do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Bar
|
||||||
|
include AASM
|
||||||
|
state :read
|
||||||
|
state :ended
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
describe AASM, '- class level definitions' do
|
||||||
|
it 'should define a class level initial_state() method on its including class' do
|
||||||
|
Foo.should respond_to(:initial_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should define a class level state() method on its including class' do
|
||||||
|
Foo.should respond_to(:state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should define a class level event() method on its including class' do
|
||||||
|
Foo.should respond_to(:event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe AASM, '- instance level definitions' do
|
||||||
|
before(:each) do
|
||||||
|
@foo = Foo.new
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should define a state querying instance method on including class' do
|
||||||
|
@foo.should respond_to(:open?)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should define an event! inance method' do
|
||||||
|
@foo.should respond_to(:close!)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO This isn't necessarily "in play" just yet
|
||||||
|
#it 'using the state macro should create a new State object' do
|
||||||
|
# AASM::SupportingClasses::State.should_receive(:new).with(:open, {})
|
||||||
|
# Foo.state :open
|
||||||
|
#end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe AASM, '- initial states' do
|
||||||
|
before(:each) do
|
||||||
|
@foo = Foo.new
|
||||||
|
@bar = Bar.new
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should set the initial state' do
|
||||||
|
@foo.current_state.should == :open
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#open? should be initially true' do
|
||||||
|
@foo.open?.should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#closed? should be initially false' do
|
||||||
|
@foo.closed?.should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should use the first state defined if no initial state is given' do
|
||||||
|
@bar.current_state.should == :read
|
||||||
|
end
|
||||||
|
end
|
41
spec/event_spec.rb
Normal file
41
spec/event_spec.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'event')
|
||||||
|
|
||||||
|
describe AASM::SupportingClasses::Event do
|
||||||
|
before(:each) do
|
||||||
|
@name = :close_order
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_event
|
||||||
|
@event = AASM::SupportingClasses::Event.new(@name) do
|
||||||
|
transitions :to => :closed, :from => [:open, :received]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should set the name' do
|
||||||
|
new_event
|
||||||
|
@event.name.should == @name
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'create StateTransitions' do
|
||||||
|
AASM::SupportingClasses::StateTransition.should_receive(:new).with({:to => :closed, :from => :open})
|
||||||
|
AASM::SupportingClasses::StateTransition.should_receive(:new).with({:to => :closed, :from => :received})
|
||||||
|
new_event
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return an array of the next possible transitions for a state' do
|
||||||
|
new_event
|
||||||
|
@event.next_states(:open).size.should == 1
|
||||||
|
@event.next_states(:received).size.should == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#fire should run #perform on each state transition' do
|
||||||
|
st = mock('StateTransition')
|
||||||
|
st.should_receive(:perform)
|
||||||
|
|
||||||
|
new_event
|
||||||
|
|
||||||
|
@event.stub!(:next_states).and_return([st])
|
||||||
|
@event.fire(:closed)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
40
spec/state_factory_spec.rb
Normal file
40
spec/state_factory_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state')
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state_factory')
|
||||||
|
|
||||||
|
describe AASM::SupportingClasses::StateFactory, '- when creating a new State' do
|
||||||
|
before(:each) do
|
||||||
|
@state = :scott
|
||||||
|
@opts = {:a => 'b'}
|
||||||
|
|
||||||
|
AASM::SupportingClasses::StateFactory.create(@state, @opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should create a new State if it has not been created yet' do
|
||||||
|
AASM::SupportingClasses::State.should_receive(:new).with(:foo, :bar => 'baz')
|
||||||
|
|
||||||
|
AASM::SupportingClasses::StateFactory.create(:foo, :bar => 'baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not create a new State if it has already been created' do
|
||||||
|
AASM::SupportingClasses::State.should_not_receive(:new).with(@state, @opts)
|
||||||
|
|
||||||
|
AASM::SupportingClasses::StateFactory.create(@state, @opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe AASM::SupportingClasses::StateFactory, '- when retrieving a State via []' do
|
||||||
|
before(:each) do
|
||||||
|
@state_name = :scottb
|
||||||
|
@opts = {:a => 'b'}
|
||||||
|
|
||||||
|
AASM::SupportingClasses::StateFactory.create(@state_name, @opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return nil if the State was never created' do
|
||||||
|
AASM::SupportingClasses::StateFactory[:foo].should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return the State' do
|
||||||
|
AASM::SupportingClasses::StateFactory[@state_name].should_not be_nil
|
||||||
|
end
|
||||||
|
end
|
69
spec/state_spec.rb
Normal file
69
spec/state_spec.rb
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TODO These are specs ported from original aasm
|
||||||
|
describe AASM::SupportingClasses::State do
|
||||||
|
before(:each) do
|
||||||
|
@name = :astate
|
||||||
|
@options = {}
|
||||||
|
@record = mock('record')
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_state
|
||||||
|
@state = AASM::SupportingClasses::State.new(@name, @options)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should set the name' do
|
||||||
|
new_state
|
||||||
|
|
||||||
|
@state.name.should == :astate
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#entering should not run_transition_action if :enter option is not passed' do
|
||||||
|
new_state
|
||||||
|
@record.should_not_receive(:run_transition_action)
|
||||||
|
|
||||||
|
@state.entering(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#entered should not run_transition_action if :after option is not passed' do
|
||||||
|
new_state
|
||||||
|
@record.should_not_receive(:run_transition_action)
|
||||||
|
|
||||||
|
@state.entered(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#exited should not run_transition_action if :exit option is not passed' do
|
||||||
|
new_state
|
||||||
|
@record.should_not_receive(:run_transition_action)
|
||||||
|
|
||||||
|
@state.exited(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#entering should run_transition_action when :enter option is passed' do
|
||||||
|
@options[:enter] = true
|
||||||
|
new_state
|
||||||
|
@record.should_receive(:run_transition_action).with(true)
|
||||||
|
|
||||||
|
@state.entering(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#entered should run_transition_action for each option when :after option is passed' do
|
||||||
|
@options[:after] = ['a', 'b']
|
||||||
|
new_state
|
||||||
|
@record.should_receive(:run_transition_action).once.with('a')
|
||||||
|
@record.should_receive(:run_transition_action).once.with('b')
|
||||||
|
|
||||||
|
@state.entered(@record)
|
||||||
|
end
|
||||||
|
|
||||||
|
it '#exited should run_transition_action when :exit option is passed' do
|
||||||
|
@options[:exit] = true
|
||||||
|
new_state
|
||||||
|
@record.should_receive(:run_transition_action).with(true)
|
||||||
|
|
||||||
|
@state.exited(@record)
|
||||||
|
end
|
||||||
|
end
|
13
spec/state_transition_spec.rb
Normal file
13
spec/state_transition_spec.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'state_transition')
|
||||||
|
|
||||||
|
describe AASM::SupportingClasses::StateTransition do
|
||||||
|
it 'should set from, to, and opts attr readers' do
|
||||||
|
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
|
||||||
|
st = AASM::SupportingClasses::StateTransition.new(opts)
|
||||||
|
|
||||||
|
st.from.should == opts[:from]
|
||||||
|
st.to.should == opts[:to]
|
||||||
|
st.opts.should == opts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in a new issue