migrating to aasm - code part

This commit is contained in:
Stefan Richter 2011-05-16 15:42:47 +08:00 committed by Reinier Balt
parent 65e3a8ff30
commit 00819ce27b
54 changed files with 2656 additions and 839 deletions

View file

@ -89,9 +89,6 @@ class TodosController < ApplicationController
specified_state = @todo.state
@saved = @todo.save
# Fix for #977 because AASM overrides @state on creation
@todo.update_attribute('state', specified_state) unless specified_state == "immediate" || specified_state.nil? || !@saved
@saved = @todo.save
@todo.update_state_from_project if @saved
unless (@saved == false) || tag_list.blank?

View file

@ -40,23 +40,27 @@ class Project < ActiveRecord::Base
validates_does_not_contain :name, :string => ','
acts_as_list :scope => 'user_id = #{user_id} AND state = \'#{state}\''
acts_as_state_machine :initial => :active, :column => 'state'
include AASM
aasm_column :state
aasm_initial_state :active
extend NamePartFinder
#include Tracks::TodoList
state :active
state :hidden, :enter => :hide_todos, :exit => :unhide_todos
state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil }
aasm_state :active
aasm_state :hidden, :enter => :hide_todos, :exit => :unhide_todos
aasm_state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil }
event :activate do
transitions :to => :active, :from => [:hidden, :completed]
aasm_event :activate do
transitions :to => :active, :from => [:active, :hidden, :completed]
end
event :hide do
aasm_event :hide do
transitions :to => :hidden, :from => [:active, :completed]
end
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active, :hidden]
end
@ -106,7 +110,7 @@ class Project < ActiveRecord::Base
# as a result of acts_as_state_machine calling state=() to update the attribute
def transition_to(candidate_state)
case candidate_state.to_sym
when current_state
when aasm_current_state
return
when :hidden
hide!

View file

@ -10,20 +10,22 @@ class RecurringTodo < ActiveRecord::Base
named_scope :completed, :conditions => { :state => 'completed'}
attr_protected :user
acts_as_state_machine :initial => :active, :column => 'state'
state :active, :enter => Proc.new { |t|
include AASM
aasm_column :state
aasm_initial_state :active
aasm_state :active, :enter => Proc.new { |t|
t[:show_from], t.completed_at = nil, nil
t.occurences_count = 0
}
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
aasm_state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active]
end
event :activate do
aasm_event :activate do
transitions :to => :active, :from => [:completed]
end

View file

@ -45,40 +45,42 @@ class Todo < ActiveRecord::Base
RE_PARTS = /'(#{RE_TODO})'\s<'(#{RE_CONTEXT})';\s'(#{RE_PROJECT})'>/ # results in array
RE_SPEC = /'#{RE_TODO}'\s<'#{RE_CONTEXT}';\s'#{RE_PROJECT}'>/ # results in string
acts_as_state_machine :initial => :active, :column => 'state'
include AASM
aasm_column :state
aasm_initial_state Proc.new { |todo| (todo.show_from && (todo.show_from > todo.user.date)) ? :deferred : :active}
# when entering active state, also remove completed_at date. Looks like :exit
# of state completed is not run, see #679
state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
state :project_hidden
state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
state :deferred
state :pending
aasm_state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
aasm_state :project_hidden, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
aasm_state :completed
aasm_state :deferred
aasm_state :pending
event :defer do
aasm_event :defer do
transitions :to => :deferred, :from => [:active]
end
event :complete do
aasm_event :complete do
transitions :to => :completed, :from => [:active, :project_hidden, :deferred]
end
event :activate do
aasm_event :activate do
transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
transitions :to => :active, :from => [:pending], :guard => :no_uncompleted_predecessors_or_deferral?
transitions :to => :deferred, :from => [:pending], :guard => :no_uncompleted_predecessors?
end
event :hide do
aasm_event :hide do
transitions :to => :project_hidden, :from => [:active, :deferred]
end
event :unhide do
aasm_event :unhide do
transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
transitions :to => :active, :from => [:project_hidden]
end
event :block do
aasm_event :block do
transitions :to => :pending, :from => [:active, :deferred]
end
@ -216,6 +218,7 @@ class Todo < ActiveRecord::Base
date = user.at_midnight(date) if (date.is_a? Date)
activate! if deferred? && date.blank?
defer! if active? && !date.blank? && date > user.date
self[:show_from] = date
end
@ -224,27 +227,7 @@ class Todo < ActiveRecord::Base
def project
original_project.nil? ? Project.null_object : original_project
end
alias_method :original_set_initial_state, :set_initial_state
def set_initial_state
if show_from && (show_from > user.date)
write_attribute self.class.state_column, 'deferred'
else
original_set_initial_state
end
end
alias_method :original_run_initial_state_actions, :run_initial_state_actions
def run_initial_state_actions
# only run the initial state actions if the standard initial state hasn't
# been changed
if self.class.initial_state.to_sym == current_state
original_run_initial_state_actions
end
end
def self.feed_options(user)
{
:title => 'Tracks Actions',

View file

@ -17,7 +17,7 @@ suppress_edit_button ||= false
</div>
<div class="buttons">
<span class="grey"><%= project.current_state.to_s.upcase %></span>
<span class="grey"><%= project.aasm_current_state.to_s.upcase %></span>
<%= link_to_delete_project(project, image_tag( "blank.png", :title => t('projects.delete_project_title'), :class=>"delete_item")) %>
<%= suppress_edit_button ? "" : link_to_edit_project(project, image_tag( "blank.png", :title => t('projects.edit_project_title'), :class=>"edit_item")) %>
</div>

View file

@ -24,4 +24,4 @@
<% else -%><%= render :partial => "notes/mobile_notes_summary", :collection => @project.notes %>
<% end -%>
<h2><%= t('projects.settings') %></h2>
<%= t('projects.state', :state => project.current_state.to_s) %>. <%= @project_default_context %>
<%= t('projects.state', :state => project.aasm_current_state.to_s) %>. <%= @project_default_context %>

View file

@ -1,11 +1,11 @@
# TODO: is this dead code?
page.select('#project_status .active span').each do |element|
element.className = @project.current_state == :active ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :active ? 'active_state' : 'inactive_state'
end
page.select('#project_status .hidden span').each do |element|
element.className = @project.current_state == :hidden ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :hidden ? 'active_state' : 'inactive_state'
end
page.select('#project_status .completed span').each do |element|
element.className = @project.current_state == :completed ? 'active_state' : 'inactive_state'
element.className = @project.aasm_current_state == :completed ? 'active_state' : 'inactive_state'
end
page.notify :notice, "Set project status to #{@project.current_state}", 5.0
page.notify :notice, "Set project status to #{@project.aasm_current_state}", 5.0

View file

@ -58,7 +58,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
login_as(:admin_user)
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"}
todos.each do |t|
assert_equal :project_hidden, t.reload().current_state
assert_equal :project_hidden, t.reload().aasm_current_state
end
assert p.reload().hidden?
end
@ -70,7 +70,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"}
xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"active"}
todos.each do |t|
assert_equal :active, t.reload().current_state
assert_equal :active, t.reload().aasm_current_state
end
assert p.reload().active?
end

View file

@ -53,26 +53,26 @@ class ProjectTest < ActiveSupport::TestCase
end
def test_project_initial_state_is_active
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
assert @timemachine.active?
end
def test_hide_project
@timemachine.hide!
assert_equal :hidden, @timemachine.current_state
assert_equal :hidden, @timemachine.aasm_current_state
assert @timemachine.hidden?
end
def test_activate_project
@timemachine.activate!
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
assert @timemachine.active?
end
def test_complete_project
assert_nil @timemachine.completed_at
@timemachine.complete!
assert_equal :completed, @timemachine.current_state
assert_equal :completed, @timemachine.aasm_current_state
assert @timemachine.completed?
assert_not_nil @timemachine.completed_at, "completed_at not expected to be nil"
assert_in_delta Time.now, @timemachine.completed_at, 1
@ -141,25 +141,27 @@ class ProjectTest < ActiveSupport::TestCase
end
def test_transition_to_another_state
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
@timemachine.transition_to(:hidden)
assert_equal :hidden, @timemachine.current_state
assert_equal :hidden, @timemachine.aasm_current_state
@timemachine.transition_to(:completed)
assert_equal :completed, @timemachine.current_state
assert_equal :completed, @timemachine.aasm_current_state
@timemachine.transition_to(:active)
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
end
def test_transition_to_same_state
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
@timemachine.transition_to(:active)
assert_equal :active, @timemachine.current_state
assert_equal :active, @timemachine.aasm_current_state
end
def test_deferred_todo_count
assert_equal 1, @timemachine.deferred_todos.count
assert_equal 0, @moremoney.deferred_todos.count
@moremoney.todos[0].show_from = next_week
@moremoney.todos[0].save
assert_equal :deferred, @moremoney.todos[0].aasm_current_state
assert_equal 1, @moremoney.deferred_todos.count
end

View file

@ -276,11 +276,11 @@ class RecurringTodoTest < ActiveSupport::TestCase
def test_toggle_completion
t = @yearly
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
t.toggle_completion!
assert_equal :completed, t.current_state
assert_equal :completed, t.aasm_current_state
t.toggle_completion!
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
end
def test_starred

View file

@ -77,10 +77,10 @@ class TodoTest < ActiveSupport::TestCase
def test_defer_an_existing_todo
@not_completed2
assert_equal :active, @not_completed2.current_state
assert_equal :active, @not_completed2.aasm_current_state
@not_completed2.show_from = next_week
assert @not_completed2.save, "should have saved successfully" + @not_completed2.errors.to_xml
assert_equal :deferred, @not_completed2.current_state
assert_equal :deferred, @not_completed2.aasm_current_state
end
def test_create_a_new_deferred_todo
@ -88,16 +88,16 @@ class TodoTest < ActiveSupport::TestCase
todo = user.todos.build
todo.show_from = next_week
todo.context_id = 1
todo.description = 'foo'
todo.description = 'foo'
assert todo.save, "should have saved successfully" + todo.errors.to_xml
assert_equal :deferred, todo.current_state
assert_equal :deferred, todo.aasm_current_state
end
def test_create_a_new_deferred_todo_by_passing_attributes
user = users(:other_user)
todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo')
todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo')
assert todo.save, "should have saved successfully" + todo.errors.to_xml
assert_equal :deferred, todo.current_state
assert_equal :deferred, todo.aasm_current_state
end
def test_feed_options
@ -108,11 +108,11 @@ class TodoTest < ActiveSupport::TestCase
def test_toggle_completion
t = @not_completed1
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
t.toggle_completion!
assert_equal :completed, t.current_state
assert_equal :completed, t.aasm_current_state
t.toggle_completion!
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
end
def test_activate_also_saves
@ -154,7 +154,7 @@ class TodoTest < ActiveSupport::TestCase
t.context_id = 1
t.save!
t.reload
assert_equal :active, t.current_state
assert_equal :active, t.aasm_current_state
end
def test_initial_state_is_deferred_when_show_from_in_future
@ -165,7 +165,7 @@ class TodoTest < ActiveSupport::TestCase
t.show_from = 1.week.from_now.to_date
t.save!
t.reload
assert_equal :deferred, t.current_state
assert_equal :deferred, t.aasm_current_state
end
def test_todo_is_not_starred

5
vendor/gems/aasm-2.2.0/.document vendored Normal file
View file

@ -0,0 +1,5 @@
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE

7
vendor/gems/aasm-2.2.0/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*.sw?
*~
.DS_Store
.idea
coverage
pkg
rdoc

137
vendor/gems/aasm-2.2.0/README.rdoc vendored Normal file
View file

@ -0,0 +1,137 @@
= Alternatives
If you are looking for an alternative to AASM, I would highly suggest transitions: http://github.com/qoobaa/transitions
= AASM - Ruby state machines
This package contains AASM, a library for adding finite state machines to Ruby classes.
AASM started as the acts_as_state_machine plugin but has evolved into a more generic library that no longer targets only ActiveRecord models.
AASM has the following features:
* States
* Machines
* Events
* Transitions
== New Callbacks
The callback chain & order on a successful event looks like:
oldstate:exit*
event:before
__find transition, if possible__
transition:on_transition*
oldstate:before_exit
newstate:before_enter
newstate:enter*
__update state__
event:success*
oldstate:after_exit
newstate:after_enter
event:after
obj:aasm_event_fired*
(*) marks old callbacks
== Download
The latest AASM can currently be pulled from the git repository on github.
* http://github.com/rubyist/aasm/tree/master
== Installation
=== From gemcutter
% sudo gem install gemcutter
% sudo gem tumble
% sudo gem install aasm
=== From GitHub hosted gems (only older releases are available)
% sudo gem sources -a http://gems.github.com # (you only need to do this once)
% sudo gem install rubyist-aasm
=== Building your own gems
% rake gemspec
% rake build
% sudo gem install pkg/aasm-2.1.gem
== Simple Example
Here's a quick example highlighting some of the features.
class Conversation
include AASM
aasm_column :current_state # defaults to aasm_state
aasm_initial_state :unread
aasm_state :unread
aasm_state :read
aasm_state :closed
aasm_event :view do
transitions :to => :read, :from => [:unread]
end
aasm_event :close do
transitions :to => :closed, :from => [:read, :unread]
end
end
== A Slightly More Complex Example
This example uses a few of the more complex features available.
class Relationship
include AASM
aasm_column :status
aasm_initial_state Proc.new { |relationship| relationship.strictly_for_fun? ? :intimate : :dating }
aasm_state :dating, :enter => :make_happy, :exit => :make_depressed
aasm_state :intimate, :enter => :make_very_happy, :exit => :never_speak_again
aasm_state :married, :enter => :give_up_intimacy, :exit => :buy_exotic_car_and_wear_a_combover
aasm_event :get_intimate do
transitions :to => :intimate, :from => [:dating], :guard => :drunk?
end
aasm_event :get_married do
transitions :to => :married, :from => [:dating, :intimate], :guard => :willing_to_give_up_manhood?
end
def strictly_for_fun?; end
def drunk?; end
def willing_to_give_up_manhood?; end
def make_happy; end
def make_depressed; end
def make_very_happy; end
def never_speak_again; end
def give_up_intimacy; end
def buy_exotic_car_and_wear_a_combover; end
end
= Other Stuff
Author:: Scott Barron <scott at elitists dot net>
License:: Original code Copyright 2006, 2007, 2008 by Scott Barron.
Released under an MIT-style license. See the LICENSE file
included in the distribution.
== Warranty
This software is provided "as is" and without any express or
implied warranties, including, without limitation, the implied
warranties of merchantibility and fitness for a particular
purpose.

108
vendor/gems/aasm-2.2.0/Rakefile vendored Normal file
View file

@ -0,0 +1,108 @@
require 'rubygems'
require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "aasm"
gem.summary = %Q{State machine mixin for Ruby objects}
gem.description = %Q{AASM is a continuation of the acts as state machine rails plugin, built for plain Ruby objects.}
gem.homepage = "http://rubyist.github.com/aasm/"
gem.authors = ["Scott Barron", "Scott Petersen", "Travis Tilley"]
gem.email = "scott@elitists.net, ttilley@gmail.com"
gem.add_development_dependency "rspec"
gem.add_development_dependency "shoulda"
gem.add_development_dependency 'sdoc'
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
end
require 'spec/rake/spectask'
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
begin
require 'rcov/rcovtask'
Rcov::RcovTask.new(:rcov_shoulda) do |test|
test.libs << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
rescue LoadError
task :rcov do
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
end
end
Spec::Rake::SpecTask.new(:spec) do |spec|
spec.libs << 'lib' << 'spec'
spec.spec_files = FileList['spec/**/*_spec.rb']
spec.spec_opts = ['-cfs']
end
Spec::Rake::SpecTask.new(:rcov_rspec) do |spec|
spec.libs << 'lib' << 'spec'
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end
task :test => :check_dependencies
task :spec => :check_dependencies
begin
require 'reek/rake_task'
Reek::RakeTask.new do |t|
t.fail_on_error = true
t.verbose = false
t.source_files = 'lib/**/*.rb'
end
rescue LoadError
task :reek do
abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
end
end
begin
require 'roodi'
require 'roodi_task'
RoodiTask.new do |t|
t.verbose = false
end
rescue LoadError
task :roodi do
abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
end
end
task :default => :test
begin
require 'rake/rdoctask'
require 'sdoc'
Rake::RDocTask.new do |rdoc|
if File.exist?('VERSION')
version = File.read('VERSION')
else
version = ""
end
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "aasm #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.options << '--fmt' << 'shtml'
rdoc.template = 'direct'
end
rescue LoadError
puts "aasm makes use of the sdoc gem. Install it with: sudo gem install sdoc"
end

1
vendor/gems/aasm-2.2.0/VERSION vendored Normal file
View file

@ -0,0 +1 @@
2.2.0

94
vendor/gems/aasm-2.2.0/aasm.gemspec vendored Normal file
View file

@ -0,0 +1,94 @@
# Generated by jeweler
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{aasm}
s.version = "2.2.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Scott Barron", "Scott Petersen", "Travis Tilley"]
s.date = %q{2010-09-13}
s.description = %q{AASM is a continuation of the acts as state machine rails plugin, built for plain Ruby objects.}
s.email = %q{scott@elitists.net, ttilley@gmail.com}
s.extra_rdoc_files = [
"LICENSE",
"README.rdoc"
]
s.files = [
".document",
".gitignore",
"LICENSE",
"README.rdoc",
"Rakefile",
"VERSION",
"aasm.gemspec",
"lib/aasm.rb",
"lib/aasm/aasm.rb",
"lib/aasm/event.rb",
"lib/aasm/persistence.rb",
"lib/aasm/persistence/active_record_persistence.rb",
"lib/aasm/state.rb",
"lib/aasm/state_machine.rb",
"lib/aasm/state_transition.rb",
"lib/aasm/supporting_classes.rb",
"spec/functional/conversation.rb",
"spec/functional/conversation_spec.rb",
"spec/spec_helper.rb",
"spec/unit/aasm_spec.rb",
"spec/unit/active_record_persistence_spec.rb",
"spec/unit/before_after_callbacks_spec.rb",
"spec/unit/event_spec.rb",
"spec/unit/state_spec.rb",
"spec/unit/state_transition_spec.rb",
"test/functional/auth_machine_test.rb",
"test/test_helper.rb",
"test/unit/aasm_test.rb",
"test/unit/event_test.rb",
"test/unit/state_test.rb",
"test/unit/state_transition_test.rb"
]
s.homepage = %q{http://rubyist.github.com/aasm/}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.7}
s.summary = %q{State machine mixin for Ruby objects}
s.test_files = [
"spec/functional/conversation.rb",
"spec/functional/conversation_spec.rb",
"spec/spec_helper.rb",
"spec/unit/aasm_spec.rb",
"spec/unit/active_record_persistence_spec.rb",
"spec/unit/before_after_callbacks_spec.rb",
"spec/unit/event_spec.rb",
"spec/unit/state_spec.rb",
"spec/unit/state_transition_spec.rb",
"test/functional/auth_machine_test.rb",
"test/test_helper.rb",
"test/unit/aasm_test.rb",
"test/unit/event_test.rb",
"test/unit/state_test.rb",
"test/unit/state_transition_test.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_development_dependency(%q<rspec>, [">= 0"])
s.add_development_dependency(%q<shoulda>, [">= 0"])
s.add_development_dependency(%q<sdoc>, [">= 0"])
else
s.add_dependency(%q<rspec>, [">= 0"])
s.add_dependency(%q<shoulda>, [">= 0"])
s.add_dependency(%q<sdoc>, [">= 0"])
end
else
s.add_dependency(%q<rspec>, [">= 0"])
s.add_dependency(%q<shoulda>, [">= 0"])
s.add_dependency(%q<sdoc>, [">= 0"])
end
end

9
vendor/gems/aasm-2.2.0/lib/aasm.rb vendored Normal file
View file

@ -0,0 +1,9 @@
module AASM
end
require 'ostruct'
require File.join(File.dirname(__FILE__), 'aasm', 'supporting_classes')
require File.join(File.dirname(__FILE__), 'aasm', 'state_machine')
require File.join(File.dirname(__FILE__), 'aasm', 'persistence')
require File.join(File.dirname(__FILE__), 'aasm', 'aasm')

196
vendor/gems/aasm-2.2.0/lib/aasm/aasm.rb vendored Normal file
View file

@ -0,0 +1,196 @@
module AASM
class InvalidTransition < RuntimeError
end
class UndefinedState < RuntimeError
end
def self.included(base) #:nodoc:
base.extend AASM::ClassMethods
AASM::Persistence.set_persistence(base)
unless AASM::StateMachine[base]
AASM::StateMachine[base] = AASM::StateMachine.new('')
end
super
end
module ClassMethods
def inherited(klass)
AASM::StateMachine[klass] = AASM::StateMachine[self].clone
super
end
def aasm_initial_state(set_state=nil)
if set_state
AASM::StateMachine[self].initial_state = set_state
else
AASM::StateMachine[self].initial_state
end
end
def aasm_initial_state=(state)
AASM::StateMachine[self].initial_state = state
end
def aasm_state(name, options={})
sm = AASM::StateMachine[self]
sm.create_state(name, options)
sm.initial_state = name unless sm.initial_state
define_method("#{name.to_s}?") do
aasm_current_state == name
end
end
def aasm_event(name, options = {}, &block)
sm = AASM::StateMachine[self]
unless sm.events.has_key?(name)
sm.events[name] = AASM::SupportingClasses::Event.new(name, options, &block)
end
define_method("#{name.to_s}!") do |*args|
aasm_fire_event(name, true, *args)
end
define_method("#{name.to_s}") do |*args|
aasm_fire_event(name, false, *args)
end
end
def aasm_states
AASM::StateMachine[self].states
end
def aasm_events
AASM::StateMachine[self].events
end
def aasm_states_for_select
AASM::StateMachine[self].states.map { |state| state.for_select }
end
end
# Instance methods
def aasm_current_state
return @aasm_current_state if @aasm_current_state
if self.respond_to?(:aasm_read_state) || self.private_methods.include?('aasm_read_state')
@aasm_current_state = aasm_read_state
end
return @aasm_current_state if @aasm_current_state
aasm_enter_initial_state
end
def aasm_enter_initial_state
state_name = aasm_determine_state_name(self.class.aasm_initial_state)
state = aasm_state_object_for_state(state_name)
state.call_action(:before_enter, self)
state.call_action(:enter, self)
self.aasm_current_state = state_name
state.call_action(:after_enter, self)
state_name
end
def aasm_events_for_current_state
aasm_events_for_state(aasm_current_state)
end
def aasm_events_for_state(state)
events = self.class.aasm_events.values.select {|event| event.transitions_from_state?(state) }
events.map {|event| event.name}
end
private
def set_aasm_current_state_with_persistence(state)
save_success = true
if self.respond_to?(:aasm_write_state) || self.private_methods.include?('aasm_write_state')
save_success = aasm_write_state(state)
end
self.aasm_current_state = state if save_success
save_success
end
def aasm_current_state=(state)
if self.respond_to?(:aasm_write_state_without_persistence) || self.private_methods.include?('aasm_write_state_without_persistence')
aasm_write_state_without_persistence(state)
end
@aasm_current_state = state
end
def aasm_determine_state_name(state)
case state
when Symbol, String
state
when Proc
state.call(self)
else
raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
end
end
def aasm_state_object_for_state(name)
obj = self.class.aasm_states.find {|s| s == name}
raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
obj
end
def aasm_fire_event(name, persist, *args)
event = self.class.aasm_events[name]
begin
old_state = aasm_state_object_for_state(aasm_current_state)
old_state.call_action(:exit, self)
# new event before callback
event.call_action(:before, self)
new_state_name = event.fire(self, *args)
unless new_state_name.nil?
new_state = aasm_state_object_for_state(new_state_name)
# new before_ callbacks
old_state.call_action(:before_exit, self)
new_state.call_action(:before_enter, self)
new_state.call_action(:enter, self)
persist_successful = true
if persist
persist_successful = set_aasm_current_state_with_persistence(new_state_name)
event.execute_success_callback(self) if persist_successful
else
self.aasm_current_state = new_state_name
end
if persist_successful
old_state.call_action(:after_exit, self)
new_state.call_action(:after_enter, self)
event.call_action(:after, self)
self.aasm_event_fired(name, old_state.name, self.aasm_current_state) if self.respond_to?(:aasm_event_fired)
else
self.aasm_event_failed(name, old_state.name) if self.respond_to?(:aasm_event_failed)
end
persist_successful
else
if self.respond_to?(:aasm_event_failed)
self.aasm_event_failed(name, old_state.name)
end
false
end
rescue StandardError => e
event.execute_error_callback(self, e)
end
end
end

109
vendor/gems/aasm-2.2.0/lib/aasm/event.rb vendored Normal file
View file

@ -0,0 +1,109 @@
class AASM::SupportingClasses::Event
attr_reader :name, :success, :options
def initialize(name, options = {}, &block)
@name = name
@transitions = []
update(options, &block)
end
def fire(obj, to_state=nil, *args)
transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}'" if transitions.size == 0
next_state = nil
transitions.each do |transition|
next if to_state and !Array(transition.to).include?(to_state)
if transition.perform(obj)
next_state = to_state || Array(transition.to).first
transition.execute(obj, *args)
break
end
end
next_state
end
def transitions_from_state?(state)
@transitions.any? { |t| t.from == state }
end
def transitions_from_state(state)
@transitions.select { |t| t.from == state }
end
def all_transitions
@transitions
end
def call_action(action, record)
action = @options[action]
action.is_a?(Array) ?
action.each {|a| _call_action(a, record)} :
_call_action(action, record)
end
def ==(event)
if event.is_a? Symbol
name == event
else
name == event.name
end
end
def update(options = {}, &block)
if options.key?(:success) then
@success = options[:success]
end
if options.key?(:error) then
@error = options[:error]
end
if block then
instance_eval(&block)
end
@options = options
self
end
def execute_success_callback(obj, success = nil)
callback = success || @success
case(callback)
when String, Symbol
obj.send(callback)
when Proc
callback.call(obj)
when Array
callback.each{|meth|self.execute_success_callback(obj, meth)}
end
end
def execute_error_callback(obj, error, error_callback=nil)
callback = error_callback || @error
raise error unless callback
case(callback)
when String, Symbol
raise NoMethodError unless obj.respond_to?(callback.to_sym)
obj.send(callback, error)
when Proc
callback.call(obj, error)
when Array
callback.each{|meth|self.execute_error_callback(obj, error, meth)}
end
end
private
def _call_action(action, record)
case action
when Symbol, String
record.send(action)
when Proc
action.call(record)
end
end
def transitions(trans_opts)
Array(trans_opts[:from]).each do |s|
@transitions << AASM::SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
end
end
end

View file

@ -0,0 +1,14 @@
module AASM::Persistence
# Checks to see this class or any of it's superclasses inherit from
# ActiveRecord::Base and if so includes ActiveRecordPersistence
def self.set_persistence(base)
# Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
hierarchy = base.ancestors.map {|klass| klass.to_s}
if hierarchy.include?("ActiveRecord::Base")
require File.join(File.dirname(__FILE__), 'persistence', 'active_record_persistence')
base.send(:include, AASM::Persistence::ActiveRecordPersistence)
end
end
end

View file

@ -0,0 +1,259 @@
module AASM
module Persistence
module ActiveRecordPersistence
# This method:
#
# * extends the model with ClassMethods
# * includes InstanceMethods
#
# Unless the corresponding methods are already defined, it includes
# * ReadState
# * WriteState
# * WriteStateWithoutPersistence
#
# Adds
#
# before_validation :aasm_ensure_initial_state, :on => :create
#
# As a result, it doesn't matter when you define your methods - the following 2 are equivalent
#
# class Foo < ActiveRecord::Base
# def aasm_write_state(state)
# "bar"
# end
# include AASM
# end
#
# class Foo < ActiveRecord::Base
# include AASM
# def aasm_write_state(state)
# "bar"
# end
# end
#
def self.included(base)
base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
base.send(:include, AASM::Persistence::ActiveRecordPersistence::ReadState) unless base.method_defined?(:aasm_read_state)
base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteState) unless base.method_defined?(:aasm_write_state)
base.send(:include, AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence) unless base.method_defined?(:aasm_write_state_without_persistence)
if base.respond_to?(:named_scope) || base.respond_to?(:scope)
base.extend(AASM::Persistence::ActiveRecordPersistence::NamedScopeMethods)
base.class_eval do
class << self
unless method_defined?(:aasm_state_without_scope)
alias_method :aasm_state_without_scope, :aasm_state
alias_method :aasm_state, :aasm_state_with_scope
end
end
end
end
if ActiveRecord::VERSION::MAJOR >= 3
base.before_validation(:aasm_ensure_initial_state, :on => :create)
else
base.before_validation_on_create(:aasm_ensure_initial_state)
end
end
module ClassMethods
# Maps to the aasm_column in the database. Defaults to "aasm_state". You can write:
#
# create_table :foos do |t|
# t.string :name
# t.string :aasm_state
# end
#
# class Foo < ActiveRecord::Base
# include AASM
# end
#
# OR:
#
# create_table :foos do |t|
# t.string :name
# t.string :status
# end
#
# class Foo < ActiveRecord::Base
# include AASM
# aasm_column :status
# end
#
# This method is both a getter and a setter
def aasm_column(column_name=nil)
if column_name
AASM::StateMachine[self].config.column = column_name.to_sym
# @aasm_column = column_name.to_sym
else
AASM::StateMachine[self].config.column ||= :aasm_state
# @aasm_column ||= :aasm_state
end
# @aasm_column
AASM::StateMachine[self].config.column
end
def find_in_state(number, state, *args)
with_state_scope state do
find(number, *args)
end
end
def count_in_state(state, *args)
with_state_scope state do
count(*args)
end
end
def calculate_in_state(state, *args)
with_state_scope state do
calculate(*args)
end
end
protected
def with_state_scope(state)
with_scope :find => {:conditions => ["#{table_name}.#{aasm_column} = ?", state.to_s]} do
yield if block_given?
end
end
end
module InstanceMethods
# Returns the current aasm_state of the object. Respects reload and
# any changes made to the aasm_state field directly
#
# Internally just calls <tt>aasm_read_state</tt>
#
# foo = Foo.find(1)
# foo.aasm_current_state # => :pending
# foo.aasm_state = "opened"
# foo.aasm_current_state # => :opened
# foo.close # => calls aasm_write_state_without_persistence
# foo.aasm_current_state # => :closed
# foo.reload
# foo.aasm_current_state # => :pending
#
def aasm_current_state
@current_state = aasm_read_state
end
private
# Ensures that if the aasm_state column is nil and the record is new
# that the initial state gets populated before validation on create
#
# foo = Foo.new
# foo.aasm_state # => nil
# foo.valid?
# foo.aasm_state # => "open" (where :open is the initial state)
#
#
# foo = Foo.find(:first)
# foo.aasm_state # => 1
# foo.aasm_state = nil
# foo.valid?
# foo.aasm_state # => nil
#
def aasm_ensure_initial_state
send("#{self.class.aasm_column}=", self.aasm_enter_initial_state.to_s) if send(self.class.aasm_column).blank?
end
end
module WriteStateWithoutPersistence
# Writes <tt>state</tt> to the state column, but does not persist it to the database
#
# foo = Foo.find(1)
# foo.aasm_current_state # => :opened
# foo.close
# foo.aasm_current_state # => :closed
# Foo.find(1).aasm_current_state # => :opened
# foo.save
# foo.aasm_current_state # => :closed
# Foo.find(1).aasm_current_state # => :closed
#
# NOTE: intended to be called from an event
def aasm_write_state_without_persistence(state)
write_attribute(self.class.aasm_column, state.to_s)
end
end
module WriteState
# Writes <tt>state</tt> to the state column and persists it to the database
#
# foo = Foo.find(1)
# foo.aasm_current_state # => :opened
# foo.close!
# foo.aasm_current_state # => :closed
# Foo.find(1).aasm_current_state # => :closed
#
# NOTE: intended to be called from an event
def aasm_write_state(state)
old_value = read_attribute(self.class.aasm_column)
write_attribute(self.class.aasm_column, state.to_s)
# see https://github.com/rubyist/aasm/issues/2
unless self.save
write_attribute(self.class.aasm_column, old_value)
# return false
return true
end
true
end
end
module ReadState
# Returns the value of the aasm_column - called from <tt>aasm_current_state</tt>
#
# If it's a new record, and the aasm state column is blank it returns the initial state:
#
# class Foo < ActiveRecord::Base
# include AASM
# aasm_column :status
# aasm_state :opened
# aasm_state :closed
# end
#
# foo = Foo.new
# foo.current_state # => :opened
# foo.close
# foo.current_state # => :closed
#
# foo = Foo.find(1)
# foo.current_state # => :opened
# foo.aasm_state = nil
# foo.current_state # => nil
#
# NOTE: intended to be called from an event
#
# This allows for nil aasm states - be sure to add validation to your model
def aasm_read_state
if new_record?
send(self.class.aasm_column).blank? ? aasm_determine_state_name(self.class.aasm_initial_state) : send(self.class.aasm_column).to_sym
else
send(self.class.aasm_column).nil? ? nil : send(self.class.aasm_column).to_sym
end
end
end
module NamedScopeMethods
def aasm_state_with_scope name, options = {}
aasm_state_without_scope name, options
unless self.respond_to?(name)
scope_options = {:conditions => { "#{table_name}.#{self.aasm_column}" => name.to_s}}
scope_method = ActiveRecord::VERSION::MAJOR >= 3 ? :scope : :named_scope
self.send(scope_method, name, scope_options)
end
end
end
end
end
end

View file

@ -0,0 +1,53 @@
class AASM::SupportingClasses::State
attr_reader :name, :options
def initialize(name, options={})
@name = name
update(options)
end
def ==(state)
if state.is_a? Symbol
name == state
else
name == state.name
end
end
def call_action(action, record)
action = @options[action]
catch :halt_aasm_chain do
action.is_a?(Array) ?
action.each {|a| _call_action(a, record)} :
_call_action(action, record)
end
end
def display_name
@display_name ||= name.to_s.gsub(/_/, ' ').capitalize
end
def for_select
[display_name, name.to_s]
end
def update(options = {})
if options.key?(:display) then
@display_name = options.delete(:display)
end
@options = options
self
end
private
def _call_action(action, record)
case action
when Symbol, String
record.send(action)
when Proc
action.call(record)
end
end
end

View file

@ -0,0 +1,32 @@
class AASM::StateMachine
def self.[](*args)
(@machines ||= {})[args]
end
def self.[]=(*args)
val = args.pop
(@machines ||= {})[args] = val
end
attr_accessor :states, :events, :initial_state, :config
attr_reader :name
def initialize(name)
@name = name
@initial_state = nil
@states = []
@events = {}
@config = OpenStruct.new
end
def clone
klone = super
klone.states = states.clone
klone.events = events.clone
klone
end
def create_state(name, options)
@states << AASM::SupportingClasses::State.new(name, options) unless @states.include?(name)
end
end

View file

@ -0,0 +1,46 @@
class AASM::SupportingClasses::StateTransition
attr_reader :from, :to, :opts
alias_method :options, :opts
def initialize(opts)
@from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
@opts = opts
end
def perform(obj)
case @guard
when Symbol, String
obj.send(@guard)
when Proc
@guard.call(obj)
else
true
end
end
def execute(obj, *args)
@on_transition.is_a?(Array) ?
@on_transition.each {|ot| _execute(obj, ot, *args)} :
_execute(obj, @on_transition, *args)
end
def ==(obj)
@from == obj.from && @to == obj.to
end
def from?(value)
@from == value
end
private
def _execute(obj, on_transition, *args)
case on_transition
when Symbol, String
obj.send(on_transition, *args)
when Proc
on_transition.call(obj, *args)
end
end
end

View file

@ -0,0 +1,6 @@
module AASM::SupportingClasses
end
require File.join(File.dirname(__FILE__), 'state_transition')
require File.join(File.dirname(__FILE__), 'event')
require File.join(File.dirname(__FILE__), 'state')

View file

@ -0,0 +1,49 @@
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'aasm')
class Conversation
include AASM
aasm_initial_state :needs_attention
aasm_state :needs_attention
aasm_state :read
aasm_state :closed
aasm_state :awaiting_response
aasm_state :junk
aasm_event :new_message do
end
aasm_event :view do
transitions :to => :read, :from => [:needs_attention]
end
aasm_event :reply do
end
aasm_event :close do
transitions :to => :closed, :from => [:read, :awaiting_response]
end
aasm_event :junk do
transitions :to => :junk, :from => [:read]
end
aasm_event :unjunk do
end
def initialize(persister)
@persister = persister
end
private
def aasm_read_state
@persister.read_state
end
def aasm_write_state(state)
@persister.write_state(state)
end
end

View file

@ -0,0 +1,8 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
require File.expand_path(File.join(File.dirname(__FILE__), 'conversation'))
describe Conversation, 'description' do
it '.aasm_states should contain all of the states' do
Conversation.aasm_states.should == [:needs_attention, :read, :closed, :awaiting_response, :junk]
end
end

View file

@ -0,0 +1,11 @@
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
require 'aasm'
require 'spec'
require 'spec/autorun'
Spec::Runner.configure do |config|
end

View file

@ -0,0 +1,462 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
class Foo
include AASM
aasm_initial_state :open
aasm_state :open, :exit => :exit
aasm_state :closed, :enter => :enter
aasm_event :close, :success => :success_callback do
transitions :to => :closed, :from => [:open]
end
aasm_event :null do
transitions :to => :closed, :from => [:open], :guard => :always_false
end
def always_false
false
end
def success_callback
end
def enter
end
def exit
end
end
class FooTwo < Foo
include AASM
aasm_state :foo
end
class Bar
include AASM
aasm_state :read
aasm_state :ended
aasm_event :foo do
transitions :to => :ended, :from => [:read]
end
end
class Baz < Bar
end
class Banker
include AASM
aasm_initial_state Proc.new { |banker| banker.rich? ? :retired : :selling_bad_mortgages }
aasm_state :retired
aasm_state :selling_bad_mortgages
RICH = 1_000_000
attr_accessor :balance
def initialize(balance = 0); self.balance = balance; end
def rich?; self.balance >= RICH; end
end
describe AASM, '- class level definitions' do
it 'should define a class level aasm_initial_state() method on its including class' do
Foo.should respond_to(:aasm_initial_state)
end
it 'should define a class level aasm_state() method on its including class' do
Foo.should respond_to(:aasm_state)
end
it 'should define a class level aasm_event() method on its including class' do
Foo.should respond_to(:aasm_event)
end
it 'should define a class level aasm_states() method on its including class' do
Foo.should respond_to(:aasm_states)
end
it 'should define a class level aasm_states_for_select() method on its including class' do
Foo.should respond_to(:aasm_states_for_select)
end
it 'should define a class level aasm_events() method on its including class' do
Foo.should respond_to(:aasm_events)
end
end
describe AASM, '- subclassing' do
it 'should have the parent states' do
Foo.aasm_states.each do |state|
FooTwo.aasm_states.should include(state)
end
end
it 'should not add the child states to the parent machine' do
Foo.aasm_states.should_not include(:foo)
end
end
describe AASM, '- aasm_states_for_select' do
it "should return a select friendly array of states in the form of [['Friendly name', 'state_name']]" do
Foo.aasm_states_for_select.should == [['Open', 'open'], ['Closed', 'closed']]
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
end
describe AASM, '- initial states' do
before(:each) do
@foo = Foo.new
@bar = Bar.new
end
it 'should set the initial state' do
@foo.aasm_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.aasm_current_state.should == :read
end
it 'should determine initial state from the Proc results' do
Banker.new(Banker::RICH - 1).aasm_current_state.should == :selling_bad_mortgages
Banker.new(Banker::RICH + 1).aasm_current_state.should == :retired
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
it 'should return true if aasm_write_state is defined and returns true' do
foo = Foo.new
def foo.aasm_write_state(state)
true
end
foo.close!.should be_true
end
it 'should return false if aasm_write_state is defined and returns false' do
foo = Foo.new
def foo.aasm_write_state(state)
false
end
foo.close!.should be_false
end
it "should not update the aasm_current_state if the write fails" do
foo = Foo.new
def foo.aasm_write_state
false
end
foo.should_receive(:aasm_write_state)
foo.close!
foo.aasm_current_state.should == :open
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).twice
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
describe "with an error callback defined" do
before do
class Foo
aasm_event :safe_close, :success => :success_callback, :error => :error_callback do
transitions :to => :closed, :from => [:open]
end
end
@foo = Foo.new
end
it "should run error_callback if an exception is raised and error_callback defined" do
def @foo.error_callback(e)
end
@foo.stub!(:enter).and_raise(e=StandardError.new)
@foo.should_receive(:error_callback).with(e)
@foo.safe_close!
end
it "should raise NoMethodError if exceptionis raised and error_callback is declared but not defined" do
@foo.stub!(:enter).and_raise(StandardError)
lambda{@foo.safe_close!}.should raise_error(NoMethodError)
end
it "should propagate an error if no error callback is declared" do
@foo.stub!(:enter).and_raise("Cannot enter safe")
lambda{@foo.close!}.should raise_error(StandardError, "Cannot enter safe")
end
end
describe "with aasm_event_fired defined" do
before do
@foo = Foo.new
def @foo.aasm_event_fired(event, from, to)
end
end
it 'should call it for successful bang fire' do
@foo.should_receive(:aasm_event_fired).with(:close, :open, :closed)
@foo.close!
end
it 'should call it for successful non-bang fire' do
@foo.should_receive(:aasm_event_fired)
@foo.close
end
it 'should not call it for failing bang fire' do
@foo.stub!(:set_aasm_current_state_with_persistence).and_return(false)
@foo.should_not_receive(:aasm_event_fired)
@foo.close!
end
end
describe "with aasm_event_failed defined" do
before do
@foo = Foo.new
def @foo.aasm_event_failed(event, from)
end
end
it 'should call it when transition failed for bang fire' do
@foo.should_receive(:aasm_event_failed).with(:null, :open)
@foo.null!
end
it 'should call it when transition failed for non-bang fire' do
@foo.should_receive(:aasm_event_failed).with(:null, :open)
@foo.null
end
it 'should not call it if persist fails for bang fire' do
@foo.stub!(:set_aasm_current_state_with_persistence).and_return(false)
@foo.should_receive(:aasm_event_failed)
@foo.close!
end
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
describe Baz do
it "should have the same states as it's parent" do
Baz.aasm_states.should == Bar.aasm_states
end
it "should have the same events as it's parent" do
Baz.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_state :prettying_up
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) }
transitions :from => :showering, :to => :prettying_up, :on_transition => [:condition_hair, :fix_hair]
end
def wear_clothes(shirt_color, trouser_type)
end
def condition_hair
end
def fix_hair
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
it 'should call on_transition with an array of methods' do
cp = ChetanPatil.new
cp.wakeup! :showering
cp.should_receive(:condition_hair)
cp.should_receive(:fix_hair)
cp.dress!(:prettying_up)
end
end

View file

@ -0,0 +1,255 @@
begin
require 'rubygems'
require 'active_record'
require 'logger'
ActiveRecord::Base.logger = Logger.new(STDERR)
# A dummy class for mocking the activerecord connection class
class Connection
end
class FooBar < ActiveRecord::Base
include AASM
# Fake this column for testing purposes
attr_accessor :aasm_state
aasm_state :open
aasm_state :closed
aasm_event :view do
transitions :to => :read, :from => [:needs_attention]
end
end
class Fi < ActiveRecord::Base
def aasm_read_state
"fi"
end
include AASM
end
class Fo < ActiveRecord::Base
def aasm_write_state(state)
"fo"
end
include AASM
end
class Fum < ActiveRecord::Base
def aasm_write_state_without_persistence(state)
"fum"
end
include AASM
end
class June < ActiveRecord::Base
include AASM
aasm_column :status
end
class Beaver < June
end
class Thief < ActiveRecord::Base
include AASM
aasm_initial_state Proc.new { |thief| thief.skilled ? :rich : :jailed }
aasm_state :rich
aasm_state :jailed
attr_accessor :skilled, :aasm_state
end
describe "aasm model", :shared => true do
it "should include AASM::Persistence::ActiveRecordPersistence" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence)
end
it "should include AASM::Persistence::ActiveRecordPersistence::InstanceMethods" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
end
end
describe FooBar, "class methods" do
before(:each) do
@klass = FooBar
end
it_should_behave_like "aasm model"
it "should include AASM::Persistence::ActiveRecordPersistence::ReadState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::ReadState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence)
end
end
describe Fi, "class methods" do
before(:each) do
@klass = Fi
end
it_should_behave_like "aasm model"
it "should not include AASM::Persistence::ActiveRecordPersistence::ReadState" do
@klass.included_modules.should_not be_include(AASM::Persistence::ActiveRecordPersistence::ReadState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence)
end
end
describe Fo, "class methods" do
before(:each) do
@klass = Fo
end
it_should_behave_like "aasm model"
it "should include AASM::Persistence::ActiveRecordPersistence::ReadState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::ReadState)
end
it "should not include AASM::Persistence::ActiveRecordPersistence::WriteState" do
@klass.included_modules.should_not be_include(AASM::Persistence::ActiveRecordPersistence::WriteState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence)
end
end
describe Fum, "class methods" do
before(:each) do
@klass = Fum
end
it_should_behave_like "aasm model"
it "should include AASM::Persistence::ActiveRecordPersistence::ReadState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::ReadState)
end
it "should include AASM::Persistence::ActiveRecordPersistence::WriteState" do
@klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::WriteState)
end
it "should not include AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence" do
@klass.included_modules.should_not be_include(AASM::Persistence::ActiveRecordPersistence::WriteStateWithoutPersistence)
end
end
describe FooBar, "instance methods" do
before(:each) do
connection = mock(Connection, :columns => [])
FooBar.stub!(:connection).and_return(connection)
end
it "should respond to aasm read state when not previously defined" do
FooBar.new.should respond_to(:aasm_read_state)
end
it "should respond to aasm write state when not previously defined" do
FooBar.new.should respond_to(:aasm_write_state)
end
it "should respond to aasm write state without persistence when not previously defined" do
FooBar.new.should respond_to(:aasm_write_state_without_persistence)
end
it "should return the initial state when new and the aasm field is nil" do
FooBar.new.aasm_current_state.should == :open
end
it "should return the aasm column when new and the aasm field is not nil" do
foo = FooBar.new
foo.aasm_state = "closed"
foo.aasm_current_state.should == :closed
end
it "should return the aasm column when not new and the aasm_column is not nil" do
foo = FooBar.new
foo.stub!(:new_record?).and_return(false)
foo.aasm_state = "state"
foo.aasm_current_state.should == :state
end
it "should allow a nil state" do
foo = FooBar.new
foo.stub!(:new_record?).and_return(false)
foo.aasm_state = nil
foo.aasm_current_state.should be_nil
end
it "should have aasm_ensure_initial_state" do
foo = FooBar.new
foo.send :aasm_ensure_initial_state
end
it "should call aasm_ensure_initial_state on validation before create" do
foo = FooBar.new
foo.should_receive(:aasm_ensure_initial_state).and_return(true)
foo.valid?
end
it "should call aasm_ensure_initial_state on validation before create" do
foo = FooBar.new
foo.stub!(:new_record?).and_return(false)
foo.should_not_receive(:aasm_ensure_initial_state)
foo.valid?
end
end
describe 'Beavers' do
it "should have the same states as it's parent" do
Beaver.aasm_states.should == June.aasm_states
end
it "should have the same events as it's parent" do
Beaver.aasm_events.should == June.aasm_events
end
it "should have the same column as it's parent" do
Beaver.aasm_column.should == :status
end
end
describe AASM::Persistence::ActiveRecordPersistence::NamedScopeMethods do
class NamedScopeExample < ActiveRecord::Base
include AASM
end
context "Does not already respond_to? the scope name" do
it "should add a scope" do
NamedScopeExample.aasm_state :unknown_scope
NamedScopeExample.scopes.keys.should include(:unknown_scope)
end
end
context "Already respond_to? the scope name" do
it "should not add a scope" do
NamedScopeExample.aasm_state :new
NamedScopeExample.scopes.keys.should_not include(:new)
end
end
end
describe 'Thieves' do
before(:each) do
connection = mock(Connection, :columns => [])
Thief.stub!(:connection).and_return(connection)
end
it 'should be rich if they\'re skilled' do
Thief.new(:skilled => true).aasm_current_state.should == :rich
end
it 'should be jailed if they\'re unskilled' do
Thief.new(:skilled => false).aasm_current_state.should == :jailed
end
end
# TODO: figure out how to test ActiveRecord reload! without a database
rescue LoadError => e
if e.message == "no such file to load -- active_record"
puts "You must install active record to run this spec. Install with sudo gem install activerecord"
else
raise
end
end

View file

@ -0,0 +1,79 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
class Foo2
include AASM
aasm_initial_state :open
aasm_state :open,
:before_enter => :before_enter_open,
:before_exit => :before_exit_open,
:after_enter => :after_enter_open,
:after_exit => :after_exit_open
aasm_state :closed,
:before_enter => :before_enter_closed,
:before_exit => :before_exit_closed,
:after_enter => :after_enter_closed,
:after_exit => :after_exit_closed
aasm_event :close, :before => :before, :after => :after do
transitions :to => :closed, :from => [:open]
end
aasm_event :open, :before => :before, :after => :after do
transitions :to => :open, :from => :closed
end
def before_enter_open
end
def before_exit_open
end
def after_enter_open
end
def after_exit_open
end
def before_enter_closed
end
def before_exit_closed
end
def after_enter_closed
end
def after_exit_closed
end
def before
end
def after
end
end
describe Foo2, '- new callbacks' do
before(:each) do
@foo = Foo2.new
end
it "should get close callbacks" do
@foo.should_receive(:before).once.ordered
@foo.should_receive(:before_exit_open).once.ordered # these should be before the state changes
@foo.should_receive(:before_enter_closed).once.ordered
@foo.should_receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes
@foo.should_receive(:after_exit_open).once.ordered # these should be after the state changes
@foo.should_receive(:after_enter_closed).once.ordered
@foo.should_receive(:after).once.ordered
@foo.close!
end
it "should get open callbacks" do
@foo.close!
@foo.should_receive(:before).once.ordered
@foo.should_receive(:before_exit_closed).once.ordered # these should be before the state changes
@foo.should_receive(:before_enter_open).once.ordered
@foo.should_receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes
@foo.should_receive(:after_exit_closed).once.ordered # these should be after the state changes
@foo.should_receive(:after_enter_open).once.ordered
@foo.should_receive(:after).once.ordered
@foo.open!
end
end

View file

@ -0,0 +1,126 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
describe AASM::SupportingClasses::Event do
before(:each) do
@name = :close_order
@success = :success_callback
end
def new_event
@event = AASM::SupportingClasses::Event.new(@name, {:success => @success}) do
transitions :to => :closed, :from => [:open, :received]
end
end
it 'should set the name' do
new_event
@event.name.should == @name
end
it 'should set the success option' do
new_event
@event.success.should == @success
end
it 'should 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
end
describe AASM::SupportingClasses::Event, 'when firing an event' do
it 'should raise an AASM::InvalidTransition error if the transitions are empty' do
obj = mock('object')
obj.stub!(:aasm_current_state)
event = AASM::SupportingClasses::Event.new(:event)
lambda { event.fire(obj) }.should raise_error(AASM::InvalidTransition)
end
it 'should return the state of the first matching transition it finds' do
event = AASM::SupportingClasses::Event.new(:event) do
transitions :to => :closed, :from => [:open, :received]
end
obj = mock('object')
obj.stub!(:aasm_current_state).and_return(:open)
event.fire(obj).should == :closed
end
end
describe AASM::SupportingClasses::Event, 'when executing the success callback' do
class ThisNameBetterNotBeInUse
include AASM
aasm_state :initial
aasm_state :symbol
aasm_state :string
aasm_state :array
aasm_state :proc
end
it "should send the success callback if it's a symbol" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_symbol, :success => :symbol_success_callback do
transitions :to => :symbol, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:symbol_success_callback)
model.with_symbol!
end
it "should send the success callback if it's a string" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_string, :success => 'string_success_callback' do
transitions :to => :string, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:string_success_callback)
model.with_string!
end
it "should call each success callback if passed an array of strings and/or symbols" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_array, :success => [:success_callback1, 'success_callback2'] do
transitions :to => :array, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:success_callback1)
model.should_receive(:success_callback2)
model.with_array!
end
it "should call each success callback if passed an array of strings and/or symbols and/or procs" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_array_including_procs, :success => [:success_callback1, 'success_callback2', lambda { |obj| obj.proc_success_callback }] do
transitions :to => :array, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:success_callback1)
model.should_receive(:success_callback2)
model.should_receive(:proc_success_callback)
model.with_array_including_procs!
end
it "should call the success callback if it's a proc" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_proc, :success => lambda { |obj| obj.proc_success_callback } do
transitions :to => :proc, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:proc_success_callback)
model.with_proc!
end
end

View file

@ -0,0 +1,85 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
# TODO These are specs ported from original aasm
describe AASM::SupportingClasses::State do
before(:each) do
@name = :astate
@options = { :crazy_custom_key => 'key' }
end
def new_state(options={})
AASM::SupportingClasses::State.new(@name, @options.merge(options))
end
it 'should set the name' do
state = new_state
state.name.should == :astate
end
it 'should set the options and expose them as options' do
state = new_state
state.options.should == @options
end
it 'should be equal to a symbol of the same name' do
state = new_state
state.should == :astate
end
it 'should be equal to a State of the same name' do
new_state.should == new_state
end
it '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 = mock('record')
record.should_receive(:foo)
state.call_action(:entering, record)
end
it '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 = mock('record')
record.should_receive(:foo)
state.call_action(:entering, record)
end
it 'should send a message to the record for each action' do
state = new_state(:entering => [:a, :b, "c", lambda {|r| r.foobar }])
record = mock('record')
record.should_receive(:a)
record.should_receive(:b)
record.should_receive(:c)
record.should_receive(:foobar)
state.call_action(:entering, record)
end
it "should stop calling actions if one of them raises :halt_aasm_chain" do
state = new_state(:entering => [:a, :b, :c])
record = mock('record')
record.should_receive(:a)
record.should_receive(:b).and_throw(:halt_aasm_chain)
record.should_not_receive(:c)
state.call_action(:entering, record)
end
it '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 = mock('record')
record.should_receive(:foobar)
state.call_action(:entering, record)
end
end

View file

@ -0,0 +1,84 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
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
it 'should pass equality check if from and to are the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.stub!(:from).and_return(opts[:from])
obj.stub!(:to).and_return(opts[:to])
st.should == obj
end
it 'should fail equality check if from are not the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.stub!(:from).and_return('blah')
obj.stub!(:to).and_return(opts[:to])
st.should_not == obj
end
it 'should fail equality check if to are not the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.stub!(:from).and_return(opts[:from])
obj.stub!(:to).and_return('blah')
st.should_not == obj
end
end
describe AASM::SupportingClasses::StateTransition, '- when performing guard checks' do
it 'should return true of there is no guard' do
opts = {:from => 'foo', :to => 'bar'}
st = AASM::SupportingClasses::StateTransition.new(opts)
st.perform(nil).should be_true
end
it 'should call the method on the object if guard is a symbol' do
opts = {:from => 'foo', :to => 'bar', :guard => :test}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.should_receive(:test)
st.perform(obj)
end
it 'should call the method on the object if guard is a string' do
opts = {:from => 'foo', :to => 'bar', :guard => 'test'}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.should_receive(:test)
st.perform(obj)
end
it 'should call the proc passing the object if the guard is a proc' do
opts = {:from => 'foo', :to => 'bar', :guard => Proc.new {|o| o.test}}
st = AASM::SupportingClasses::StateTransition.new(opts)
obj = mock('object')
obj.should_receive(:test)
st.perform(obj)
end
end

View file

@ -0,0 +1,120 @@
require 'test_helper'
class AuthMachine
include AASM
attr_accessor :activation_code, :activated_at, :deleted_at
aasm_initial_state :pending
aasm_state :passive
aasm_state :pending, :enter => :make_activation_code
aasm_state :active, :enter => :do_activate
aasm_state :suspended
aasm_state :deleted, :enter => :do_delete, :exit => :do_undelete
aasm_event :register do
transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| u.can_register? }
end
aasm_event :activate do
transitions :from => :pending, :to => :active
end
aasm_event :suspend do
transitions :from => [:passive, :pending, :active], :to => :suspended
end
aasm_event :delete do
transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted
end
aasm_event :unsuspend do
transitions :from => :suspended, :to => :active, :guard => Proc.new {|u| u.has_activated? }
transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| u.has_activation_code? }
transitions :from => :suspended, :to => :passive
end
def initialize
# the AR backend uses a before_validate_on_create :aasm_ensure_initial_state
# lets do something similar here for testing purposes.
aasm_enter_initial_state
end
def make_activation_code
@activation_code = 'moo'
end
def do_activate
@activated_at = Time.now
@activation_code = nil
end
def do_delete
@deleted_at = Time.now
end
def do_undelete
@deleted_at = false
end
def can_register?
true
end
def has_activated?
!!@activated_at
end
def has_activation_code?
!!@activation_code
end
end
class AuthMachineTest < Test::Unit::TestCase
context 'authentication state machine' do
context 'on initialization' do
setup do
@auth = AuthMachine.new
end
should 'be in the pending state' do
assert_equal :pending, @auth.aasm_current_state
end
should 'have an activation code' do
assert @auth.has_activation_code?
assert_not_nil @auth.activation_code
end
end
context 'when being unsuspended' do
should 'be active if previously activated' do
@auth = AuthMachine.new
@auth.activate!
@auth.suspend!
@auth.unsuspend!
assert_equal :active, @auth.aasm_current_state
end
should 'be pending if not previously activated, but an activation code is present' do
@auth = AuthMachine.new
@auth.suspend!
@auth.unsuspend!
assert_equal :pending, @auth.aasm_current_state
end
should 'be passive if not previously activated and there is no activation code' do
@auth = AuthMachine.new
@auth.activation_code = nil
@auth.suspend!
@auth.unsuspend!
assert_equal :passive, @auth.aasm_current_state
end
end
end
end

View file

@ -0,0 +1,33 @@
require 'ostruct'
require 'rubygems'
begin
gem 'minitest'
rescue Gem::LoadError
puts 'minitest gem not found'
end
begin
require 'minitest/autorun'
puts 'using minitest'
rescue LoadError
require 'test/unit'
puts 'using test/unit'
end
require 'rr'
require 'shoulda'
class Test::Unit::TestCase
include RR::Adapters::TestUnit
end
begin
require 'ruby-debug'
Debugger.start
rescue LoadError
end
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'aasm'

View file

@ -0,0 +1,54 @@
require 'test_helper'
class EventTest < Test::Unit::TestCase
def new_event
@event = AASM::SupportingClasses::Event.new(@name, {:success => @success}) do
transitions :to => :closed, :from => [:open, :received]
end
end
context 'event' do
setup do
@name = :close_order
@success = :success_callback
end
should 'set the name' do
assert_equal @name, new_event.name
end
should 'set the success option' do
assert_equal @success, new_event.success
end
should 'create StateTransitions' do
mock(AASM::SupportingClasses::StateTransition).new({:to => :closed, :from => :open})
mock(AASM::SupportingClasses::StateTransition).new({:to => :closed, :from => :received})
new_event
end
context 'when firing' do
should 'raise an AASM::InvalidTransition error if the transitions are empty' do
event = AASM::SupportingClasses::Event.new(:event)
obj = OpenStruct.new
obj.aasm_current_state = :open
assert_raise AASM::InvalidTransition do
event.fire(obj)
end
end
should 'return the state of the first matching transition it finds' do
event = AASM::SupportingClasses::Event.new(:event) do
transitions :to => :closed, :from => [:open, :received]
end
obj = OpenStruct.new
obj.aasm_current_state = :open
assert_equal :closed, event.fire(obj)
end
end
end
end

View file

@ -0,0 +1,69 @@
require 'test_helper'
class StateTest < Test::Unit::TestCase
def new_state(options={})
AASM::SupportingClasses::State.new(@name, @options.merge(options))
end
context 'state' do
setup do
@name = :astate
@options = { :crazy_custom_key => 'key' }
end
should 'set the name' do
assert_equal :astate, new_state.name
end
should 'set the display_name from name' do
assert_equal "Astate", new_state.display_name
end
should 'set the display_name from options' do
assert_equal "A State", new_state(:display => "A State").display_name
end
should 'set the options and expose them as options' do
assert_equal @options, new_state.options
end
should 'equal a symbol of the same name' do
assert_equal new_state, :astate
end
should 'equal a state of the same name' do
assert_equal new_state, new_state
end
should 'send a message to the record for an action if the action is present as a symbol' do
state = new_state(:entering => :foo)
mock(record = Object.new).foo
state.call_action(:entering, record)
end
should 'send a message to the record for an action if the action is present as a string' do
state = new_state(:entering => 'foo')
mock(record = Object.new).foo
state.call_action(:entering, record)
end
should 'call a proc with the record as its argument for an action if the action is present as a proc' do
state = new_state(:entering => Proc.new {|r| r.foobar})
mock(record = Object.new).foobar
state.call_action(:entering, record)
end
should 'send a message to the record for each action if the action is present as an array' do
state = new_state(:entering => [:a, :b, 'c', lambda {|r| r.foobar}])
record = Object.new
mock(record).a
mock(record).b
mock(record).c
mock(record).foobar
state.call_action(:entering, record)
end
end
end

View file

@ -0,0 +1,75 @@
require 'test_helper'
class StateTransitionTest < Test::Unit::TestCase
context 'state transition' do
setup do
@opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
@st = AASM::SupportingClasses::StateTransition.new(@opts)
end
should 'set from, to, and opts attr readers' do
assert_equal @opts[:from], @st.from
assert_equal @opts[:to], @st.to
assert_equal @opts, @st.options
end
should 'pass equality check if from and to are the same' do
obj = OpenStruct.new
obj.from = @opts[:from]
obj.to = @opts[:to]
assert_equal @st, obj
end
should 'fail equality check if from is not the same' do
obj = OpenStruct.new
obj.from = 'blah'
obj.to = @opts[:to]
assert_not_equal @st, obj
end
should 'fail equality check if to is not the same' do
obj = OpenStruct.new
obj.from = @opts[:from]
obj.to = 'blah'
assert_not_equal @st, obj
end
context 'when performing guard checks' do
should 'return true if there is no guard' do
opts = {:from => 'foo', :to => 'bar'}
st = AASM::SupportingClasses::StateTransition.new(opts)
assert st.perform(nil)
end
should 'call the method on the object if guard is a symbol' do
opts = {:from => 'foo', :to => 'bar', :guard => :test_guard}
st = AASM::SupportingClasses::StateTransition.new(opts)
mock(obj = Object.new).test_guard
st.perform(obj)
end
should 'call the method on the object if guard is a string' do
opts = {:from => 'foo', :to => 'bar', :guard => 'test_guard'}
st = AASM::SupportingClasses::StateTransition.new(opts)
mock(obj = Object.new).test_guard
st.perform(obj)
end
should 'call the proc passing the object if guard is a proc' do
opts = {:from => 'foo', :to => 'bar', :guard => Proc.new {|o| o.test_guard}}
st = AASM::SupportingClasses::StateTransition.new(opts)
mock(obj = Object.new).test_guard
st.perform(obj)
end
end
end
end

View file

@ -1,13 +0,0 @@
* trunk *
break with true value [Kaspar Schiess]
* 2.1 *
After actions [Saimon Moore]
* 2.0 * (2006-01-20 15:26:28 -0500)
Enter / Exit actions
Transition guards
Guards and actions can be a symbol pointing to a method or a Proc
* 1.0 * (2006-01-15 12:16:55 -0500)
Initial Release

View file

@ -1,20 +0,0 @@
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.

View file

@ -1,33 +0,0 @@
= Acts As State Machine
This act gives an Active Record model the ability to act as a finite state
machine (FSM).
Acquire via subversion at:
http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
If prompted, use the user/pass anonymous/anonymous.
== Example
class Order < ActiveRecord::Base
acts_as_state_machine :initial => :opened
state :opened
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
state :returned
event :close do
transitions :to => :closed, :from => :opened
end
event :return do
transitions :to => :returned, :from => :closed
end
end
o = Order.create
o.close! # notice is sent by mailer
o.return!

View file

@ -1,28 +0,0 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
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

View file

@ -1,11 +0,0 @@
* Currently invalid events are ignored, create an option so that they can be
ignored or raise an exception.
* Query for a list of possible next states.
* Make listing states optional since they can be inferred from the events.
Only required to list a state if you want to define a transition block for it.
* Real transition actions
* Default states

View file

@ -1,5 +0,0 @@
require 'acts_as_state_machine'
ActiveRecord::Base.class_eval do
include ScottBarron::Acts::StateMachine
end

View file

@ -1,276 +0,0 @@
module ScottBarron #:nodoc:
module Acts #:nodoc:
module StateMachine #:nodoc:
class InvalidState < Exception #:nodoc:
end
class NoInitialState < Exception #:nodoc:
end
def self.included(base) #:nodoc:
base.extend ActMacro
end
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
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)
@guard ? obj.send(:run_transition_action, @guard) : true
end
def perform(record)
return false unless guard(record)
loopback = record.current_state == to
states = record.class.read_inheritable_attribute(:states)
next_state = states[to]
old_state = states[record.current_state]
next_state.entering(record) unless loopback
if record.new_record?
record.send(record.class.state_column.to_s + '=', to.to_s)
else
record.update_attribute(record.class.state_column, to.to_s)
end
next_state.entered(record) unless loopback
old_state.exited(record) unless loopback
true
end
def ==(obj)
@from == obj.from && @to == obj.to
end
end
class Event
attr_reader :name
attr_reader :transitions
attr_reader :opts
def initialize(name, opts, transition_table, &block)
@name = name.to_sym
@transitions = transition_table[@name] = []
instance_eval(&block) if block
@opts = opts
@opts.freeze
@transitions.freeze
freeze
end
def next_states(record)
@transitions.select { |t| t.from == record.current_state }
end
def fire(record)
next_states(record).each do |transition|
break true if transition.perform(record)
end
end
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
module ActMacro
# Configuration options are
#
# * +column+ - specifies the column name to use for keeping the state (default: state)
# * +initial+ - specifies an initial state for newly created objects (required)
def acts_as_state_machine(opts)
self.extend(ClassMethods)
raise NoInitialState unless opts[:initial]
write_inheritable_attribute :states, {}
write_inheritable_attribute :initial_state, opts[:initial]
write_inheritable_attribute :transition_table, {}
write_inheritable_attribute :event_table, {}
write_inheritable_attribute :state_column, opts[:column] || 'state'
class_inheritable_reader :initial_state
class_inheritable_reader :state_column
class_inheritable_reader :transition_table
class_inheritable_reader :event_table
self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)
before_create :set_initial_state
after_create :run_initial_state_actions
end
end
module InstanceMethods
def set_initial_state #:nodoc:
write_attribute self.class.state_column, self.class.initial_state.to_s
end
def run_initial_state_actions
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_sym]
initial.entering(self)
initial.entered(self)
end
# Returns the current state the object is in, as a Ruby symbol.
def current_state
x = self.send(self.class.state_column)
return x.to_sym if not x.nil?
# if current state is not yet set, set it
self.set_initial_state
return self.current_state
end
# Returns what the next state for a given event would be, as a Ruby symbol.
def next_state_for_event(event)
ns = next_states_for_event(event)
ns.empty? ? nil : ns.first.to
end
def next_states_for_event(event)
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
s.from == current_state
end
end
def run_transition_action(action)
Symbol === action ? self.method(action).call : action.call(self)
end
private :run_transition_action
end
module ClassMethods
# Returns an array of all known states.
def states
read_inheritable_attribute(:states).keys
end
# Define an event. This takes a block which describes all valid transitions
# for this event.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed
#
# event :close_order do
# transitions :to => :closed, :from => :open
# end
# end
#
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
# to and <tt>:from</tt> is a state (or Array of states) from which this
# event can be fired.
#
# This creates an instance method used for firing the event. The method
# created is the name of the event followed by an exclamation point (!).
# Example: <tt>order.close_order!</tt>.
def event(event, opts={}, &block)
tt = read_inheritable_attribute(:transition_table)
et = read_inheritable_attribute(:event_table)
e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
define_method("#{event.to_s}!") { e.fire(self) }
end
# Define a state of the system. +state+ can take an optional Proc object
# which will be executed every time the system transitions into that
# state. The proc will be passed the current object.
#
# Example:
#
# class Order < ActiveRecord::Base
# acts_as_state_machine :initial => :open
#
# state :open
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
# end
def state(name, opts={})
state = SupportingClasses::State.new(name.to_sym, opts)
read_inheritable_attribute(:states)[name.to_sym] = state
define_method("#{state.name}?") { current_state == state.name }
end
# Wraps ActiveRecord::Base.find to conveniently find all records in
# a given state. Options:
#
# * +number+ - This is just :first or :all from ActiveRecord +find+
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def find_in_state(number, state, *args)
with_state_scope state do
find(number, *args)
end
end
# Wraps ActiveRecord::Base.count to conveniently count all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
def count_in_state(state, *args)
with_state_scope state do
count(*args)
end
end
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
# a given state. Options:
#
# * +state+ - The state to find
# * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
def calculate_in_state(state, *args)
with_state_scope state do
calculate(*args)
end
end
protected
def with_state_scope(state)
raise InvalidState unless states.include?(state)
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
yield if block_given?
end
end
end
end
end
end

View file

@ -1,224 +0,0 @@
require File.dirname(__FILE__) + '/test_helper'
include ScottBarron::Acts::StateMachine
class ActsAsStateMachineTest < Test::Unit::TestCase
fixtures :conversations
def test_no_initial_value_raises_exception
assert_raise(NoInitialState) {
Person.acts_as_state_machine({})
}
end
def test_initial_state_value
assert_equal :needs_attention, Conversation.initial_state
end
def test_column_was_set
assert_equal 'state_machine', Conversation.state_column
end
def test_initial_state
c = Conversation.create
assert_equal :needs_attention, c.current_state
assert c.needs_attention?
end
def test_states_were_set
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |s|
assert Conversation.states.include?(s)
end
end
def test_event_methods_created
c = Conversation.create
%w(new_message! view! reply! close! junk! unjunk!).each do |event|
assert c.respond_to?(event)
end
end
def test_query_methods_created
c = Conversation.create
%w(needs_attention? read? closed? awaiting_response? junk?).each do |event|
assert c.respond_to?(event)
end
end
def test_transition_table
tt = Conversation.transition_table
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
end
def test_next_state_for_event
c = Conversation.create
assert_equal :read, c.next_state_for_event(:view)
end
def test_change_state
c = Conversation.create
c.view!
assert c.read?
end
def test_can_go_from_read_to_closed_because_guard_passes
c = Conversation.create
c.can_close = true
c.view!
c.reply!
c.close!
assert_equal :closed, c.current_state
end
def test_cannot_go_from_read_to_closed_because_of_guard
c = Conversation.create
c.can_close = false
c.view!
c.reply!
c.close!
assert_equal :read, c.current_state
end
def test_ignore_invalid_events
c = Conversation.create
c.view!
c.junk!
# This is the invalid event
c.new_message!
assert_equal :junk, c.current_state
end
def test_entry_action_executed
c = Conversation.create
c.read_enter = false
c.view!
assert c.read_enter
end
def test_after_actions_executed
c = Conversation.create
c.read_after_first = false
c.read_after_second = false
c.closed_after = false
c.view!
assert c.read_after_first
assert c.read_after_second
c.can_close = true
c.close!
assert c.closed_after
assert_equal :closed, c.current_state
end
def test_after_actions_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_after_first = false
c.read_after_second = false
c.view!
assert !c.read_after_first
assert !c.read_after_second
c.can_close = true
c.close!
c.closed_after = false
c.close!
assert !c.closed_after
end
def test_exit_action_executed
c = Conversation.create
c.read_exit = false
c.view!
c.junk!
assert c.read_exit
end
def test_entry_and_exit_not_run_on_loopback_transition
c = Conversation.create
c.view!
c.read_enter = false
c.read_exit = false
c.view!
assert !c.read_enter
assert !c.read_exit
end
def test_entry_and_after_actions_called_for_initial_state
c = Conversation.create
assert c.needs_attention_enter
assert c.needs_attention_after
end
def test_run_transition_action_is_private
c = Conversation.create
assert_raise(NoMethodError) { c.run_transition_action :foo }
end
def test_find_all_in_state
cs = Conversation.find_in_state(:all, :read)
assert_equal 2, cs.size
end
def test_find_first_in_state
c = Conversation.find_in_state(:first, :read)
assert_equal conversations(:first).id, c.id
end
def test_find_all_in_state_with_conditions
cs = Conversation.find_in_state(:all, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal 1, cs.size
assert_equal conversations(:second).id, cs.first.id
end
def test_find_first_in_state_with_conditions
c = Conversation.find_in_state(:first, :read, :conditions => ['subject = ?', conversations(:second).subject])
assert_equal conversations(:second).id, c.id
end
def test_count_in_state
cnt0 = Conversation.count(['state_machine = ?', 'read'])
cnt = Conversation.count_in_state(:read)
assert_equal cnt0, cnt
end
def test_count_in_state_with_conditions
cnt0 = Conversation.count(['state_machine = ? AND subject = ?', 'read', 'Foo'])
cnt = Conversation.count_in_state(:read, ['subject = ?', 'Foo'])
assert_equal cnt0, cnt
end
def test_find_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.find_in_state(:all, :dead)
}
end
def test_count_in_invalid_state_raises_exception
assert_raise(InvalidState) {
Conversation.count_in_state(:dead)
}
end
def test_can_access_events_via_event_table
event = Conversation.event_table[:junk]
assert_equal :junk, event.name
assert_equal "finished", event.opts[:note]
end
end

View file

@ -1,18 +0,0 @@
sqlite:
:adapter: sqlite
:dbfile: state_machine.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: state_machine.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: state_machine_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: state_machine_test

View file

@ -1,67 +0,0 @@
class Conversation < ActiveRecord::Base
attr_writer :can_close
attr_accessor :read_enter, :read_exit, :read_after_first, :read_after_second,
:closed_after, :needs_attention_enter, :needs_attention_after
acts_as_state_machine :initial => :needs_attention, :column => 'state_machine'
state :needs_attention, :enter => Proc.new { |o| o.needs_attention_enter = true },
:after => Proc.new { |o| o.needs_attention_after = true }
state :read, :enter => :read_enter_action,
:exit => Proc.new { |o| o.read_exit = true },
:after => [:read_after_first_action, :read_after_second_action]
state :closed, :after => :closed_after_action
state :awaiting_response
state :junk
event :new_message do
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
end
event :view do
transitions :to => :read, :from => [:needs_attention, :read]
end
event :reply do
transitions :to => :awaiting_response, :from => [:read, :closed]
end
event :close do
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => Proc.new {|o| o.can_close?}
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
end
event :junk, :note => "finished" do
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
end
event :unjunk do
transitions :to => :closed, :from => :junk
end
def can_close?
@can_close
end
def read_enter_action
self.read_enter = true
end
def always_true
true
end
def read_after_first_action
self.read_after_first = true
end
def read_after_second_action
self.read_after_second = true
end
def closed_after_action
self.closed_after = true
end
end

View file

@ -1,11 +0,0 @@
first:
id: 1
state_machine: read
subject: This is a test
closed: false
second:
id: 2
state_machine: read
subject: Foo
closed: false

View file

@ -1,2 +0,0 @@
class Person < ActiveRecord::Base
end

View file

@ -1,11 +0,0 @@
ActiveRecord::Schema.define(:version => 1) do
create_table :conversations, :force => true do |t|
t.column :state_machine, :string
t.column :subject, :string
t.column :closed, :boolean
end
create_table :people, :force => true do |t|
t.column :name, :string
end
end

View file

@ -1,38 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
RAILS_ROOT = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_support/binding_of_caller'
require 'active_support/breakpoint'
require "#{File.dirname(__FILE__)}/../init"
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end