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 [![Build Status](https://secure.travis-ci.org/aasm/aasm.png)](http://travis-ci.org/aasm/aasm) [![Code Climate](https://codeclimate.com/github/aasm/aasm.png)](https://codeclimate.com/github/aasm/aasm) [![Coverage Status](https://coveralls.io/repos/aasm/aasm/badge.png?branch=master)](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