Merge remote branch 'ttilley/master'

Conflicts:
	Rakefile
This commit is contained in:
Travis Tilley 2009-10-23 16:28:29 -04:00
commit 72e43ad88e
24 changed files with 637 additions and 793 deletions

5
.document Normal file
View File

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

11
.gitignore vendored
View File

@ -1,5 +1,8 @@
rdoc
pkg
coverage
*~
*.gemspec
*.sw?
*~
.DS_Store
.idea
coverage
pkg
rdoc

View File

@ -1,33 +0,0 @@
* In AR persistence, move state column from class level variables into the StateMachine object for the class
* allowed :to array and :on_transition callback [Kevin Triplett]
* Support enter and exit actions on states
* Use named_scope in AR persistence layer, if available [Jan De Poorter]
* Incremented version number
* Cleaned up aasm_states_for_select to return the value as a string
* Specs and bug fixes for the ActiveRecordPersistence, keeping persistence columns in sync
Allowing for nil values in states for active record
Only set state to default state before_validation_on_create
New rake task to uninstall, build and reinstall the gem (useful for development)
Changed scott's email address to protect it from spambots when publishing rdocs
New non-(!) methods that allow for firing events without persisting [Jeff Dean]
* Added aasm_states_for_select that will return a select friendly collection of states.
* Add some event callbacks, #aasm_event_fired(from, to), and #aasm_event_failed(event)
Based on transition logging suggestion [Artem Vasiliev] and timestamp column suggestion [Mike Ferrier]
* Add #aasm_events_for_state and #aasm_events_for_current_state [Joao Paulo Lins]
* Ensure that a state is written for a new record even if aasm_current_state or
{state}= are never called.
* Fix AR persistence so new records have their state set. [Joao Paulo Lins]
* Make #event! methods return a boolean [Joel Chippindale]

View File

@ -25,7 +25,7 @@ The callback chain & order on a successful event looks like:
__update state__
event:success*
oldstate:after_exit
oldstate:after_enter
newstate:after_enter
event:after
obj:aasm_event_fired*

187
Rakefile
View File

@ -1,95 +1,108 @@
# Copyright 2008 Scott Barron (scott@elitists.net)
# All rights reserved
# This file may be distributed under an MIT style license.
# See MIT-LICENSE for details.
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://github.com/rubyist/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 'rubygems'
require 'rake/gempackagetask'
require 'rake/testtask'
require 'rake/rdoctask'
require 'spec/rake/spectask'
rescue Exception
nil
end
require 'sdoc'
Rake::RDocTask.new do |rdoc|
if File.exist?('VERSION')
version = File.read('VERSION')
else
version = ""
end
if `ruby -Ilib -raasm -e "print AASM.Version"` =~ /([0-9.]+)$/
CURRENT_VERSION = $1
else
CURRENT_VERSION = '0.0.0'
end
$package_version = CURRENT_VERSION
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "ttilley-aasm #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
PKG_FILES = FileList['[A-Z]*',
'lib/**/*.rb',
'doc/**/*'
]
desc 'Generate documentation for the acts as state machine plugin.'
rd = Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'html'
rdoc.template = 'doc/jamis.rb'
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AASM'
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc' << '--title' << 'AASM'
rdoc.rdoc_files.include('README.rdoc', 'MIT-LICENSE', 'TODO', 'CHANGELOG')
rdoc.rdoc_files.include('lib/*.rb', 'lib/**/*.rb', 'doc/**/*.rdoc')
end
if !defined?(Gem)
puts "Package target requires RubyGEMs"
else
spec = Gem::Specification.new do |s|
s.name = 'aasm'
s.version = $package_version
s.summary = 'State machine mixin for Ruby objects'
s.description = <<EOF
AASM is a continuation of the acts as state machine rails plugin, built for plain Ruby objects.
EOF
s.files = PKG_FILES.to_a
s.require_path = 'lib'
s.has_rdoc = true
s.extra_rdoc_files = rd.rdoc_files.reject {|fn| fn =~ /\.rb$/}.to_a
s.rdoc_options = rd.options
s.authors = ['Scott Barron', 'Scott Petersen', 'Travis Tilley']
s.email = 'scott@elitists.net'
s.homepage = 'http://github.com/rubyist/aasm'
end
package_task = Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
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
if !defined?(Spec)
puts "spec and cruise targets require RSpec"
else
desc "Run all examples with RCov"
Spec::Rake::SpecTask.new('cruise') do |t|
t.spec_files = FileList['spec/**/*.rb']
t.rcov = true
t.rcov_opts = ['--exclude', 'spec', '--exclude', 'Library', '--exclude', 'rcov.rb']
end
desc "Run all examples"
Spec::Rake::SpecTask.new('spec') do |t|
t.spec_files = FileList['spec/**/*.rb']
t.rcov = false
t.spec_opts = ['-cfs']
end
end
if !defined?(Gem)
puts "Package target requires RubyGEMs"
else
desc "sudo gem uninstall aasm && rake gem && sudo gem install pkg/aasm-3.0.0.gem"
task :reinstall do
puts `sudo gem uninstall aasm && rake gem && sudo gem install pkg/aasm-3.0.0.gem`
end
end
task :default => [:spec]

9
TODO
View File

@ -1,9 +0,0 @@
Before Next Release:
* Add #aasm_next_state_for_event
* Add #aasm_next_states_for_event
Cool ideas from users:
* Support multiple state machines on one object (Chris Nelson)
* http://justbarebones.blogspot.com/2007/11/actsasstatemachine-enhancements.html (Chetan Patil)

1
VERSION Normal file
View File

@ -0,0 +1 @@
2.1.2

View File

@ -1,591 +0,0 @@
module RDoc
module Page
FONTS = "\"Bitstream Vera Sans\", Verdana, Arial, Helvetica, sans-serif"
STYLE = <<CSS
a {
color: #00F;
text-decoration: none;
}
a:hover {
color: #77F;
text-decoration: underline;
}
body, td, p {
font-family: %fonts%;
background: #FFF;
color: #000;
margin: 0px;
font-size: small;
}
#content {
margin: 2em;
}
#description p {
margin-bottom: 0.5em;
}
.sectiontitle {
margin-top: 1em;
margin-bottom: 1em;
padding: 0.5em;
padding-left: 2em;
background: #005;
color: #FFF;
font-weight: bold;
border: 1px dotted black;
}
.attr-rw {
padding-left: 1em;
padding-right: 1em;
text-align: center;
color: #055;
}
.attr-name {
font-weight: bold;
}
.attr-desc {
}
.attr-value {
font-family: monospace;
}
.file-title-prefix {
font-size: large;
}
.file-title {
font-size: large;
font-weight: bold;
background: #005;
color: #FFF;
}
.banner {
background: #005;
color: #FFF;
border: 1px solid black;
padding: 1em;
}
.banner td {
background: transparent;
color: #FFF;
}
h1 a, h2 a, .sectiontitle a, .banner a {
color: #FF0;
}
h1 a:hover, h2 a:hover, .sectiontitle a:hover, .banner a:hover {
color: #FF7;
}
.dyn-source {
display: none;
background: #FFE;
color: #000;
border: 1px dotted black;
margin: 0.5em 2em 0.5em 2em;
padding: 0.5em;
}
.dyn-source .cmt {
color: #00F;
font-style: italic;
}
.dyn-source .kw {
color: #070;
font-weight: bold;
}
.method {
margin-left: 1em;
margin-right: 1em;
margin-bottom: 1em;
}
.description pre {
padding: 0.5em;
border: 1px dotted black;
background: #FFE;
}
.method .title {
font-family: monospace;
font-size: large;
border-bottom: 1px dashed black;
margin-bottom: 0.3em;
padding-bottom: 0.1em;
}
.method .description, .method .sourcecode {
margin-left: 1em;
}
.description p, .sourcecode p {
margin-bottom: 0.5em;
}
.method .sourcecode p.source-link {
text-indent: 0em;
margin-top: 0.5em;
}
.method .aka {
margin-top: 0.3em;
margin-left: 1em;
font-style: italic;
text-indent: 2em;
}
h1 {
padding: 1em;
border: 1px solid black;
font-size: x-large;
font-weight: bold;
color: #FFF;
background: #007;
}
h2 {
padding: 0.5em 1em 0.5em 1em;
border: 1px solid black;
font-size: large;
font-weight: bold;
color: #FFF;
background: #009;
}
h3, h4, h5, h6 {
padding: 0.2em 1em 0.2em 1em;
border: 1px dashed black;
color: #000;
background: #AAF;
}
.sourcecode > pre {
padding: 0.5em;
border: 1px dotted black;
background: #FFE;
}
CSS
XHTML_PREAMBLE = %{<?xml version="1.0" encoding="%charset%"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
}
HEADER = XHTML_PREAMBLE + <<ENDHEADER
<html>
<head>
<title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%" />
<link rel="stylesheet" href="%style_url%" type="text/css" media="screen" />
<script language="JavaScript" type="text/javascript">
// <![CDATA[
function toggleSource( id )
{
var elem
var link
if( document.getElementById )
{
elem = document.getElementById( id )
link = document.getElementById( "l_" + id )
}
else if ( document.all )
{
elem = eval( "document.all." + id )
link = eval( "document.all.l_" + id )
}
else
return false;
if( elem.style.display == "block" )
{
elem.style.display = "none"
link.innerHTML = "show source"
}
else
{
elem.style.display = "block"
link.innerHTML = "hide source"
}
}
function openCode( url )
{
window.open( url, "SOURCE_CODE", "width=400,height=400,scrollbars=yes" )
}
// ]]>
</script>
</head>
<body>
ENDHEADER
FILE_PAGE = <<HTML
<table border='0' cellpadding='0' cellspacing='0' width="100%" class='banner'>
<tr><td>
<table width="100%" border='0' cellpadding='0' cellspacing='0'><tr>
<td class="file-title" colspan="2"><span class="file-title-prefix">File</span><br />%short_name%</td>
<td align="right">
<table border='0' cellspacing="0" cellpadding="2">
<tr>
<td>Path:</td>
<td>%full_path%
IF:cvsurl
&nbsp;(<a href="%cvsurl%">CVS</a>)
ENDIF:cvsurl
</td>
</tr>
<tr>
<td>Modified:</td>
<td>%dtm_modified%</td>
</tr>
</table>
</td></tr>
</table>
</td></tr>
</table><br>
HTML
###################################################################
CLASS_PAGE = <<HTML
<table width="100%" border='0' cellpadding='0' cellspacing='0' class='banner'><tr>
<td class="file-title"><span class="file-title-prefix">%classmod%</span><br />%full_name%</td>
<td align="right">
<table cellspacing=0 cellpadding=2>
<tr valign="top">
<td>In:</td>
<td>
START:infiles
HREF:full_path_url:full_path:
IF:cvsurl
&nbsp;(<a href="%cvsurl%">CVS</a>)
ENDIF:cvsurl
END:infiles
</td>
</tr>
IF:parent
<tr>
<td>Parent:</td>
<td>
IF:par_url
<a href="%par_url%">
ENDIF:par_url
%parent%
IF:par_url
</a>
ENDIF:par_url
</td>
</tr>
ENDIF:parent
</table>
</td>
</tr>
</table>
HTML
###################################################################
METHOD_LIST = <<HTML
<div id="content">
IF:diagram
<table cellpadding='0' cellspacing='0' border='0' width="100%"><tr><td align="center">
%diagram%
</td></tr></table>
ENDIF:diagram
IF:description
<div class="description">%description%</div>
ENDIF:description
IF:requires
<div class="sectiontitle">Required Files</div>
<ul>
START:requires
<li>HREF:aref:name:</li>
END:requires
</ul>
ENDIF:requires
IF:toc
<div class="sectiontitle">Contents</div>
<ul>
START:toc
<li><a href="#%href%">%secname%</a></li>
END:toc
</ul>
ENDIF:toc
IF:methods
<div class="sectiontitle">Methods</div>
<ul>
START:methods
<li>HREF:aref:name:</li>
END:methods
</ul>
ENDIF:methods
IF:includes
<div class="sectiontitle">Included Modules</div>
<ul>
START:includes
<li>HREF:aref:name:</li>
END:includes
</ul>
ENDIF:includes
START:sections
IF:sectitle
<div class="sectiontitle"><a nem="%secsequence%">%sectitle%</a></div>
IF:seccomment
<div class="description">
%seccomment%
</div>
ENDIF:seccomment
ENDIF:sectitle
IF:classlist
<div class="sectiontitle">Classes and Modules</div>
%classlist%
ENDIF:classlist
IF:constants
<div class="sectiontitle">Constants</div>
<table border='0' cellpadding='5'>
START:constants
<tr valign='top'>
<td class="attr-name">%name%</td>
<td>=</td>
<td class="attr-value">%value%</td>
</tr>
IF:desc
<tr valign='top'>
<td>&nbsp;</td>
<td colspan="2" class="attr-desc">%desc%</td>
</tr>
ENDIF:desc
END:constants
</table>
ENDIF:constants
IF:attributes
<div class="sectiontitle">Attributes</div>
<table border='0' cellpadding='5'>
START:attributes
<tr valign='top'>
<td class='attr-rw'>
IF:rw
[%rw%]
ENDIF:rw
</td>
<td class='attr-name'>%name%</td>
<td class='attr-desc'>%a_desc%</td>
</tr>
END:attributes
</table>
ENDIF:attributes
IF:method_list
START:method_list
IF:methods
<div class="sectiontitle">%type% %category% methods</div>
START:methods
<div class="method">
<div class="title">
IF:callseq
<a name="%aref%"></a><b>%callseq%</b>
ENDIF:callseq
IFNOT:callseq
<a name="%aref%"></a><b>%name%</b>%params%
ENDIF:callseq
IF:codeurl
[ <a href="javascript:openCode('%codeurl%')">source</a> ]
ENDIF:codeurl
</div>
IF:m_desc
<div class="description">
%m_desc%
</div>
ENDIF:m_desc
IF:aka
<div class="aka">
This method is also aliased as
START:aka
<a href="%aref%">%name%</a>
END:aka
</div>
ENDIF:aka
IF:sourcecode
<div class="sourcecode">
<p class="source-link">[ <a href="javascript:toggleSource('%aref%_source')" id="l_%aref%_source">show source</a> ]</p>
<div id="%aref%_source" class="dyn-source">
<pre>
%sourcecode%
</pre>
</div>
</div>
ENDIF:sourcecode
</div>
END:methods
ENDIF:methods
END:method_list
ENDIF:method_list
END:sections
</div>
HTML
FOOTER = <<ENDFOOTER
</body>
</html>
ENDFOOTER
BODY = HEADER + <<ENDBODY
!INCLUDE! <!-- banner header -->
<div id="bodyContent">
#{METHOD_LIST}
</div>
#{FOOTER}
ENDBODY
########################## Source code ##########################
SRC_PAGE = XHTML_PREAMBLE + <<HTML
<html>
<head><title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
<style>
.ruby-comment { color: green; font-style: italic }
.ruby-constant { color: #4433aa; font-weight: bold; }
.ruby-identifier { color: #222222; }
.ruby-ivar { color: #2233dd; }
.ruby-keyword { color: #3333FF; font-weight: bold }
.ruby-node { color: #777777; }
.ruby-operator { color: #111111; }
.ruby-regexp { color: #662222; }
.ruby-value { color: #662222; font-style: italic }
.kw { color: #3333FF; font-weight: bold }
.cmt { color: green; font-style: italic }
.str { color: #662222; font-style: italic }
.re { color: #662222; }
</style>
</head>
<body bgcolor="white">
<pre>%code%</pre>
</body>
</html>
HTML
########################## Index ################################
FR_INDEX_BODY = <<HTML
!INCLUDE!
HTML
FILE_INDEX = XHTML_PREAMBLE + <<HTML
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
<style>
<!--
body {
background-color: #EEE;
font-family: #{FONTS};
color: #000;
margin: 0px;
}
.banner {
background: #005;
color: #FFF;
padding: 0.2em;
font-size: small;
font-weight: bold;
text-align: center;
}
.entries {
margin: 0.25em 1em 0 1em;
font-size: x-small;
}
a {
color: #00F;
text-decoration: none;
white-space: nowrap;
}
a:hover {
color: #77F;
text-decoration: underline;
}
-->
</style>
<base target="docwin">
</head>
<body>
<div class="banner">%list_title%</div>
<div class="entries">
START:entries
<a href="%href%">%name%</a><br>
END:entries
</div>
</body></html>
HTML
CLASS_INDEX = FILE_INDEX
METHOD_INDEX = FILE_INDEX
INDEX = XHTML_PREAMBLE + <<HTML
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>%title%</title>
<meta http-equiv="Content-Type" content="text/html; charset=%charset%">
</head>
<frameset cols="20%,*">
<frameset rows="15%,35%,50%">
<frame src="fr_file_index.html" title="Files" name="Files" />
<frame src="fr_class_index.html" name="Classes" />
<frame src="fr_method_index.html" name="Methods" />
</frameset>
IF:inline_source
<frame src="%initial_page%" name="docwin">
ENDIF:inline_source
IFNOT:inline_source
<frameset rows="80%,20%">
<frame src="%initial_page%" name="docwin">
<frame src="blank.html" name="source">
</frameset>
ENDIF:inline_source
<noframes>
<body bgcolor="white">
Click <a href="html/index.html">here</a> for a non-frames
version of this page.
</body>
</noframes>
</frameset>
</html>
HTML
end
end

View File

@ -4,10 +4,6 @@ require File.join(File.dirname(__FILE__), 'state_machine')
require File.join(File.dirname(__FILE__), 'persistence')
module AASM
def self.Version
'2.1.1'
end
class InvalidTransition < RuntimeError
end
@ -88,7 +84,20 @@ module AASM
@aasm_current_state = aasm_read_state
end
return @aasm_current_state if @aasm_current_state
aasm_determine_state_name(self.class.aasm_initial_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
@ -101,6 +110,7 @@ module AASM
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')
@ -120,12 +130,12 @@ module AASM
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."
when Symbol, String
state
when Proc
state.call(self)
else
raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
end
end
@ -148,13 +158,13 @@ module AASM
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)
@ -163,7 +173,7 @@ module AASM
self.aasm_current_state = new_state_name
end
if persist_successful
if persist_successful
old_state.call_action(:after_exit, self)
new_state.call_action(:after_enter, self)
event.call_action(:after, self)

View File

@ -4,13 +4,11 @@ module AASM
module SupportingClasses
class Event
attr_reader :name, :success, :options
def initialize(name, options = {}, &block)
@name = name
@success = options[:success]
@transitions = []
@options = options
instance_eval(&block) if block
update(options, &block)
end
def fire(obj, to_state=nil, *args)
@ -37,35 +35,59 @@ module AASM
@transitions.select { |t| t.from == state }
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 call_action(action, record)
action = @options[action]
case action
when Symbol, String
record.send(action)
when Proc
action.call(record)
when Array
action.each { |a| record.send(a) }
end
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 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
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 << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))

View File

@ -155,7 +155,7 @@ module AASM
# foo.aasm_state # => nil
#
def aasm_ensure_initial_state
send("#{self.class.aasm_column}=", self.aasm_current_state.to_s)
send("#{self.class.aasm_column}=", self.aasm_enter_initial_state.to_s) if send(self.class.aasm_column).blank?
end
end

View File

@ -4,7 +4,8 @@ module AASM
attr_reader :name, :options
def initialize(name, options={})
@name, @options = name, options
@name = name
update(options)
end
def ==(state)
@ -17,19 +18,38 @@ module AASM
def call_action(action, record)
action = @options[action]
case action
when Symbol, String
record.send(action)
when Proc
action.call(record)
when Array
action.each { |a| record.send(a) }
end
action.is_a?(Array) ?
action.each {|a| _call_action(a, record)} :
_call_action(action, record)
end
def display_name
@display_name ||= name.to_s.gsub(/_/, ' ').capitalize
end
def for_select
[name.to_s.gsub(/_/, ' ').capitalize, name.to_s]
[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
end
end

View File

@ -15,7 +15,7 @@ module AASM
attr_reader :name
def initialize(name)
@name = name
@name = name
@initial_state = nil
@states = []
@events = {}
@ -25,6 +25,7 @@ module AASM
def clone
klone = super
klone.states = states.clone
klone.events = events.clone
klone
end

View File

@ -2,6 +2,7 @@ module AASM
module SupportingClasses
class 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]
@ -10,27 +11,40 @@ module AASM
def perform(obj)
case @guard
when Symbol, String
obj.send(@guard)
when Proc
@guard.call(obj)
else
true
when Symbol, String
obj.send(@guard)
when Proc
@guard.call(obj)
else
true
end
end
def execute(obj, *args)
case @on_transition
when Symbol, String
obj.send(@on_transition, *args)
when Proc
@on_transition.call(obj, *args)
end
@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
end
end

View File

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

View File

@ -354,6 +354,7 @@ class ChetanPatil
aasm_state :showering
aasm_state :working
aasm_state :dating
aasm_state :prettying_up
aasm_event :wakeup do
transitions :from => :sleeping, :to => [:showering, :working]
@ -362,10 +363,17 @@ class ChetanPatil
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
@ -413,4 +421,12 @@ describe ChetanPatil do
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

@ -50,6 +50,18 @@ describe AASM::SupportingClasses::State do
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 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})

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

33
test/test_helper.rb Normal file
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'

0
test/unit/aasm_test.rb Normal file
View File

54
test/unit/event_test.rb Normal file
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

69
test/unit/state_test.rb Normal file
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