Repository: rubyist/aasm
Branch: master
Commit: a4bc27cff0d7
Files: 70
Total size: 106.7 KB
Directory structure:
gitextract_cxl63xfk/
├── .document
├── .gitignore
├── .travis.yml
├── API
├── CHANGELOG.md
├── Gemfile
├── HOWTO
├── LICENSE
├── README.md
├── Rakefile
├── aasm.gemspec
├── lib/
│ ├── aasm/
│ │ ├── aasm.rb
│ │ ├── base.rb
│ │ ├── deprecated/
│ │ │ └── aasm.rb
│ │ ├── errors.rb
│ │ ├── event.rb
│ │ ├── instance_base.rb
│ │ ├── localizer.rb
│ │ ├── persistence/
│ │ │ ├── active_record_persistence.rb
│ │ │ ├── base.rb
│ │ │ └── mongoid_persistence.rb
│ │ ├── persistence.rb
│ │ ├── state.rb
│ │ ├── state_machine.rb
│ │ ├── transition.rb
│ │ └── version.rb
│ └── aasm.rb
└── spec/
├── database.yml
├── en.yml
├── en_deprecated_style.yml
├── models/
│ ├── active_record/
│ │ └── api.rb
│ ├── argument.rb
│ ├── auth_machine.rb
│ ├── bar.rb
│ ├── callback_new_dsl.rb
│ ├── callback_old_dsl.rb
│ ├── conversation.rb
│ ├── father.rb
│ ├── foo.rb
│ ├── invalid_persistor.rb
│ ├── mongoid/
│ │ ├── simple_mongoid.rb
│ │ └── simple_new_dsl_mongoid.rb
│ ├── not_auto_loaded/
│ │ └── process.rb
│ ├── parametrised_event.rb
│ ├── persistence.rb
│ ├── process_with_new_dsl.rb
│ ├── silencer.rb
│ ├── son.rb
│ ├── sub_classing.rb
│ ├── this_name_better_not_be_in_use.rb
│ ├── transactor.rb
│ ├── validator.rb
│ └── worker.rb
├── schema.rb
├── spec_helper.rb
└── unit/
├── api_spec.rb
├── callbacks_spec.rb
├── complex_example_spec.rb
├── event_spec.rb
├── initial_state_spec.rb
├── inspection_spec.rb
├── localizer_spec.rb
├── memory_leak_spec.rb
├── new_dsl_spec.rb
├── persistence/
│ ├── active_record_persistence_spec.rb
│ └── mongoid_persistance_spec.rb
├── simple_example_spec.rb
├── state_spec.rb
├── subclassing_spec.rb
└── transition_spec.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .document
================================================
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
-
LICENSE
================================================
FILE: .gitignore
================================================
*.sw?
*~
.DS_Store
.idea
coverage
pkg
rdoc
Gemfile.lock
spec/debug.log
spec/*.db
TODO
.rvmrc
alto
================================================
FILE: .travis.yml
================================================
language: ruby
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- 2.0.0
# - jruby-18mode # JRuby in 1.8 mode
# - jruby-19mode # JRuby in 1.9 mode
- rbx-18mode
- rbx-19mode
services: mongodb
================================================
FILE: API
================================================
Overwrite method to read the current state. Used to provide another storage mechanism,
different from the standard Rails read_attribute method.
class MyClass
include AASM
def aasm_read_state
# retrieve the current state manually
end
end
Overwrite method to write the current state (and actually persist it). Used to provide
another storage mechanism, different from the standard Rails write_attribute method.
class MyClass
include AASM
def aasm_write_state
# store and persist the current state manually
end
end
Overwrite method to write the current state (without persisting it).
class MyClass
include AASM
def aasm_write_state_without_persistence
# store the current state manually
end
end
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
## 3.0.19
* fixed deprecation warning with *Rails 4* (`Relation#update_all` with conditions is deprecated)
* fixing [issue #69](https://github.com/aasm/aasm/issues/69) ( *ActiveRecord* scopes are not chainable)
## 3.0.18
* fixing [issue #66](https://github.com/aasm/aasm/issues/66) (state methods not reflecting the current state)
## 3.0.17
* supporting instance level inspection for states (including permissible state, see [issue #54](https://github.com/aasm/aasm/issues/54))
* added autocreation of constants for each state ([@jherdman](https://github.com/jherdman))
## 3.0.16
* added autocreation of state scopes for Mongoid (thanks to [@jonnyshields](https://github.com/johnnyshields))
## 3.0.15
* added support for localized state names (on a class level, like `Record.aasm.states.map(&:localized_name)`)
## 3.0.14
* supporting event inspection for to-states transitions (`Event#transitions_to_state?`)
## 3.0.13
* supporting *ActiveRecord* transactions when firing an event
## 3.0.12
* `aasm_from_states_for_state` now supports to filter for specific transition
## 3.0.11
* added class method `aasm_from_states_for_state` to retrieve all from states (regarding transitions) for a given state
## 3.0.10
* added support for transitions from all other states (thanks to [@swrobel](https://github.com/swrobel))
## 3.0.9
* guard checks (e.g. `may_edit?`) now support guard parameters as well
## 3.0.8
* fixed issue with generating docs using yard
## 3.0.7
* removed deprecation warning when localizing aasm state names (look at [issue #38](https://github.com/rubyist/aasm/issues/38) for details)
## 3.0.6
* bugfix: if configured to skip validation the code does not validate anymore
## 3.0.5
* bugfix: get rid of error with old rubygems versions
## 3.0.4
* bugfix: Subclasses of aasm-enabled classes don't lose settings anymore (thanks to codez)
## 3.0.3
* bugfix: ActiveRecord scopes are generated when using the new DSL
## 3.0.2
* ActiveRecord persistence can ignore validation when trying to save invalid models
## 3.0.1
* added support for Mongoid (Thanks, Michał Taberski)
## 3.0.0
* switched documentation to the new DSL
* whiny transactions: by default, raise an exception if an event transition is not possible
* you may disable whiny transactions
## 2.4.0
* supporting new DSL (which is much shorter)
## 2.3.1
* bugfix: avoid naming conflict with i18n
## 2.3.0
* supporting i18n
* supporting regular expressions for hash values and strings
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
gemspec
================================================
FILE: HOWTO
================================================
How to
1. Run tests for Mongoid
Start MongoDB
$> mongod
Run the specs
$> rspec spec/unit/persistence/mongoid_persistance_spec.rb
================================================
FILE: LICENSE
================================================
Copyright (c) 2006-2012 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.
================================================
FILE: README.md
================================================
# AASM - Ruby state machines [](http://travis-ci.org/aasm/aasm) [](https://codeclimate.com/github/aasm/aasm) [](https://coveralls.io/r/aasm/aasm)
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. It currently provides adapters for
[ActiveRecord](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) and
[Mongoid](http://mongoid.org/), but it can be used for any Ruby class, no matter what
parent class it has (if any).
## Usage
Adding a state machine is as simple as including the AASM module and start defining
**states** and **events** together with their **transitions**:
```ruby
class Job
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => [:running, :cleaning], :to => :sleeping
end
end
end
```
This provides you with a couple of public methods for instances of the class `Job`:
```ruby
job = Job.new
job.sleeping? # => true
job.may_run? # => true
job.run
job.running? # => true
job.sleeping? # => false
job.may_run? # => false
job.run # => raises AASM::InvalidTransition
```
If you don't like exceptions and prefer a simple `true` or `false` as response, tell
AASM not to be *whiny*:
```ruby
class Job
...
aasm :whiny_transitions => false do
...
end
end
job.running? # => true
job.may_run? # => false
job.run # => false
```
### Callbacks
You can define a number of callbacks for your transitions. These methods will be
called, when certain criteria are met, like entering a particular state:
```ruby
class Job
include AASM
aasm do
state :sleeping, :initial => true, :before_enter => :do_something
state :running
event :run, :after => Proc.new { |user| notify_somebody(user) } do
transitions :from => :sleeping, :to => :running, :on_transition => Proc.new {|obj, *args| obj.set_process(*args) }
end
event :sleep do
after do
...
end
error do |e|
...
end
transitions :from => :running, :to => :sleeping
end
end
def set_process(name)
...
end
def do_something
...
end
def notify_somebody(user)
...
end
end
```
In this case `do_something` is called before actually entering the state `sleeping`,
while `notify_somebody` is called after the transition `run` (from `sleeping` to `running`)
is finished.
Here you can see a list of all possible callbacks, together with their order of calling:
```ruby
event:before
previous_state:before_exit
new_state:before_enter
...update state...
previous_state:after_exit
new_state:after_enter
event:after
```
Also, you can pass parameters to events:
```ruby
job = Job.new
job.run(:running, :defragmentation)
```
In this case the `set_process` would be called with `:defagmentation` argument.
In case of an error during the event processing the error is rescued and passed to `:error`
callback, which can handle it or re-raise it for further propagation.
### Guards
Let's assume you want to allow particular transitions only if a defined condition is
given. For this you can set up a guard per transition, which will run before actually
running the transition. If the guard returns `false` the transition will be
denied (raising `AASM::InvalidTransition` or returning `false` itself):
```ruby
class Job
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => :running, :to => :sleeping, :guard => :cleaning_needed?
end
end
def cleaning_needed?
false
end
end
job = Job.new
job.run
job.may_sleep? # => false
job.sleep # => raises AASM::InvalidTransition
```
### ActiveRecord
AASM comes with support for ActiveRecord and allows automatical persisting of the object's
state in the database.
```ruby
class Job < ActiveRecord::Base
include AASM
aasm do # default column: aasm_state
state :sleeping, :initial => true
state :running
event :run do
transitions :from => :sleeping, :to => :running
end
event :sleep do
transitions :from => :running, :to => :sleeping
end
end
end
```
You can tell AASM to auto-save the object or leave it unsaved
```ruby
job = Job.new
job.run # not saved
job.run! # saved
```
Saving includes running all validations on the `Job` class. If you want make sure
the state gets saved without running validations (and thereby maybe persisting an
invalid object state), simply tell AASM to skip the validations:
```ruby
class Job < ActiveRecord::Base
include AASM
aasm :skip_validation_on_save => true do
state :sleeping, :initial => true
state :running
event :run do
transitions :from => :sleeping, :to => :running
end
event :sleep do
transitions :from => :running, :to => :sleeping
end
end
end
```
### Automatic Scopes
AASM will automatically create scope methods for each state in the model.
```ruby
class Job < ActiveRecord::Base
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
end
def sleeping
"This method name is in already use"
end
end
```
```ruby
class JobsController < ApplicationController
def index
@running_jobs = jobs.running
@recent_cleaning_jobs = jobs.cleaning.where('created_at >= ?', 3.days.ago)
# @sleeping_jobs = jobs.sleeping #=> "This method name is in already use"
end
end
```
### Transaction support
Since version *3.0.13* AASM supports ActiveRecord transactions. So whenever a transition
callback or the state update fails, all changes to any database record are rolled back.
### Column name & migration
As a default AASM uses the column `aasm_state` to store the states. You can override
this by defining your favorite column name, using `:column` like this:
```ruby
class Job < ActiveRecord::Base
include AASM
aasm :column => 'my_state' do
...
end
end
```
Whatever column name is used, make sure to add a migration to provide this column
(of type `string`):
```ruby
class AddJobState < ActiveRecord::Migration
def self.up
add_column :jobs, :aasm_state, :string
end
def self.down
remove_column :jobs, :aasm_state
end
end
```
### Inspection
AASM supports a couple of methods to find out which states or events are provided or permissible.
Given the `Job` class from above:
```ruby
job = Job.new
job.aasm.states
=> [:sleeping, :running, :cleaning]
job.aasm.states(:permissible => true)
=> [:running]
job.run
job.aasm.states(:permissible => true)
=> [:cleaning, :sleeping]
job.aasm.events
=> [:run, :clean, :sleep]
```
## Installation ##
### Manually from RubyGems.org ###
```sh
% gem install aasm
```
### Or if you are using Bundler ###
```ruby
# Gemfile
gem 'aasm'
```
### Building your own gems ###
```sh
% rake build
% sudo gem install pkg/aasm-x.y.z.gem
```
## Latest changes ##
Look at the [CHANGELOG](https://github.com/aasm/aasm/blob/master/CHANGELOG.md) for details.
## Questions? ##
Feel free to
* [create an issue on GitHub](https://github.com/aasm/aasm/issues)
* [ask a question on StackOverflow](http://stackoverflow.com) (tag with `aasm`)
* send us a tweet [@aasm](http://twitter.com/aasm)
## Authors ##
* [Scott Barron](https://github.com/rubyist)
* [Travis Tilley](https://github.com/ttilley)
* [Thorsten Böttger](http://github.com/alto)
## 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.
## License ##
Copyright (c) 2006-2012 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.
================================================
FILE: Rakefile
================================================
require 'bundler/gem_tasks'
require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
require 'rdoc/task'
require 'aasm/version'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "aasm #{AASM::VERSION}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
task :default => :spec
================================================
FILE: aasm.gemspec
================================================
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "aasm/version"
Gem::Specification.new do |s|
s.name = "aasm"
s.version = AASM::VERSION
s.authors = ["Scott Barron", "Scott Petersen", "Travis Tilley", "Thorsten Boettger"]
s.email = %q{scott@elitists.net, ttilley@gmail.com, aasm@mt7.de}
s.homepage = %q{https://github.com/aasm/aasm}
s.summary = %q{State machine mixin for Ruby objects}
s.description = %q{AASM is a continuation of the acts as state machine rails plugin, built for plain Ruby objects.}
s.date = Time.now
s.licenses = ["MIT"]
s.add_development_dependency 'activerecord', '3.2.12'
# s.add_development_dependency 'activerecord', '4.0.0.rc1'
s.add_development_dependency 'mongoid' if Gem::Version.create(RUBY_VERSION.dup) >= Gem::Version.create('1.9.3')
s.add_development_dependency 'rake'
s.add_development_dependency 'sdoc'
s.add_development_dependency 'rspec', '~> 2.0'
s.add_development_dependency 'rr'
s.add_development_dependency 'sqlite3'
s.add_development_dependency 'minitest'
# s.add_development_dependency 'debugger'
# s.add_development_dependency 'pry'
s.add_development_dependency 'ruby-debug-completion'
s.add_development_dependency 'coveralls'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
end
================================================
FILE: lib/aasm/aasm.rb
================================================
module AASM
def self.included(base) #:nodoc:
base.extend AASM::ClassMethods
# do not overwrite existing state machines, which could have been created by
# inheritance, see class method inherited
AASM::StateMachine[base] ||= AASM::StateMachine.new('')
AASM::Persistence.load_persistence(base)
super
end
module ClassMethods
# make sure inheritance (aka subclassing) works with AASM
def inherited(base)
AASM::StateMachine[base] = AASM::StateMachine[self].clone
super
end
# this is the entry point for all state and event definitions
def aasm(options={}, &block)
@aasm ||= AASM::Base.new(self, options)
@aasm.instance_eval(&block) if block # new DSL
@aasm
end
# TODO: maybe better: aasm.initial_state
def aasm_initial_state(set_state=nil)
if set_state
# deprecated way to set the value
AASM::StateMachine[self].initial_state = set_state
else
AASM::StateMachine[self].initial_state
end
end
# is this better?: aasm.states.name.from_states
def aasm_from_states_for_state(state, options={})
if options[:transition]
aasm.events[options[:transition]].transitions_to_state(state).flatten.map(&:from).flatten
else
aasm.events.map {|k,v| v.transitions_to_state(state)}.flatten.map(&:from).flatten
end
end
# deprecated
def aasm_initial_state=(state)
AASM::StateMachine[self].initial_state = state
end
# deprecated
def aasm_state(name, options={})
aasm.state(name, options)
end
# deprecated
def aasm_event(name, options = {}, &block)
aasm.event(name, options, &block)
end
# deprecated
def aasm_states
aasm.states
end
# deprecated
def aasm_events
aasm.events
end
# deprecated
def aasm_states_for_select
aasm.states_for_select
end
# aasm.event(:event_name).human?
def aasm_human_event_name(event) # event_name?
AASM::Localizer.new.human_event_name(self, event)
end
end # ClassMethods
def aasm
@aasm ||= AASM::InstanceBase.new(self)
end
# may be overwritten by persistence mixins
def aasm_read_state
# all the following lines behave like @current_state ||= aasm.enter_initial_state
current = aasm.instance_variable_get("@current_state")
return current if current
aasm.instance_variable_set("@current_state", aasm.enter_initial_state)
end
# may be overwritten by persistence mixins
def aasm_write_state(new_state)
true
end
# may be overwritten by persistence mixins
def aasm_write_state_without_persistence(new_state)
true
end
# deprecated
def aasm_current_state
# warn "#aasm_current_state is deprecated and will be removed in version 3.2.0; please use #aasm.state instead!"
aasm.current_state
end
# deprecated
def aasm_enter_initial_state
# warn "#aasm_enter_initial_state is deprecated and will be removed in version 3.2.0; please use #aasm.enter_initial_state instead!"
aasm.enter_initial_state
end
# deprecated
def aasm_events_for_current_state
# warn "#aasm_events_for_current_state is deprecated and will be removed in version 3.2.0; please use #aasm.events instead!"
aasm.events(aasm.current_state)
end
# deprecated
def aasm_permissible_events_for_current_state
# warn "#aasm_permissible_events_for_current_state is deprecated and will be removed in version 3.2.0; please use #aasm.permissible_events instead!"
aasm.permissible_events
end
# deprecated
def aasm_events_for_state(state_name)
# warn "#aasm_events_for_state(state_name) is deprecated and will be removed in version 3.2.0; please use #aasm.events(state_name) instead!"
aasm.events(state_name)
end
# deprecated
def aasm_human_state
# warn "#aasm_human_state is deprecated and will be removed in version 3.2.0; please use #aasm.human_state instead!"
aasm.human_state
end
private
def aasm_fire_event(event_name, options, *args)
event = self.class.aasm_events[event_name]
begin
old_state = aasm.state_object_for_name(aasm.current_state)
old_state.fire_callbacks(:exit, self)
# new event before callback
event.fire_callbacks(:before, self)
if new_state_name = event.fire(self, *args)
fired(event, old_state, new_state_name, options)
else
failed(event_name, old_state)
end
rescue StandardError => e
event.fire_callbacks(:error, self, e) || raise(e)
end
end
def fired(event, old_state, new_state_name, options)
persist = options[:persist]
new_state = aasm.state_object_for_name(new_state_name)
# new before_ callbacks
old_state.fire_callbacks(:before_exit, self)
new_state.fire_callbacks(:before_enter, self)
new_state.fire_callbacks(:enter, self)
persist_successful = true
if persist
persist_successful = aasm.set_current_state_with_persistence(new_state_name)
event.fire_callbacks(:success, self) if persist_successful
else
aasm.current_state = new_state_name
end
if persist_successful
old_state.fire_callbacks(:after_exit, self)
new_state.fire_callbacks(:after_enter, self)
event.fire_callbacks(:after, self)
self.aasm_event_fired(event.name, old_state.name, aasm.current_state) if self.respond_to?(:aasm_event_fired)
else
self.aasm_event_failed(event.name, old_state.name) if self.respond_to?(:aasm_event_failed)
end
persist_successful
end
def failed(event_name, old_state)
if self.respond_to?(:aasm_event_failed)
self.aasm_event_failed(event_name, old_state.name)
end
if AASM::StateMachine[self.class].config.whiny_transitions
raise AASM::InvalidTransition, "Event '#{event_name}' cannot transition from '#{aasm.current_state}'"
else
false
end
end
end
================================================
FILE: lib/aasm/base.rb
================================================
module AASM
class Base
def initialize(clazz, options={}, &block)
@clazz = clazz
@state_machine = AASM::StateMachine[@clazz]
@state_machine.config.column = options[:column].to_sym if options[:column]
if options.key?(:whiny_transitions)
@state_machine.config.whiny_transitions = options[:whiny_transitions]
elsif @state_machine.config.whiny_transitions.nil?
@state_machine.config.whiny_transitions = true # this is the default, so let's cry
end
if options.key?(:skip_validation_on_save)
@state_machine.config.skip_validation_on_save = options[:skip_validation_on_save]
elsif @state_machine.config.skip_validation_on_save.nil?
@state_machine.config.skip_validation_on_save = false # this is the default, so don't store any new state if the model is invalid
end
end
def initial_state
@state_machine.initial_state
end
# define a state
def state(name, options={})
# @clazz.aasm_state(name, options)
@state_machine.add_state(name, @clazz, options)
@state_machine.initial_state = name if options[:initial] || !@state_machine.initial_state
@clazz.send(:define_method, "#{name.to_s}?") do
aasm.current_state == name
end
unless @clazz.const_defined?("STATE_#{name.to_s.upcase}")
@clazz.const_set("STATE_#{name.to_s.upcase}", name)
end
end
# define an event
def event(name, options={}, &block)
# @clazz.aasm_event(name, options, &block)
unless @state_machine.events.has_key?(name)
@state_machine.events[name] = AASM::Event.new(name, options, &block)
end
# an addition over standard aasm so that, before firing an event, you can ask
# may_event? and get back a boolean that tells you whether the guard method
# on the transition will let this happen.
@clazz.send(:define_method, "may_#{name.to_s}?") do |*args|
aasm.may_fire_event?(name, *args)
end
@clazz.send(:define_method, "#{name.to_s}!") do |*args|
aasm_fire_event(name, {:persist => true}, *args)
end
@clazz.send(:define_method, "#{name.to_s}") do |*args|
aasm_fire_event(name, {:persist => false}, *args)
end
end
def states
@state_machine.states
end
def events
@state_machine.events
end
def states_for_select
states.map { |state| state.for_select }
end
end
end
================================================
FILE: lib/aasm/deprecated/aasm.rb
================================================
module AASM
module ClassMethods
def human_event_name(*args)
warn "AASM.human_event_name is deprecated and will be removed in version 3.1.0; please use AASM.aasm_human_event_name instead!"
aasm_human_event_name(*args)
end
end
def human_state
warn "AASM#human_state is deprecated and will be removed in version 3.1.0; please use AASM#aasm_human_state instead!"
aasm_human_state
end
end
================================================
FILE: lib/aasm/errors.rb
================================================
module AASM
class InvalidTransition < RuntimeError; end
class UndefinedState < RuntimeError; end
end
================================================
FILE: lib/aasm/event.rb
================================================
module AASM
class Event
attr_reader :name, :options
def initialize(name, options = {}, &block)
@name = name
@transitions = []
update(options, &block)
end
# a neutered version of fire - it doesn't actually fire the event, it just
# executes the transition guards to determine if a transition is even
# an option given current conditions.
def may_fire?(obj, to_state=nil, *args)
_fire(obj, true, to_state, *args) # true indicates test firing
end
def fire(obj, to_state=nil, *args)
_fire(obj, false, to_state, *args) # false indicates this is not a test (fire!)
end
def transitions_from_state?(state)
transitions_from_state(state).any?
end
def transitions_from_state(state)
@transitions.select { |t| t.from == state }
end
def transitions_to_state?(state)
transitions_to_state(state).any?
end
def transitions_to_state(state)
@transitions.select { |t| t.to == state }
end
# deprecated
def all_transitions
# warn "Event#all_transitions is deprecated and will be removed in version 3.2.0; please use Event#transitions instead!"
transitions
end
def fire_callbacks(callback_name, record, *args)
invoke_callbacks(@options[callback_name], record, args)
end
def ==(event)
if event.is_a? Symbol
name == event
else
name == event.name
end
end
private
def update(options = {}, &block)
@options = options
if block then
instance_eval(&block)
end
self
end
# Execute if test == false, otherwise return true/false depending on whether it would fire
def _fire(obj, test, to_state=nil, *args)
result = test ? false : nil
if @transitions.map(&:from).any?
transitions = @transitions.select { |t| t.from == obj.aasm_current_state }
return result if transitions.size == 0
else
transitions = @transitions
end
transitions.each do |transition|
next if to_state and !Array(transition.to).include?(to_state)
if transition.perform(obj, *args)
if test
result = true
else
result = to_state || Array(transition.to).first
transition.execute(obj, *args)
end
break
end
end
result
end
def invoke_callbacks(code, record, args)
case code
when Symbol, String
record.send(code, *args)
true
when Proc
record.instance_exec(*args, &code)
true
when Array
code.each {|a| invoke_callbacks(a, record, args)}
true
else
false
end
end
## DSL interface
def transitions(trans_opts=nil)
if trans_opts # define new transitions
# Create a separate transition for each from state to the given state
Array(trans_opts[:from]).each do |s|
@transitions << AASM::Transition.new(trans_opts.merge({:from => s.to_sym}))
end
# Create a transition if to is specified without from (transitions from ANY state)
@transitions << AASM::Transition.new(trans_opts) if @transitions.empty? && trans_opts[:to]
end
@transitions
end
[:after, :before, :error, :success].each do |callback_name|
define_method callback_name do |*args, &block|
options[callback_name] = Array(options[callback_name])
options[callback_name] << block if block
options[callback_name] += Array(args)
end
end
end
end # AASM
================================================
FILE: lib/aasm/instance_base.rb
================================================
module AASM
class InstanceBase
def initialize(instance)
@instance = instance
end
def current_state
@instance.aasm_read_state
end
def current_state=(state)
@instance.aasm_write_state_without_persistence(state)
@current_state = state
end
def enter_initial_state
state_name = determine_state_name(@instance.class.aasm_initial_state)
state_object = state_object_for_name(state_name)
state_object.fire_callbacks(:before_enter, @instance)
state_object.fire_callbacks(:enter, @instance)
self.current_state = state_name
state_object.fire_callbacks(:after_enter, @instance)
state_name
end
def human_state
AASM::Localizer.new.human_state_name(@instance.class, current_state)
end
def states(options={})
if options[:permissible]
# ugliness level 1000
transitions = @instance.class.aasm.events.values.map {|e| e.transitions_from_state(current_state) }
tos = transitions.map {|t| t[0] ? t[0].to : nil}.flatten.compact.map(&:to_sym).uniq
@instance.class.aasm.states.select {|s| tos.include?(s.name.to_sym)}
else
@instance.class.aasm.states
end
end
# QUESTION: shouldn't events and permissible_events be the same thing?
# QUESTION: shouldn't events return objects instead of strings?
def events(state=current_state)
events = @instance.class.aasm.events.values.select {|e| e.transitions_from_state?(state) }
events.map {|e| e.name}
end
# filters the results of events_for_current_state so that only those that
# are really currently possible (given transition guards) are shown.
# QUESTION: what about events.permissible ?
def permissible_events
events.select{ |e| @instance.send(("may_" + e.to_s + "?").to_sym) }
end
def state_object_for_name(name)
obj = @instance.class.aasm.states.find {|s| s == name}
raise AASM::UndefinedState, "State :#{name} doesn't exist" if obj.nil?
obj
end
def determine_state_name(state)
case state
when Symbol, String
state
when Proc
state.call(@instance)
else
raise NotImplementedError, "Unrecognized state-type given. Expected Symbol, String, or Proc."
end
end
def may_fire_event?(name, *args)
event = @instance.class.aasm.events[name]
event.may_fire?(@instance, *args)
end
def set_current_state_with_persistence(state)
save_success = @instance.aasm_write_state(state)
self.current_state = state if save_success
save_success
end
end
end
================================================
FILE: lib/aasm/localizer.rb
================================================
module AASM
class Localizer
def human_event_name(klass, event)
checklist = ancestors_list(klass).inject([]) do |list, ancestor|
list << :"#{i18n_scope(klass)}.events.#{i18n_klass(ancestor)}.#{event}"
list
end
translate_queue(checklist) || I18n.translate(checklist.shift, :default => event.to_s.humanize)
end
def human_state_name(klass, state)
checklist = ancestors_list(klass).inject([]) do |list, ancestor|
list << item_for(klass, state, ancestor)
list << item_for(klass, state, ancestor, :old_style => true)
list
end
translate_queue(checklist) || I18n.translate(checklist.shift, :default => state.to_s.humanize)
end
private
def item_for(klass, state, ancestor, options={})
separator = options[:old_style] ? '.' : '/'
:"#{i18n_scope(klass)}.attributes.#{i18n_klass(ancestor)}.#{klass.aasm_column}#{separator}#{state}"
end
def translate_queue(checklist)
(0...(checklist.size-1)).each do |i|
begin
return I18n.translate(checklist.shift, :raise => true)
rescue I18n::MissingTranslationData
# that's okay
end
end
nil
end
# added for rails 2.x compatibility
def i18n_scope(klass)
klass.respond_to?(:i18n_scope) ? klass.i18n_scope : :activerecord
end
# added for rails < 3.0.3 compatibility
def i18n_klass(klass)
klass.model_name.respond_to?(:i18n_key) ? klass.model_name.i18n_key : klass.name.underscore
end
def ancestors_list(klass)
klass.ancestors.select do |ancestor|
ancestor.respond_to?(:model_name) unless ancestor == ActiveRecord::Base
end
end
end
end # AASM
================================================
FILE: lib/aasm/persistence/active_record_persistence.rb
================================================
module AASM
module Persistence
module ActiveRecordPersistence
# This method:
#
# * extends the model with ClassMethods
# * includes InstanceMethods
#
# 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.send(:include, AASM::Persistence::Base)
base.extend AASM::Persistence::ActiveRecordPersistence::ClassMethods
base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
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
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
# Writes state 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)
success = if AASM::StateMachine[self.class].config.skip_validation_on_save
self.class.where(self.class.primary_key => self.id).update_all(self.class.aasm_column => state.to_s) == 1
else
self.save
end
unless success
write_attribute(self.class.aasm_column, old_value)
return false
end
true
end
# Writes state 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
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
aasm.enter_initial_state if send(self.class.aasm_column).blank?
end
def aasm_fire_event(name, options, *args)
transaction do
super
end
end
end # InstanceMethods
end
end
end
================================================
FILE: lib/aasm/persistence/base.rb
================================================
module AASM
module Persistence
module Base
def self.included(base) #:nodoc:
base.extend ClassMethods
end
# Returns the value of the aasm_column - called from aasm.current_state
#
# If it's a new record, and the aasm state column is blank it returns the initial state
# (example provided here for ActiveRecord, but it's true for Mongoid as well):
#
# class Foo < ActiveRecord::Base
# include AASM
# aasm :column => :status do
# state :opened
# state :closed
# end
# 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
state = send(self.class.aasm_column)
if new_record?
state.blank? ? aasm.determine_state_name(self.class.aasm_initial_state) : state.to_sym
else
state.nil? ? nil : state.to_sym
end
end
module ClassMethods
# Maps to the aasm_column in the database. Defaults to "aasm_state". You can write
# (example provided here for ActiveRecord, but it's true for Mongoid as well):
#
# 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
end # ClassMethods
end # Base
end # Persistence
class Base
# make sure to create a (named) scope for each state
def state_with_scope(name, *args)
state_without_scope(name, *args)
unless @clazz.respond_to?(name)
if @clazz.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
conditions = {"#{@clazz.table_name}.#{@clazz.aasm_column}" => name.to_s}
if ActiveRecord::VERSION::MAJOR >= 4
@clazz.class_eval do
scope name, lambda { where(conditions) }
end
elsif ActiveRecord::VERSION::MAJOR >= 3
@clazz.class_eval do
scope name, where(conditions)
end
else
@clazz.class_eval do
named_scope name, :conditions => conditions
end
end
elsif @clazz.ancestors.map {|klass| klass.to_s}.include?("Mongoid::Document")
scope_options = lambda { @clazz.send(:where, {@clazz.aasm_column.to_sym => name.to_s}) }
@clazz.send(:scope, name, scope_options)
end
end
end
alias_method :state_without_scope, :state
alias_method :state, :state_with_scope
end # Base
end # AASM
================================================
FILE: lib/aasm/persistence/mongoid_persistence.rb
================================================
module AASM
module Persistence
module MongoidPersistence
# This method:
#
# * extends the model with ClassMethods
# * includes InstanceMethods
#
# Adds
#
# before_validation :aasm_ensure_initial_state
#
# As a result, it doesn't matter when you define your methods - the following 2 are equivalent
#
# class Foo
# include Mongoid::Document
# def aasm_write_state(state)
# "bar"
# end
# include AASM
# end
#
# class Foo
# include Mongoid::Document
# include AASM
# def aasm_write_state(state)
# "bar"
# end
# end
#
def self.included(base)
base.send(:include, AASM::Persistence::Base)
base.extend AASM::Persistence::MongoidPersistence::ClassMethods
base.send(:include, AASM::Persistence::MongoidPersistence::InstanceMethods)
# Mongoid's Validatable gem dependency goes not have a before_validation_on_xxx hook yet.
# base.before_validation_on_create :aasm_ensure_initial_state
base.before_validation :aasm_ensure_initial_state
end
module ClassMethods
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 with_state_scope(state)
with_scope where(aasm_column.to_sym => state.to_s) do
yield if block_given?
end
end
end
module InstanceMethods
# Writes state to the state column and persists it to the database
# using update_attribute (which bypasses validation)
#
# 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)
unless self.save(:validate => false)
write_attribute(self.class.aasm_column, old_value)
return false
end
true
end
# Writes state 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
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}=", aasm.enter_initial_state.to_s) if send(self.class.aasm_column).blank?
end
end # InstanceMethods
module NamedScopeMethods
def aasm_state_with_named_scope name, options = {}
aasm_state_without_named_scope name, options
self.named_scope name, :conditions => { "#{table_name}.#{self.aasm_column}" => name.to_s} unless self.respond_to?(name)
end
end
end
end
end
================================================
FILE: lib/aasm/persistence.rb
================================================
module AASM
module Persistence
class << self
def load_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_files_for(:active_record)
base.send(:include, AASM::Persistence::ActiveRecordPersistence)
elsif hierarchy.include?("Mongoid::Document")
require_files_for(:mongoid)
base.send(:include, AASM::Persistence::MongoidPersistence)
end
end
private
def require_files_for(persistence)
['base', "#{persistence}_persistence"].each do |file_name|
require File.join(File.dirname(__FILE__), 'persistence', file_name)
end
end
end # class << self
end
end # AASM
================================================
FILE: lib/aasm/state.rb
================================================
module AASM
class State
attr_reader :name, :options
def initialize(name, clazz, options={})
@name = name
@clazz = clazz
update(options)
end
def ==(state)
if state.is_a? Symbol
name == state
else
name == state.name
end
end
def <=>(state)
if state.is_a? Symbol
name <=> state
else
name <=> state.name
end
end
def to_s
name.to_s
end
def fire_callbacks(action, record)
action = @options[action]
catch :halt_aasm_chain do
action.is_a?(Array) ?
action.each {|a| _fire_callbacks(a, record)} :
_fire_callbacks(action, record)
end
end
def display_name
@display_name ||= begin
if Module.const_defined?(:I18n)
localized_name
else
name.to_s.gsub(/_/, ' ').capitalize
end
end
end
def localized_name
AASM::Localizer.new.human_state_name(@clazz, self)
end
def for_select
[display_name, name.to_s]
end
private
def update(options = {})
if options.key?(:display) then
@display_name = options.delete(:display)
end
@options = options
self
end
def _fire_callbacks(action, record)
case action
when Symbol, String
record.send(action)
when Proc
action.call(record)
end
end
end
end # AASM
================================================
FILE: lib/aasm/state_machine.rb
================================================
module AASM
class StateMachine
# the following two methods provide the storage of all state machines
def self.[](clazz)
(@machines ||= {})[clazz.to_s]
end
def self.[]=(clazz, machine)
(@machines ||= {})[clazz.to_s] = machine
end
attr_accessor :states, :events, :initial_state, :config
attr_reader :name
# QUESTION: what's the name for? [alto, 2012-11-28]
def initialize(name)
@name = name
@initial_state = nil
@states = []
@events = {}
@config = OpenStruct.new
end
# called internally by Ruby 1.9 after clone()
def initialize_copy(orig)
super
@states = @states.dup
@events = @events.dup
end
def add_state(name, clazz, options)
@states << AASM::State.new(name, clazz, options) unless @states.include?(name)
end
end # StateMachine
end # AASM
================================================
FILE: lib/aasm/transition.rb
================================================
module AASM
class Transition
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
# TODO: should be named allowed? or similar
def perform(obj, *args)
case @guard
when Symbol, String
obj.send(@guard, *args)
when Proc
@guard.call(obj, *args)
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 Proc
on_transition.arity == 0 ? on_transition.call : on_transition.call(obj, *args)
when Symbol, String
obj.send(:method, on_transition.to_sym).arity == 0 ? obj.send(on_transition) : obj.send(on_transition, *args)
end
end
end
end # AASM
================================================
FILE: lib/aasm/version.rb
================================================
module AASM
VERSION = "3.0.19"
end
================================================
FILE: lib/aasm.rb
================================================
require 'ostruct'
%w(
version
errors
base
instance_base
transition
event
state
localizer
state_machine
persistence
aasm
).each { |file| require File.join(File.dirname(__FILE__), 'aasm', file) }
# load the deprecated methods and modules
Dir[File.join(File.dirname(__FILE__), 'aasm', 'deprecated', '*.rb')].sort.each { |f| require File.expand_path(f) }
================================================
FILE: spec/database.yml
================================================
sqlite3:
adapter: sqlite3
database: spec/aasm.sqlite3.db
================================================
FILE: spec/en.yml
================================================
en:
activerecord:
events:
localizer_test_model:
close: "Let's close it!"
attributes:
localizer_test_model:
aasm_state/opened: "It's open now!"
================================================
FILE: spec/en_deprecated_style.yml
================================================
en:
activerecord:
events:
localizer_test_model:
close: "Let's close it!"
attributes:
localizer_test_model:
aasm_state:
opened: "It's open now!"
================================================
FILE: spec/models/active_record/api.rb
================================================
class DefaultState
attr_accessor :transient_store, :persisted_store
include AASM
aasm do
state :alpha, :initial => true
state :beta
state :gamma
event :release do
transitions :from => [:alpha, :beta, :gamma], :to => :beta
end
end
end
class ProvidedState
attr_accessor :transient_store, :persisted_store
include AASM
aasm do
state :alpha, :initial => true
state :beta
state :gamma
event :release do
transitions :from => [:alpha, :beta, :gamma], :to => :beta
end
end
def aasm_read_state
:beta
end
def aasm_write_state(new_state)
@persisted_store = new_state
end
def aasm_write_state_without_persistence(new_state)
@transient_store = new_state
end
end
class PersistedState < ActiveRecord::Base
attr_accessor :transient_store, :persisted_store
include AASM
aasm do
state :alpha, :initial => true
state :beta
state :gamma
event :release do
transitions :from => [:alpha, :beta, :gamma], :to => :beta
end
end
end
class ProvidedAndPersistedState < ActiveRecord::Base
attr_accessor :transient_store, :persisted_store
include AASM
aasm do
state :alpha, :initial => true
state :beta
state :gamma
event :release do
transitions :from => [:alpha, :beta, :gamma], :to => :beta
end
end
def aasm_read_state
:gamma
end
def aasm_write_state(new_state)
@persisted_store = new_state
end
def aasm_write_state_without_persistence(new_state)
@transient_store = new_state
end
end
================================================
FILE: spec/models/argument.rb
================================================
class Argument
include AASM
aasm do
state :invalid, :initial => true
state :valid
event :valid do
transitions :to => :valid, :from => [:invalid]
end
end
end
================================================
FILE: spec/models/auth_machine.rb
================================================
class AuthMachine
include AASM
attr_accessor :activation_code, :activated_at, :deleted_at
aasm do
state :passive
state :pending, :initial => true, :enter => :make_activation_code
state :active, :enter => :do_activate
state :suspended
state :deleted, :enter => :do_delete, :exit => :do_undelete
state :waiting
event :register do
transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| u.can_register? }
end
event :activate do
transitions :from => :pending, :to => :active
end
event :suspend do
transitions :from => [:passive, :pending, :active], :to => :suspended
end
event :delete do
transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted
end
# a dummy event that can never happen
event :unpassify do
transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false }
end
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
event :wait do
transitions :from => :suspended, :to => :waiting, :guard => :if_polite?
end
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
def if_polite?(phrase = nil)
phrase == :please
end
end
================================================
FILE: spec/models/bar.rb
================================================
class Bar
include AASM
aasm do
state :read
state :ended
event :foo do
transitions :to => :ended, :from => [:read]
end
end
end
class Baz < Bar
end
================================================
FILE: spec/models/callback_new_dsl.rb
================================================
class CallbackNewDsl
include AASM
aasm do
state :open, :initial => true,
:before_enter => :before_enter_open,
:after_enter => :after_enter_open,
:before_exit => :before_exit_open,
:exit => :exit_open,
:after_exit => :after_exit_open
state :closed,
:before_enter => :before_enter_closed,
:enter => :enter_closed,
:after_enter => :after_enter_closed,
:before_exit => :before_exit_closed,
:after_exit => :after_exit_closed
event :close, :before => :before, :after => :after do
transitions :to => :closed, :from => [:open]
end
event :open, :before => :before, :after => :after do
transitions :to => :open, :from => :closed
end
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
def enter_closed; end
def exit_open; end
end
================================================
FILE: spec/models/callback_old_dsl.rb
================================================
class CallbackOldDsl
include AASM
aasm_initial_state :open
aasm_state :open,
:before_enter => :before_enter_open,
:after_enter => :after_enter_open,
:before_exit => :before_exit_open,
:exit => :exit_open,
:after_exit => :after_exit_open
aasm_state :closed,
:before_enter => :before_enter_closed,
:enter => :enter_closed,
:after_enter => :after_enter_closed,
:before_exit => :before_exit_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
def enter_closed; end
def exit_open; end
end
================================================
FILE: spec/models/conversation.rb
================================================
class Conversation
include AASM
aasm do
state :needs_attention, :initial => true
state :read
state :closed
state :awaiting_response
state :junk
event :new_message do
end
event :view do
transitions :to => :read, :from => [:needs_attention]
end
event :reply do
end
event :close do
transitions :to => :closed, :from => [:read, :awaiting_response]
end
event :junk do
transitions :to => :junk, :from => [:read]
end
event :unjunk do
end
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
================================================
FILE: spec/models/father.rb
================================================
require 'active_record'
class Father < ActiveRecord::Base
include AASM
aasm do
state :missing_details, :initial => true
state :pending_details_confirmation
event :add_details do
transitions :from => :missing_details, :to => :pending_details_confirmation
end
end
def update_state
if may_add_details?
add_details!
end
end
end
================================================
FILE: spec/models/foo.rb
================================================
class Foo
include AASM
aasm do
state :open, :initial => true, :exit => :exit
state :closed, :enter => :enter
event :close, :success => :success_callback do
transitions :from => [:open], :to => [:closed]
end
event :null do
transitions :from => [:open], :to => :closed, :guard => :always_false
end
end
def always_false
false
end
def success_callback
end
def enter
end
def exit
end
end
class FooTwo < Foo
include AASM
aasm do
state :foo
end
end
================================================
FILE: spec/models/invalid_persistor.rb
================================================
require 'active_record'
class InvalidPersistor < ActiveRecord::Base
include AASM
aasm :column => :status, :skip_validation_on_save => true do
state :sleeping, :initial => true
state :running
event :run do
transitions :to => :running, :from => :sleeping
end
event :sleep do
transitions :to => :sleeping, :from => :running
end
end
validates_presence_of :name
end
================================================
FILE: spec/models/mongoid/simple_mongoid.rb
================================================
class SimpleMongoid
include Mongoid::Document
include AASM
field :status, type: String
aasm_column :status
aasm_state :unknown_scope
aasm_state :new
end
================================================
FILE: spec/models/mongoid/simple_new_dsl_mongoid.rb
================================================
class SimpleNewDslMongoid
include Mongoid::Document
include AASM
field :status, type: String
aasm :column => :status
aasm do
state :unknown_scope
state :new
end
end
================================================
FILE: spec/models/not_auto_loaded/process.rb
================================================
module Models
class Process
include AASM
aasm_state :sleeping
aasm_state :running
aasm_state :suspended
aasm_event :start do
transitions :from => :sleeping, :to => :running
end
aasm_event :stop do
transitions :from => :running, :to => :suspended
end
end
end
================================================
FILE: spec/models/parametrised_event.rb
================================================
class ParametrisedEvent
include AASM
aasm do
state :sleeping, :initial => true
state :showering
state :working
state :dating
state :prettying_up
event :wakeup do
transitions :from => :sleeping, :to => [:showering, :working]
end
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
end
def wear_clothes(shirt_color, trouser_type)
end
def condition_hair
end
def fix_hair
end
end
================================================
FILE: spec/models/persistence.rb
================================================
class Gate < ActiveRecord::Base
include AASM
# Fake this column for testing purposes
attr_accessor :aasm_state
aasm do
state :opened
state :closed
event :view do
transitions :to => :read, :from => [:needs_attention]
end
end
end
class Reader < ActiveRecord::Base
include AASM
def aasm_read_state
"fi"
end
end
class Writer < ActiveRecord::Base
def aasm_write_state(state)
"fo"
end
include AASM
end
class Transient < ActiveRecord::Base
def aasm_write_state_without_persistence(state)
"fum"
end
include AASM
end
class Simple < ActiveRecord::Base
include AASM
aasm_column :status
aasm_state :unknown_scope
aasm_state :new
end
class SimpleNewDsl < ActiveRecord::Base
include AASM
aasm :column => :status
aasm do
state :unknown_scope
state :new
end
end
class Derivate < Simple
end
class DerivateNewDsl < SimpleNewDsl
end
class Thief < ActiveRecord::Base
if ActiveRecord::VERSION::MAJOR >= 3
self.table_name = 'thieves'
else
set_table_name "thieves"
end
include AASM
aasm_initial_state Proc.new { |thief| thief.skilled ? :rich : :jailed }
aasm_state :rich
aasm_state :jailed
attr_accessor :skilled, :aasm_state
end
================================================
FILE: spec/models/process_with_new_dsl.rb
================================================
class ProcessWithNewDsl
include AASM
def self.state(*args)
raise "wrong state method"
end
attr_accessor :flagged
aasm do
state :sleeping, :initial => true
state :running, :after_enter => :flag
state :suspended
event :start do
transitions :from => :sleeping, :to => :running
end
event :stop do
transitions :from => :running, :to => :suspended
end
end
def flag
self.flagged = true
end
def self.event(*args)
raise "wrong event method"
end
end
================================================
FILE: spec/models/silencer.rb
================================================
class Silencer
include AASM
aasm :whiny_transitions => false do
state :silent, :initial => true
state :crying
state :smiling
event :cry do
transitions :from => :silent, :to => :crying
end
event :smile do
transitions :from => :crying, :to => :smiling
end
event :smile_any do
transitions :to => :smiling
end
end
end
================================================
FILE: spec/models/son.rb
================================================
class Son < Father
include AASM
end
================================================
FILE: spec/models/sub_classing.rb
================================================
class SubClassing < Silencer
end
================================================
FILE: spec/models/this_name_better_not_be_in_use.rb
================================================
class ThisNameBetterNotBeInUse
include AASM
aasm do
state :initial
state :symbol
state :string
state :array
state :proc
end
end
================================================
FILE: spec/models/transactor.rb
================================================
require 'active_record'
class Transactor < ActiveRecord::Base
belongs_to :worker
include AASM
aasm :column => :status do
state :sleeping, :initial => true
state :running, :before_enter => :start_worker, :after_enter => :fail
event :run do
transitions :to => :running, :from => :sleeping
end
end
private
def start_worker
worker.update_attribute(:status, 'running')
end
def fail
raise StandardError.new('failed on purpose')
end
end
================================================
FILE: spec/models/validator.rb
================================================
require 'active_record'
class Validator < ActiveRecord::Base
include AASM
aasm :column => :status do
state :sleeping, :initial => true
state :running
event :run do
transitions :to => :running, :from => :sleeping
end
event :sleep do
transitions :to => :sleeping, :from => :running
end
end
validates_presence_of :name
end
================================================
FILE: spec/models/worker.rb
================================================
class Worker < ActiveRecord::Base
end
================================================
FILE: spec/schema.rb
================================================
ActiveRecord::Schema.define(:version => 0) do
%w{gates readers writers transients simples simple_new_dsls thieves localizer_test_models persisted_states provided_and_persisted_states}.each do |table_name|
create_table table_name, :force => true do |t|
t.string "aasm_state"
end
end
create_table "validators", :force => true do |t|
t.string "name"
t.string "status"
end
create_table "transactors", :force => true do |t|
t.string "name"
t.string "status"
t.integer "worker_id"
end
create_table "workers", :force => true do |t|
t.string "name"
t.string "status"
end
create_table "invalid_persistors", :force => true do |t|
t.string "name"
t.string "status"
end
create_table "fathers", :force => true do |t|
t.string "aasm_state"
t.string "type"
end
end
================================================
FILE: spec/spec_helper.rb
================================================
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
require 'aasm'
require 'rspec'
require 'rspec/autorun'
require 'coveralls'
Coveralls.wear!
# require 'ruby-debug'; Debugger.settings[:autoeval] = true; debugger; rubys_debugger = 'annoying'
# require 'ruby-debug/completion'
# require 'pry'
def load_schema
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['sqlite3'])
load(File.dirname(__FILE__) + "/schema.rb")
end
# custom spec helpers
Dir[File.dirname(__FILE__) + "/spec_helpers/**/*.rb"].sort.each { |f| require File.expand_path(f) }
# example model classes
Dir[File.dirname(__FILE__) + "/models/*.rb"].sort.each { |f| require File.expand_path(f) }
================================================
FILE: spec/unit/api_spec.rb
================================================
require 'spec_helper'
require 'models/active_record/api.rb'
describe "reading the current state" do
it "uses the AASM default" do
DefaultState.new.aasm.current_state.should eql :alpha
end
it "uses the provided method" do
ProvidedState.new.aasm.current_state.should eql :beta
end
it "uses the persistence storage" do
PersistedState.new.aasm.current_state.should eql :alpha
end
it "uses the provided method even if persisted" do
ProvidedAndPersistedState.new.aasm.current_state.should eql :gamma
end
end
describe "writing and persisting the current state" do
it "uses the AASM default" do
o = DefaultState.new
o.release!
o.persisted_store.should be_nil
end
it "uses the provided method" do
o = ProvidedState.new
o.release!
o.persisted_store.should eql :beta
end
it "uses the persistence storage" do
o = PersistedState.new
o.release!
o.persisted_store.should be_nil
end
it "uses the provided method even if persisted" do
o = ProvidedAndPersistedState.new
o.release!
o.persisted_store.should eql :beta
end
end
describe "writing the current state without persisting it" do
it "uses the AASM default" do
o = DefaultState.new
o.release
o.transient_store.should be_nil
end
it "uses the provided method" do
o = ProvidedState.new
o.release
o.transient_store.should eql :beta
end
it "uses the persistence storage" do
o = PersistedState.new
o.release
o.transient_store.should be_nil
end
it "uses the provided method even if persisted" do
o = ProvidedAndPersistedState.new
o.release
o.transient_store.should eql :beta
end
end
================================================
FILE: spec/unit/callbacks_spec.rb
================================================
require 'spec_helper'
describe 'callbacks for the old DSL' do
let(:callback) {CallbackOldDsl.new}
it "should get close callbacks" do
callback.should_receive(:exit_open).once.ordered
callback.should_receive(:before).once.ordered
callback.should_receive(:before_exit_open).once.ordered # these should be before the state changes
callback.should_receive(:before_enter_closed).once.ordered
callback.should_receive(:enter_closed).once.ordered
callback.should_receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes
callback.should_receive(:after_exit_open).once.ordered # these should be after the state changes
callback.should_receive(:after_enter_closed).once.ordered
callback.should_receive(:after).once.ordered
callback.close!
end
end
describe 'callbacks for the new DSL' do
let(:callback) {CallbackNewDsl.new}
it "be called in order" do
callback.should_receive(:exit_open).once.ordered
callback.should_receive(:before).once.ordered
callback.should_receive(:before_exit_open).once.ordered # these should be before the state changes
callback.should_receive(:before_enter_closed).once.ordered
callback.should_receive(:enter_closed).once.ordered
callback.should_receive(:aasm_write_state).once.ordered.and_return(true) # this is when the state changes
callback.should_receive(:after_exit_open).once.ordered # these should be after the state changes
callback.should_receive(:after_enter_closed).once.ordered
callback.should_receive(:after).once.ordered
callback.close!
end
end
describe '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.aasm.stub!(:set_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)
lambda {@foo.null!}.should raise_error(AASM::InvalidTransition)
end
it 'should call it when transition failed for non-bang fire' do
@foo.should_receive(:aasm_event_failed).with(:null, :open)
lambda {@foo.null}.should raise_error(AASM::InvalidTransition)
end
it 'should not call it if persist fails for bang fire' do
@foo.aasm.stub!(:set_current_state_with_persistence).and_return(false)
@foo.should_receive(:aasm_event_failed)
@foo.close!
end
end
end
================================================
FILE: spec/unit/complex_example_spec.rb
================================================
require 'spec_helper'
describe 'on initialization' do
let(:auth) {AuthMachine.new}
it 'should be in the pending state' do
auth.aasm_current_state.should == :pending
end
it 'should have an activation code' do
auth.has_activation_code?.should be_true
auth.activation_code.should_not be_nil
end
end
describe 'when being unsuspended' do
let(:auth) {AuthMachine.new}
it 'should be able to be unsuspended' do
auth.activate!
auth.suspend!
auth.may_unsuspend?.should be_true
end
it 'should not be able to be unsuspended into active' do
auth.suspend!
auth.may_unsuspend?(:active).should_not be_true
end
it 'should be able to be unsuspended into active if polite' do
auth.suspend!
auth.may_wait?(:waiting, :please).should be_true
auth.wait!(nil, :please)
end
it 'should not be able to be unsuspended into active if not polite' do
auth.suspend!
auth.may_wait?(:waiting).should_not be_true
auth.may_wait?(:waiting, :rude).should_not be_true
lambda {auth.wait!(nil, :rude)}.should raise_error(AASM::InvalidTransition)
lambda {auth.wait!}.should raise_error(AASM::InvalidTransition)
end
it 'should not be able to be unpassified' do
auth.activate!
auth.suspend!
auth.unsuspend!
auth.may_unpassify?.should_not be_true
lambda {auth.unpassify!}.should raise_error(AASM::InvalidTransition)
end
it 'should be active if previously activated' do
auth.activate!
auth.suspend!
auth.unsuspend!
auth.aasm_current_state.should == :active
end
it 'should be pending if not previously activated, but an activation code is present' do
auth.suspend!
auth.unsuspend!
auth.aasm_current_state.should == :pending
end
it 'should be passive if not previously activated and there is no activation code' do
auth.activation_code = nil
auth.suspend!
auth.unsuspend!
auth.aasm_current_state.should == :passive
end
end
================================================
FILE: spec/unit/event_spec.rb
================================================
require 'spec_helper'
describe 'adding an event' do
let(:event) do
AASM::Event.new(:close_order, {:success => :success_callback}) do
before :before_callback
after :after_callback
transitions :to => :closed, :from => [:open, :received]
end
end
it 'should set the name' do
event.name.should == :close_order
end
it 'should set the success callback' do
event.options[:success].should == :success_callback
end
it 'should set the after callback' do
event.options[:after].should == [:after_callback]
end
it 'should set the before callback' do
event.options[:before].should == [:before_callback]
end
it 'should create transitions' do
transitions = event.all_transitions
transitions[0].from.should == :open
transitions[0].to.should == :closed
transitions[1].from.should == :received
transitions[1].to.should == :closed
end
end
describe 'transition inspection' do
let(:event) do
AASM::Event.new(:run) do
transitions :to => :running, :from => :sleeping
end
end
it 'should support inspecting transitions from other states' do
event.transitions_from_state(:sleeping).map(&:to).should == [:running]
event.transitions_from_state?(:sleeping).should be_true
event.transitions_from_state(:cleaning).map(&:to).should == []
event.transitions_from_state?(:cleaning).should be_false
end
it 'should support inspecting transitions to other states' do
event.transitions_to_state(:running).map(&:from).should == [:sleeping]
event.transitions_to_state?(:running).should be_true
event.transitions_to_state(:cleaning).map(&:to).should == []
event.transitions_to_state?(:cleaning).should be_false
end
end
describe 'firing an event' do
it 'should return nil if the transitions are empty' do
obj = mock('object')
obj.stub!(:aasm_current_state)
event = AASM::Event.new(:event)
event.fire(obj).should be_nil
end
it 'should return the state of the first matching transition it finds' do
event = AASM::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
it 'should call the guard with the params passed in' do
event = AASM::Event.new(:event) do
transitions :to => :closed, :from => [:open, :received], :guard => :guard_fn
end
obj = mock('object')
obj.stub!(:aasm_current_state).and_return(:open)
obj.should_receive(:guard_fn).with('arg1', 'arg2').and_return(true)
event.fire(obj, nil, 'arg1', 'arg2').should == :closed
end
end
describe 'should fire callbacks' do
describe 'success' do
it "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 "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 "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 "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 { 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 "if it's a proc" do
ThisNameBetterNotBeInUse.instance_eval {
aasm_event :with_proc, :success => lambda { proc_success_callback } do
transitions :to => :proc, :from => [:initial]
end
}
model = ThisNameBetterNotBeInUse.new
model.should_receive(:proc_success_callback)
model.with_proc!
end
end
describe 'after' do
it "if they set different ways" do
ThisNameBetterNotBeInUse.instance_eval do
aasm_event :with_afters, :after => :do_one_thing_after do
after do
do_another_thing_after_too
end
after do
do_third_thing_at_last
end
transitions :to => :proc, :from => [:initial]
end
end
model = ThisNameBetterNotBeInUse.new
model.should_receive(:do_one_thing_after).once.ordered
model.should_receive(:do_another_thing_after_too).once.ordered
model.should_receive(:do_third_thing_at_last).once.ordered
model.with_afters!
end
end
describe 'before' do
it "if it's a proc" do
ThisNameBetterNotBeInUse.instance_eval do
aasm_event :before_as_proc do
before do
do_something_before
end
transitions :to => :proc, :from => [:initial]
end
end
model = ThisNameBetterNotBeInUse.new
model.should_receive(:do_something_before).once
model.before_as_proc!
end
end
it 'in right order' do
ThisNameBetterNotBeInUse.instance_eval do
aasm_event :in_right_order, :after => :do_something_after do
before do
do_something_before
end
transitions :to => :proc, :from => [:initial]
end
end
model = ThisNameBetterNotBeInUse.new
model.should_receive(:do_something_before).once.ordered
model.should_receive(:do_something_after).once.ordered
model.in_right_order!
end
end
describe 'parametrised events' do
let(:pe) {ParametrisedEvent.new}
it 'should transition to specified next state (sleeping to showering)' do
pe.wakeup!(:showering)
pe.aasm_current_state.should == :showering
end
it 'should transition to specified next state (sleeping to working)' do
pe.wakeup!(:working)
pe.aasm_current_state.should == :working
end
it 'should transition to default (first or showering) state' do
pe.wakeup!
pe.aasm_current_state.should == :showering
end
it 'should transition to default state when on_transition invoked' do
pe.dress!(nil, 'purple', 'dressy')
pe.aasm_current_state.should == :working
end
it 'should call on_transition method with args' do
pe.wakeup!(:showering)
pe.should_receive(:wear_clothes).with('blue', 'jeans')
pe.dress!(:working, 'blue', 'jeans')
end
it 'should call on_transition proc' do
pe.wakeup!(:showering)
pe.should_receive(:wear_clothes).with('purple', 'slacks')
pe.dress!(:dating, 'purple', 'slacks')
end
it 'should call on_transition with an array of methods' do
pe.wakeup!(:showering)
pe.should_receive(:condition_hair)
pe.should_receive(:fix_hair)
pe.dress!(:prettying_up)
end
end
describe 'event firing without persistence' do
it 'should attempt to persist if aasm_write_state is defined' do
foo = Foo.new
def foo.aasm_write_state; end
foo.should be_open
foo.should_receive(:aasm_write_state_without_persistence)
foo.close
end
end
================================================
FILE: spec/unit/initial_state_spec.rb
================================================
require 'spec_helper'
class Banker
include AASM
aasm do
state :retired
state :selling_bad_mortgages
end
aasm_initial_state Proc.new { |banker| banker.rich? ? :retired : :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 'initial states' do
let(:bar) {Bar.new}
it 'should use the first state defined if no initial state is given' do
bar.aasm_current_state.should == :read
# bar.aasm.current_state.should == :read # not yet supported
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
================================================
FILE: spec/unit/inspection_spec.rb
================================================
require 'spec_helper'
describe 'inspection for common cases' do
it 'should support the old DSL' do
Foo.should respond_to(:aasm_states)
Foo.aasm_states.should include(:open)
Foo.aasm_states.should include(:closed)
Foo.should respond_to(:aasm_initial_state)
Foo.aasm_initial_state.should == :open
Foo.should respond_to(:aasm_events)
Foo.aasm_events.should include(:close)
Foo.aasm_events.should include(:null)
end
it 'should support the new DSL' do
Foo.aasm.should respond_to(:states)
Foo.aasm.states.should include(:open)
Foo.aasm.states.should include(:closed)
Foo.aasm.should respond_to(:initial_state)
Foo.aasm.initial_state.should == :open
Foo.aasm.should respond_to(:events)
Foo.aasm.events.should include(:close)
Foo.aasm.events.should include(:null)
end
context "instance level inspection" do
let(:foo) { Foo.new }
let(:two) { FooTwo.new }
it "delivers all states" do
states = foo.aasm.states
states.should include(:open)
states.should include(:closed)
states = foo.aasm.states(:permissible => true)
states.should include(:closed)
states.should_not include(:open)
foo.close
foo.aasm.states(:permissible => true).should be_empty
end
it "delivers all states for subclasses" do
states = two.aasm.states
states.should include(:open)
states.should include(:closed)
states.should include(:foo)
states = two.aasm.states(:permissible => true)
states.should include(:closed)
states.should_not include(:open)
two.close
two.aasm.states(:permissible => true).should be_empty
end
it "delivers all events" do
events = foo.aasm.events
events.should include(:close)
events.should include(:null)
foo.close
foo.aasm.events.should be_empty
end
end
it 'should list states in the order they have been defined' do
Conversation.aasm.states.should == [:needs_attention, :read, :closed, :awaiting_response, :junk]
end
end
describe "special cases" do
it "should support valid a state name" do
Argument.aasm_states.should include(:invalid)
Argument.aasm_states.should include(:valid)
argument = Argument.new
argument.invalid?.should be_true
argument.aasm_current_state.should == :invalid
argument.valid!
argument.valid?.should be_true
argument.aasm_current_state.should == :valid
end
end
describe :aasm_states_for_select do
it "should return a select friendly array of states" do
Foo.should respond_to(:aasm_states_for_select)
Foo.aasm_states_for_select.should == [['Open', 'open'], ['Closed', 'closed']]
end
end
describe :aasm_from_states_for_state do
it "should return all from states for a state" do
AuthMachine.should respond_to(:aasm_from_states_for_state)
froms = AuthMachine.aasm_from_states_for_state(:active)
[:pending, :passive, :suspended].each {|from| froms.should include(from)}
end
it "should return from states for a state for a particular transition only" do
froms = AuthMachine.aasm_from_states_for_state(:active, :transition => :unsuspend)
[:suspended].each {|from| froms.should include(from)}
end
end
describe 'permissible events' do
let(:foo) {Foo.new}
it 'work' do
foo.aasm.permissible_events.should include(:close)
foo.aasm.permissible_events.should_not include(:null)
end
end
================================================
FILE: spec/unit/localizer_spec.rb
================================================
require 'spec_helper'
require 'active_record'
require 'logger'
require 'i18n'
load_schema
class LocalizerTestModel < ActiveRecord::Base
include AASM
attr_accessor :aasm_state
aasm_initial_state :opened
aasm_state :opened
aasm_state :closed
aasm_event :close
aasm_event :open
end
describe 'localized state names' do
before(:all) do
I18n.load_path << 'spec/en.yml'
I18n.default_locale = :en
I18n.reload!
end
after(:all) do
I18n.load_path.clear
end
it 'should localize' do
LocalizerTestModel.aasm.states.detect {|s| s == :opened}.localized_name.should == "It's open now!"
end
it 'should use fallback' do
LocalizerTestModel.aasm.states.detect {|s| s == :closed}.localized_name.should == 'Closed'
end
end
describe AASM::Localizer, "new style" do
before(:all) do
I18n.load_path << 'spec/en.yml'
I18n.default_locale = :en
I18n.reload!
end
after(:all) do
I18n.load_path.clear
end
let (:foo_opened) { LocalizerTestModel.new }
let (:foo_closed) { LocalizerTestModel.new.tap { |x| x.aasm_state = :closed } }
context 'aasm_human_state' do
it 'should return translated state value' do
foo_opened.aasm_human_state.should == "It's open now!"
end
it 'should return humanized value if not localized' do
foo_closed.aasm_human_state.should == "Closed"
end
end
context 'aasm_human_event_name' do
it 'should return translated event name' do
LocalizerTestModel.aasm_human_event_name(:close).should == "Let's close it!"
end
it 'should return humanized event name' do
LocalizerTestModel.aasm_human_event_name(:open).should == "Open"
end
end
end
describe AASM::Localizer, "deprecated style" do
before(:all) do
I18n.load_path << 'spec/en_deprecated_style.yml'
I18n.default_locale = :en
I18n.reload!
end
after(:all) do
I18n.load_path.clear
end
let (:foo_opened) { LocalizerTestModel.new }
let (:foo_closed) { LocalizerTestModel.new.tap { |x| x.aasm_state = :closed } }
context 'aasm_human_state' do
it 'should return translated state value' do
foo_opened.aasm_human_state.should == "It's open now!"
end
it 'should return humanized value if not localized' do
foo_closed.aasm_human_state.should == "Closed"
end
end
context 'aasm_human_event_name' do
it 'should return translated event name' do
LocalizerTestModel.aasm_human_event_name(:close).should == "Let's close it!"
end
it 'should return humanized event name' do
LocalizerTestModel.aasm_human_event_name(:open).should == "Open"
end
end
end
================================================
FILE: spec/unit/memory_leak_spec.rb
================================================
# require 'spec_helper'
# describe "state machines" do
# def number_of_objects(clazz)
# ObjectSpace.each_object(clazz) {}
# end
# def machines
# AASM::StateMachine.instance_variable_get("@machines")
# end
# it "should be created without memory leak" do
# machines_count = machines.size
# state_count = number_of_objects(AASM::State)
# event_count = number_of_objects(AASM::Event)
# puts "event_count = #{event_count}"
# transition_count = number_of_objects(AASM::Transition)
# load File.expand_path(File.dirname(__FILE__) + '/../models/not_auto_loaded/process.rb')
# machines.size.should == machines_count + 1 # + Process
# number_of_objects(Models::Process).should == 0
# number_of_objects(AASM::State).should == state_count + 3 # + Process
# puts "event_count = #{number_of_objects(AASM::Event)}"
# number_of_objects(AASM::Event).should == event_count + 2 # + Process
# number_of_objects(AASM::Transition).should == transition_count + 2 # + Process
# Models.send(:remove_const, "Process") if Models.const_defined?("Process")
# load File.expand_path(File.dirname(__FILE__) + '/../models/not_auto_loaded/process.rb')
# machines.size.should == machines_count + 1 # + Process
# number_of_objects(AASM::State).should == state_count + 3 # + Process
# # ObjectSpace.each_object(AASM::Event) {|o| puts o.inspect}
# puts "event_count = #{number_of_objects(AASM::Event)}"
# number_of_objects(AASM::Event).should == event_count + 2 # + Process
# number_of_objects(AASM::Transition).should == transition_count + 2 # + Process
# end
# end
================================================
FILE: spec/unit/new_dsl_spec.rb
================================================
require 'spec_helper'
describe "the new dsl" do
let(:process) {ProcessWithNewDsl.new}
it 'should not conflict with other event or state methods' do
lambda {ProcessWithNewDsl.state}.should raise_error(RuntimeError, "wrong state method")
lambda {ProcessWithNewDsl.event}.should raise_error(RuntimeError, "wrong event method")
end
end
================================================
FILE: spec/unit/persistence/active_record_persistence_spec.rb
================================================
require 'active_record'
require 'logger'
require 'spec_helper'
load_schema
# if you want to see the statements while running the spec enable the following line
# ActiveRecord::Base.logger = Logger.new(STDERR)
shared_examples_for "aasm model" do
it "should include persistence mixins" do
klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence)
klass.included_modules.should be_include(AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
end
end
describe "instance methods" do
let(:gate) {Gate.new}
it "should respond to aasm persistence methods" do
gate.should respond_to(:aasm_read_state)
gate.should respond_to(:aasm_write_state)
gate.should respond_to(:aasm_write_state_without_persistence)
end
it "should return the initial state when new and the aasm field is nil" do
gate.aasm_current_state.should == :opened
end
it "should return the aasm column when new and the aasm field is not nil" do
gate.aasm_state = "closed"
gate.aasm_current_state.should == :closed
end
it "should return the aasm column when not new and the aasm_column is not nil" do
gate.stub!(:new_record?).and_return(false)
gate.aasm_state = "state"
gate.aasm_current_state.should == :state
end
it "should allow a nil state" do
gate.stub!(:new_record?).and_return(false)
gate.aasm_state = nil
gate.aasm_current_state.should be_nil
end
it "should call aasm_ensure_initial_state on validation before create" do
gate.should_receive(:aasm_ensure_initial_state).and_return(true)
gate.valid?
end
it "should not call aasm_ensure_initial_state on validation before update" do
gate.stub!(:new_record?).and_return(false)
gate.should_not_receive(:aasm_ensure_initial_state)
gate.valid?
end
end
describe 'subclasses' do
it "should have the same states as its parent class" do
Derivate.aasm_states.should == Simple.aasm_states
end
it "should have the same events as its parent class" do
Derivate.aasm_events.should == Simple.aasm_events
end
it "should have the same column as its parent class" do
Derivate.aasm_column.should == :status
end
it "should have the same column as its parent even for the new dsl" do
SimpleNewDsl.aasm_column.should == :status
DerivateNewDsl.aasm_column.should == :status
end
end
describe "named scopes with the old DSL" do
context "Does not already respond_to? the scope name" do
it "should add a scope" do
Simple.should respond_to(:unknown_scope)
SimpleNewDsl.unknown_scope.is_a?(ActiveRecord::Relation).should be_true
end
end
context "Already respond_to? the scope name" do
it "should not add a scope" do
Simple.should respond_to(:new)
Simple.new.class.should == Simple
end
end
end
describe "named scopes with the new DSL" do
context "Does not already respond_to? the scope name" do
it "should add a scope" do
SimpleNewDsl.should respond_to(:unknown_scope)
SimpleNewDsl.unknown_scope.is_a?(ActiveRecord::Relation).should be_true
end
end
context "Already respond_to? the scope name" do
it "should not add a scope" do
SimpleNewDsl.should respond_to(:new)
SimpleNewDsl.new.class.should == SimpleNewDsl
end
end
end
describe 'initial states' do
it 'should support conditions' do
Thief.new(:skilled => true).aasm_current_state.should == :rich
Thief.new(:skilled => false).aasm_current_state.should == :jailed
end
end
describe 'transitions with persistence' do
it "should work for valid models" do
valid_object = Validator.create(:name => 'name')
valid_object.should be_sleeping
valid_object.status = :running
valid_object.should be_running
end
it 'should not store states for invalid models' do
validator = Validator.create(:name => 'name')
validator.should be_valid
validator.should be_sleeping
validator.name = nil
validator.should_not be_valid
validator.run!.should be_false
validator.should be_sleeping
validator.reload
validator.should_not be_running
validator.should be_sleeping
validator.name = 'another name'
validator.should be_valid
validator.run!.should be_true
validator.should be_running
validator.reload
validator.should be_running
validator.should_not be_sleeping
end
it 'should store states for invalid models if configured' do
persistor = InvalidPersistor.create(:name => 'name')
persistor.should be_valid
persistor.should be_sleeping
persistor.name = nil
persistor.should_not be_valid
persistor.run!.should be_true
persistor.should be_running
persistor = InvalidPersistor.find(persistor.id)
persistor.valid?
persistor.should be_valid
persistor.should be_running
persistor.should_not be_sleeping
persistor.reload
persistor.should be_running
persistor.should_not be_sleeping
end
describe 'transactions' do
it 'should rollback all changes' do
worker = Worker.create!(:name => 'worker', :status => 'sleeping')
transactor = Transactor.create!(:name => 'transactor', :worker => worker)
transactor.should be_sleeping
worker.status.should == 'sleeping'
lambda {transactor.run!}.should raise_error(StandardError, 'failed on purpose')
transactor.should be_running
worker.reload.status.should == 'sleeping'
end
end
end
================================================
FILE: spec/unit/persistence/mongoid_persistance_spec.rb
================================================
describe 'mongoid', :if => Gem::Version.create(RUBY_VERSION.dup) >= Gem::Version.create('1.9.3') do
# describe 'mongoid' do
before(:all) do
require 'mongoid'
require 'logger'
require 'spec_helper'
Dir[File.dirname(__FILE__) + "/../../models/mongoid/*.rb"].sort.each { |f| require File.expand_path(f) }
# if you want to see the statements while running the spec enable the following line
# Mongoid.logger = Logger.new(STDERR)
DATABASE_NAME = "mongoid_#{Process.pid}"
Mongoid.configure do |config|
config.connect_to DATABASE_NAME
end
end
after do
Mongoid.purge!
end
describe "named scopes with the old DSL" do
context "Does not already respond_to? the scope name" do
it "should add a scope" do
SimpleMongoid.should respond_to(:unknown_scope)
SimpleMongoid.unknown_scope.class.should == Mongoid::Criteria
end
end
context "Already respond_to? the scope name" do
it "should not add a scope" do
SimpleMongoid.should respond_to(:new)
SimpleMongoid.new.class.should == SimpleMongoid
end
end
end
describe "named scopes with the new DSL" do
context "Does not already respond_to? the scope name" do
it "should add a scope" do
SimpleNewDslMongoid.should respond_to(:unknown_scope)
SimpleNewDslMongoid.unknown_scope.class.should == Mongoid::Criteria
end
end
context "Already respond_to? the scope name" do
it "should not add a scope" do
SimpleNewDslMongoid.should respond_to(:new)
SimpleNewDslMongoid.new.class.should == SimpleNewDslMongoid
end
end
end
describe "#find_in_state" do
let!(:model) { SimpleNewDslMongoid.create!(:status => :unknown_scope) }
let!(:model_id) { model._id }
it "should respond to method" do
SimpleNewDslMongoid.should respond_to(:find_in_state)
end
it "should find the model when given the correct scope and model id" do
SimpleNewDslMongoid.find_in_state(model_id, 'unknown_scope').class.should == SimpleNewDslMongoid
SimpleNewDslMongoid.find_in_state(model_id, 'unknown_scope').should == model
end
it "should raise DocumentNotFound error when given incorrect scope" do
expect {SimpleNewDslMongoid.find_in_state(model_id, 'new')}.to raise_error Mongoid::Errors::DocumentNotFound
end
it "should raise DocumentNotFound error when given incorrect model id" do
expect {SimpleNewDslMongoid.find_in_state('bad_id', 'unknown_scope')}.to raise_error Mongoid::Errors::DocumentNotFound
end
end
describe "#count_in_state" do
before do
3.times { SimpleNewDslMongoid.create!(:status => :unknown_scope) }
end
it "should respond to method" do
SimpleNewDslMongoid.should respond_to(:count_in_state)
end
it "should return n for a scope with n records persisted" do
SimpleNewDslMongoid.count_in_state('unknown_scope').class.should == Fixnum
SimpleNewDslMongoid.count_in_state('unknown_scope').should == 3
end
it "should return zero for a scope without records persisted" do
SimpleNewDslMongoid.count_in_state('new').class.should == Fixnum
SimpleNewDslMongoid.count_in_state('new').should == 0
end
end
describe "#with_state_scope" do
before do
3.times { SimpleNewDslMongoid.create!(:status => :unknown_scope) }
2.times { SimpleNewDslMongoid.create!(:status => :new) }
end
it "should respond to method" do
SimpleNewDslMongoid.should respond_to(:with_state_scope)
end
it "should correctly process block" do
SimpleNewDslMongoid.with_state_scope('unknown_scope') do
SimpleNewDslMongoid.count
end.should == 3
SimpleNewDslMongoid.with_state_scope('new') do
SimpleNewDslMongoid.count
end.should == 2
end
end
end
================================================
FILE: spec/unit/simple_example_spec.rb
================================================
require 'spec_helper'
class Payment
include AASM
aasm do
state :initialised, :initial => true
state :filled_out
state :authorised
event :fill_out do
transitions :from => :initialised, :to => :filled_out
end
event :authorise do
transitions :from => :filled_out, :to => :authorised
end
end
end
describe 'state machine' do
let(:payment) {Payment.new}
it 'starts with an initial state' do
payment.aasm_current_state.should == :initialised
# payment.aasm.current_state.should == :initialised # not yet supported
payment.should respond_to(:initialised?)
payment.should be_initialised
end
it 'allows transitions to other states' do
payment.should respond_to(:fill_out)
payment.should respond_to(:fill_out!)
payment.fill_out!
payment.should respond_to(:filled_out?)
payment.should be_filled_out
payment.should respond_to(:authorise)
payment.should respond_to(:authorise!)
payment.authorise
payment.should respond_to(:authorised?)
payment.should be_authorised
end
it 'denies transitions to other states' do
lambda {payment.authorise}.should raise_error(AASM::InvalidTransition)
lambda {payment.authorise!}.should raise_error(AASM::InvalidTransition)
payment.fill_out
lambda {payment.fill_out}.should raise_error(AASM::InvalidTransition)
lambda {payment.fill_out!}.should raise_error(AASM::InvalidTransition)
payment.authorise
lambda {payment.fill_out}.should raise_error(AASM::InvalidTransition)
lambda {payment.fill_out!}.should raise_error(AASM::InvalidTransition)
end
it 'defines constants for each state name' do
Payment::STATE_INITIALISED.should eq(:initialised)
Payment::STATE_FILLED_OUT.should eq(:filled_out)
Payment::STATE_AUTHORISED.should eq(:authorised)
end
end
================================================
FILE: spec/unit/state_spec.rb
================================================
require 'spec_helper'
describe AASM::State do
before(:each) do
@name = :astate
@options = { :crazy_custom_key => 'key' }
end
def new_state(options={})
AASM::State.new(@name, Conversation, @options.merge(options))
end
it 'should set the name' do
state = new_state
state.name.should == :astate
end
it 'should set the display_name from name' do
new_state.display_name.should == 'Astate'
end
it 'should set the display_name from options' do
new_state(:display => "A State").display_name.should == 'A State'
end
it 'should set the options and expose them as options' do
new_state.options.should == @options
end
it 'should be equal to a symbol of the same name' do
new_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.fire_callbacks(: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.fire_callbacks(: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.fire_callbacks(: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.fire_callbacks(: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.fire_callbacks(:entering, record)
end
end
================================================
FILE: spec/unit/subclassing_spec.rb
================================================
require 'spec_helper'
describe 'subclassing' do
let(:son) {Son.new}
it 'should have the parent states' do
Foo.aasm_states.each do |state|
FooTwo.aasm_states.should include(state)
end
Baz.aasm_states.should == Bar.aasm_states
end
it 'should not add the child states to the parent machine' do
Foo.aasm_states.should_not include(:foo)
end
it "should have the same events as its parent" do
Baz.aasm_events.should == Bar.aasm_events
end
it 'should know how to respond to `may_add_details?`' do
son.may_add_details?.should be_true
end
it 'should not break if I call Son#update_state' do
son.update_state
son.aasm_current_state.should == :pending_details_confirmation
end
end
================================================
FILE: spec/unit/transition_spec.rb
================================================
require 'spec_helper'
describe 'transitions' do
it 'should raise an exception when whiny' do
process = ProcessWithNewDsl.new
lambda { process.stop! }.should raise_error(AASM::InvalidTransition)
process.should be_sleeping
end
it 'should not raise an exception when not whiny' do
silencer = Silencer.new
silencer.smile!.should be_false
silencer.should be_silent
end
it 'should not raise an exception when superclass not whiny' do
sub = SubClassing.new
sub.smile!.should be_false
sub.should be_silent
end
it 'should not raise an exception when from is nil even if whiny' do
silencer = Silencer.new
silencer.smile_any!.should be_true
silencer.should be_smiling
end
end
describe AASM::Transition do
it 'should set from, to, and opts attr readers' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::Transition.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::Transition.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::Transition.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::Transition.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::Transition, '- when performing guard checks' do
it 'should return true of there is no guard' do
opts = {:from => 'foo', :to => 'bar'}
st = AASM::Transition.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::Transition.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::Transition.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::Transition.new(opts)
obj = mock('object')
obj.should_receive(:test)
st.perform(obj)
end
end
describe AASM::Transition, '- when executing the transition with a Proc' do
it 'should call a Proc on the object with args' do
opts = {:from => 'foo', :to => 'bar', :on_transition => Proc.new {|o| o.test}}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
opts[:on_transition].should_receive(:call).with(any_args)
st.execute(obj, args)
end
it 'should call a Proc on the object without args' do
opts = {:from => 'foo', :to => 'bar', :on_transition => Proc.new {||}}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
opts[:on_transition].should_receive(:call).with(no_args)
st.execute(obj, args)
end
end
describe AASM::Transition, '- when executing the transition with an :on_transtion method call' do
it 'should accept a String for the method name' do
opts = {:from => 'foo', :to => 'bar', :on_transition => 'test'}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
obj.should_receive(:test)
st.execute(obj, args)
end
it 'should accept a Symbol for the method name' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
obj.should_receive(:test)
st.execute(obj, args)
end
it 'should pass args if the target method accepts them' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
obj.class.class_eval do
define_method(:test) {|*args| 'success'}
end
return_value = st.execute(obj, args)
return_value.should == 'success'
end
it 'should NOT pass args if the target method does NOT accept them' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = mock('object')
obj.class.class_eval do
define_method(:test) {|*args| 'success'}
end
return_value = st.execute(obj, args)
return_value.should == 'success'
end
end