aq1018-acts_as_multiple_state_machines 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-01-09
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ rails/init.rb
6
+ lib/acts_as_multiple_state_machines.rb
7
+ lib/acts/as/multiple/state_machines/active_record_extension.rb
8
+ lib/acts/as/multiple/state_machines/exceptions.rb
9
+ lib/acts/as/multiple/state_machines/supporting_classes.rb
10
+ lib/acts/as/multiple/state_machines/supporting_classes/event.rb
11
+ lib/acts/as/multiple/state_machines/supporting_classes/state.rb
12
+ lib/acts/as/multiple/state_machines/supporting_classes/state_machine.rb
13
+ lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory.rb
14
+ lib/acts/as/multiple/state_machines/supporting_classes/state_transition.rb
15
+ test/test_acts_as_multiple_state_machines.rb
16
+ test/fixtures/conversations.yml
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = ActsAsMultipleStateMachines
2
+
3
+ * FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2009 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'spec/rake/spectask'
4
+ require 'pathname'
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+
8
+ require 'lib/acts_as_multiple_state_machines.rb'
9
+
10
+ AUTHOR = "Aaron Qian"
11
+ EMAIL = "aaron [a] ekohe [d] com"
12
+ GEM_NAME = "acts_as_multiple_state_machines"
13
+ GEM_VERSION = Acts::As::Multiple::StateMachines::VERSION
14
+
15
+ GEM_CLEAN = ["log", "pkg"]
16
+ GEM_EXTRAS = { :has_rdoc => true }
17
+
18
+ PROJECT_NAME = "acts_as_multiple_state_machines"
19
+ PROJECT_URL = "http://acts-as-multiple-state-machines.rubyforge.com"
20
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY = "similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model"
21
+
22
+ Hoe.new(GEM_NAME, GEM_VERSION) do |p|
23
+ p.rubyforge_name = "acts-as-multiple-state-machines"
24
+ p.developer('Aaron Qian', 'aaron [a] ekohe [d] com')
25
+ end
26
+
27
+ desc 'Default: run unit tests.'
28
+ task :default => [:clean_db, :test]
29
+
30
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
31
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
32
+
33
+ desc "Install #{GEM_NAME} #{GEM_VERSION}"
34
+ task :install => [ :package ] do
35
+ sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
36
+ end
37
+
38
+ desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
39
+ task :uninstall => [ :clobber ] do
40
+ sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
41
+ end
42
+
43
+ desc 'Run specifications'
44
+ Spec::Rake::SpecTask.new(:spec) do |t|
45
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
46
+ t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
47
+
48
+ begin
49
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
50
+ t.rcov_opts << '--exclude' << 'spec'
51
+ t.rcov_opts << '--text-summary'
52
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
53
+ rescue Exception
54
+ # rcov not installed
55
+ end
56
+ end
57
+
58
+ desc 'Remove the stale db file'
59
+ task :clean_db do
60
+ `rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
61
+ end
62
+
63
+ desc 'Test the acts as multiple state machines plugin.'
64
+ Rake::TestTask.new(:test) do |t|
65
+ t.libs << 'lib'
66
+ t.pattern = 'test/**/test_*.rb'
67
+ t.verbose = true
68
+ end
@@ -0,0 +1,86 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ def self.included(base) #:nodoc:
6
+ base.extend ActMacro
7
+ end
8
+
9
+ module ActMacro
10
+ # Configuration options are
11
+ #
12
+ # * +column+ - specifies the column name to use for keeping the state (default: state)
13
+ # * +initial+ - specifies an initial state for newly created objects (required)
14
+ def acts_as_state_machine(options = {}, &block)
15
+ class_eval do
16
+ extend ClassMethods
17
+ include InstanceMethods
18
+
19
+ # make sure only does it once...
20
+ unless respond_to?(:state_machine_classes)
21
+ write_inheritable_attribute :state_machine_classes, {}
22
+ class_inheritable_reader :state_machine_classes
23
+ before_create :set_initial_state
24
+ after_create :run_initial_state_actions
25
+ end
26
+
27
+ name = options.delete(:name) || 'default'
28
+ name = name.to_sym
29
+
30
+ raise DuplicateStateMachine unless state_machine_classes[name].nil?
31
+ state_machine_classes[name] = SupportingClasses::StateMachineFactory.build(name, self, options, &block)
32
+ end
33
+
34
+ end
35
+
36
+ module ClassMethods
37
+ def state_machines
38
+ read_inheritable_attribute :state_machine_classes
39
+ end
40
+
41
+ def find_in_state(state_machine_name, number, state, *args)
42
+ with_state_scope state_machine_name, state do
43
+ find(number, *args)
44
+ end
45
+ end
46
+
47
+ def count_in_state(state_machine_name, state, *args)
48
+ with_state_scope state_machine_name, state do
49
+ count(*args)
50
+ end
51
+ end
52
+
53
+ def calculate_in_state(state_machine_name, state, *args)
54
+ with_state_scope state_machine_name, state do
55
+ calculate(*args)
56
+ end
57
+ end
58
+
59
+ def with_state_scope(state_machine_name, state)
60
+ sm = state_machine_classes[state_machine_name.to_sym]
61
+ raise InvalidState unless sm.states.include?(state.to_sym)
62
+
63
+ with_scope :find => {:conditions => ["#{table_name}.#{sm.state_column} = ?", state.to_s]} do
64
+ yield if block_given?
65
+ end
66
+ end
67
+ end
68
+
69
+ module InstanceMethods
70
+ def state_machines
71
+ @state_machines ||= state_machine_classes.inject({}) {|sm, (name,klass) | sm[name] = klass.new(self); sm}
72
+ end
73
+
74
+ def set_initial_state
75
+ state_machines.values.each(&:set_initial_state)
76
+ end
77
+
78
+ def run_initial_state_actions
79
+ state_machines.values.each(&:run_initial_state_actions)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,17 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ class InvalidState < Exception #:nodoc:
6
+ end
7
+
8
+ class NoInitialState < Exception #:nodoc:
9
+ end
10
+
11
+ class DuplicateStateMachine < Exception #:nodoc:
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ # Default transition action. Always returns true.
7
+ NOOP = lambda { |o| true }
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ class Event
7
+
8
+ attr_reader :name
9
+ attr_reader :transitions
10
+ attr_reader :opts
11
+ attr_reader :sm
12
+
13
+ def initialize(state_machine, name, opts, transition_table, &block)
14
+ @name = name.to_sym
15
+ @transitions = transition_table[@name] = []
16
+ @sm = state_machine
17
+
18
+ instance_eval(&block) if block
19
+ @opts = opts
20
+ @opts.freeze
21
+ @transitions.freeze
22
+ freeze
23
+ end
24
+
25
+ def next_states(record)
26
+ @transitions.select { |t| t.from == sm.current_state(record).to_s }
27
+ end
28
+
29
+ def fire(record)
30
+ next_states(record).each do |transition|
31
+ break true if transition.perform(record)
32
+ end
33
+ end
34
+
35
+ def transitions(trans_opts)
36
+ Array(trans_opts[:from]).each do |s|
37
+ @transitions << SupportingClasses::StateTransition.new(sm, trans_opts.merge({:from => s.to_sym}))
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ class State
7
+
8
+ attr_reader :name, :value, :sm
9
+
10
+ def initialize(state_machine, name, options)
11
+ @name = name.to_sym
12
+ @value = (options[:value] || @name).to_s
13
+ @after = Array(options[:after])
14
+ @enter = options[:enter] || NOOP
15
+ @exit = options[:exit] || NOOP
16
+ @sm = state_machine
17
+ end
18
+
19
+ def entering(record)
20
+ sm.send(:run_transition_action, record, @enter)
21
+ end
22
+
23
+ def entered(record)
24
+ @after.each { |action| sm.send(:run_transition_action, record, action) }
25
+ end
26
+
27
+ def exited(record)
28
+ sm.send(:run_transition_action, record, @exit)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,112 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ class StateMachine
7
+ class_inheritable_reader :name,
8
+ :initial_state,
9
+ :state_column,
10
+ :state_map,
11
+ :transition_table,
12
+ :event_table
13
+
14
+ attr_reader :record
15
+
16
+ def initialize(record)
17
+ @record = record
18
+ end
19
+
20
+ class << self
21
+ def states
22
+ state_map.keys.collect { |state| state.to_sym }
23
+ end
24
+
25
+ def current_state(record)
26
+ record.send(state_column).to_sym
27
+ end
28
+
29
+ def event(event, opts={}, &block)
30
+ e = SupportingClasses::Event.new(self, event, opts, transition_table, &block)
31
+ event_table[event.to_sym] = e
32
+ class_eval "
33
+ def #{event}!
34
+ event_table[:#{event}].fire(record)
35
+ end
36
+ "
37
+ end
38
+
39
+ def state(name, opts={})
40
+ state = SupportingClasses::State.new(self, name, opts)
41
+ state_map[state.value] = state
42
+ state_name = state.name
43
+ class_eval "
44
+ def #{state.name}?
45
+ current_state.to_s == state_map['#{state.value}'].value
46
+ end
47
+ "
48
+ end
49
+
50
+ private
51
+ def set_config_options(name, options)
52
+ write_inheritable_attribute :name, name
53
+ write_inheritable_attribute :state_map, {}
54
+ write_inheritable_attribute :initial_state, options[:initial]
55
+ write_inheritable_attribute :transition_table, {}
56
+ write_inheritable_attribute :event_table, {}
57
+ write_inheritable_attribute :state_column, options[:column] || 'state'
58
+ end
59
+
60
+ def freeze_config!
61
+ state_map.freeze
62
+ transition_table.freeze
63
+ event_table.freeze
64
+ end
65
+
66
+ def configure(name, options, &block)
67
+ set_config_options(name, options)
68
+ instance_eval(&block)
69
+ freeze_config!
70
+ end
71
+
72
+ def run_transition_action(record, action)
73
+ Symbol === action ? record.method(action).call : action.call(record)
74
+ end
75
+ end
76
+
77
+ def set_initial_state
78
+ record.write_attribute state_column, initial_state.to_s
79
+ end
80
+
81
+ def run_initial_state_actions
82
+ initial = state_map[initial_state.to_s]
83
+ initial.entering(record)
84
+ initial.entered(record)
85
+ end
86
+
87
+ # Returns the current state the object is in, as a Ruby symbol.
88
+ def current_state
89
+ self.class.current_state(record)
90
+ end
91
+
92
+ # Returns what the next state for a given event would be, as a Ruby symbol.
93
+ def next_state_for_event(event)
94
+ ns = next_states_for_event(event)
95
+ ns.empty? ? nil : ns.first.to.to_sym
96
+ end
97
+
98
+ def next_states_for_event(event)
99
+ transition_table[event.to_sym].select do |s|
100
+ s.from == current_state.to_s
101
+ end
102
+ end
103
+
104
+ def states
105
+ self.class.states
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,25 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ class StateMachineFactory
7
+ class << self
8
+ def build(name, model, options, &block)
9
+ raise NoInitialState unless options[:initial]
10
+ klass = model.const_set(state_machine_class_name(name), Class.new(SupportingClasses::StateMachine))
11
+ klass.send :configure, name, options, &block
12
+ klass
13
+ end
14
+
15
+ def state_machine_class_name(name)
16
+ "#{name.to_s.classify}StateMachine"
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ module SupportingClasses
6
+ class StateTransition
7
+
8
+ attr_reader :from, :to, :opts, :sm
9
+
10
+ def initialize(state_machine, options)
11
+ @from = options[:from].to_s
12
+ @to = options[:to].to_s
13
+ @guard = options[:guard] || NOOP
14
+ @opts = options
15
+ @sm = state_machine
16
+ end
17
+
18
+ def guard(obj)
19
+ @guard ? sm.send(:run_transition_action, obj, @guard) : true
20
+ end
21
+
22
+ def perform(record)
23
+ return false unless guard(record)
24
+ loopback = sm.current_state(record).to_s == to
25
+ states = sm.state_map
26
+ next_state = states[to]
27
+ old_state = states[sm.current_state(record).to_s]
28
+
29
+ next_state.entering(record) unless loopback
30
+
31
+ record.update_attribute(sm.state_column, next_state.value)
32
+
33
+ next_state.entered(record) unless loopback
34
+ old_state.exited(record) unless loopback
35
+ true
36
+ end
37
+
38
+ def ==(obj)
39
+ @from == obj.from && @to == obj.to
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ module Acts
2
+ module As
3
+ module Multiple
4
+ module StateMachines
5
+ VERSION = '0.1'
6
+ end
7
+ end
8
+ end
9
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + '/../lib/acts_as_multiple_state_machines'
2
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/exceptions'
3
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes'
4
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/event'
5
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state'
6
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_transition'
7
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_machine'
8
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory'
9
+ require File.dirname(__FILE__) + '/../lib/acts/as/multiple/state_machines/active_record_extension'
10
+
11
+ ActiveRecord::Base.class_eval do
12
+ include ::Acts::As::Multiple::StateMachines
13
+ end
@@ -0,0 +1,11 @@
1
+ first:
2
+ id: 1
3
+ state_machine: read
4
+ subject: This is a test
5
+ closed: false
6
+
7
+ second:
8
+ id: 2
9
+ state_machine: read
10
+ subject: Foo
11
+ closed: false
@@ -0,0 +1,697 @@
1
+ RAILS_ROOT = File.dirname(__FILE__)
2
+
3
+ require "rubygems"
4
+ require "test/unit"
5
+ require "active_record"
6
+ require "active_record/fixtures"
7
+
8
+ $:.unshift File.dirname(__FILE__) + "/../lib"
9
+ require File.dirname(__FILE__) + "/../rails/init"
10
+
11
+ # Log everything to a global StringIO object instead of a file.
12
+ require "stringio"
13
+ $LOG = StringIO.new
14
+ $LOGGER = Logger.new($LOG)
15
+ ActiveRecord::Base.logger = $LOGGER
16
+
17
+ ActiveRecord::Base.configurations = {
18
+ "sqlite" => {
19
+ :adapter => "sqlite",
20
+ :dbfile => "state_machine.sqlite.db"
21
+ },
22
+
23
+ "sqlite3" => {
24
+ :adapter => "sqlite3",
25
+ :dbfile => ":memory:"
26
+ },
27
+
28
+ "mysql" => {
29
+ :adapter => "mysql",
30
+ :host => "localhost",
31
+ :username => "rails",
32
+ :password => nil,
33
+ :database => "state_machine_test"
34
+ },
35
+
36
+ "postgresql" => {
37
+ :min_messages => "ERROR",
38
+ :adapter => "postgresql",
39
+ :username => "postgres",
40
+ :password => "postgres",
41
+ :database => "state_machine_test"
42
+ }
43
+ }
44
+
45
+ # Connect to the database.
46
+ ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3")
47
+
48
+ # Create table for conversations.
49
+ ActiveRecord::Migration.verbose = false
50
+ ActiveRecord::Schema.define(:version => 1) do
51
+ create_table :conversations, :force => true do |t|
52
+ t.column :state_machine, :string
53
+ t.column :subject, :string
54
+ t.column :closed, :boolean
55
+ t.column :another_state_machine, :string
56
+ end
57
+ end
58
+
59
+ class Test::Unit::TestCase
60
+ self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
61
+ self.use_transactional_fixtures = true
62
+ self.use_instantiated_fixtures = false
63
+
64
+ def create_fixtures(*table_names, &block)
65
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names, &block)
66
+ end
67
+ end
68
+
69
+ class Conversation < ActiveRecord::Base
70
+ attr_writer :can_close
71
+ attr_accessor :read_enter, :read_exit,
72
+ :needs_attention_enter, :needs_attention_after,
73
+ :read_after_first, :read_after_second,
74
+ :closed_after
75
+
76
+ # How's THAT for self-documenting? ;-)
77
+ def always_true
78
+ true
79
+ end
80
+
81
+ def can_close?
82
+ !!@can_close
83
+ end
84
+
85
+ def read_enter_action
86
+ self.read_enter = true
87
+ end
88
+
89
+ def read_after_first_action
90
+ self.read_after_first = true
91
+ end
92
+
93
+ def read_after_second_action
94
+ self.read_after_second = true
95
+ end
96
+
97
+ def closed_after_action
98
+ self.closed_after = true
99
+ end
100
+ end
101
+
102
+ class ActsAsStateMachineTest < Test::Unit::TestCase
103
+ include ::Acts::As::Multiple::StateMachines
104
+ fixtures :conversations
105
+
106
+ def teardown
107
+ Conversation.class_eval do
108
+ state_machine_classes.keys.each do |name|
109
+ remove_const SupportingClasses::StateMachineFactory.state_machine_class_name(name)
110
+ end
111
+
112
+ write_inheritable_attribute :state_machine_classes, {}
113
+
114
+ # Clear out any callbacks that were set by acts_as_state_machine.
115
+ write_inheritable_attribute :before_create, []
116
+ write_inheritable_attribute :after_create, []
117
+ end
118
+ end
119
+
120
+ def test_no_initial_value_raises_exception
121
+ assert_raises(NoInitialState) do
122
+ Conversation.class_eval { acts_as_state_machine }
123
+ end
124
+ end
125
+
126
+ def test_state_column
127
+ Conversation.class_eval do
128
+ acts_as_state_machine(:initial => :needs_attention, :column => "state_machine") do
129
+ state :needs_attention
130
+ end
131
+ end
132
+
133
+ assert_equal "state_machine", Conversation.state_machines[:default].state_column
134
+ end
135
+
136
+ def test_initial_state_value
137
+ Conversation.class_eval do
138
+ acts_as_state_machine :initial => :needs_attention do
139
+ state :needs_attention
140
+ end
141
+ end
142
+
143
+ assert_equal :needs_attention, Conversation.state_machines[:default].initial_state
144
+ end
145
+
146
+ def test_initial_state
147
+ Conversation.class_eval do
148
+ acts_as_state_machine :initial => :needs_attention do
149
+ state :needs_attention
150
+ end
151
+ end
152
+
153
+ c = Conversation.create!
154
+ assert_equal :needs_attention, c.state_machines[:default].current_state
155
+ assert c.state_machines[:default].needs_attention?
156
+ end
157
+
158
+ def test_states_were_set
159
+ Conversation.class_eval do
160
+ acts_as_state_machine :initial => :needs_attention do
161
+ state :needs_attention
162
+ state :read
163
+ state :closed
164
+ state :awaiting_response
165
+ state :junk
166
+ end
167
+ end
168
+
169
+ [:needs_attention, :read, :closed, :awaiting_response, :junk].each do |state|
170
+ assert Conversation.state_machines[:default].states.include?(state)
171
+ end
172
+ end
173
+
174
+ def test_query_methods_created
175
+ Conversation.class_eval do
176
+ acts_as_state_machine :initial => :needs_attention do
177
+ state :needs_attention
178
+ state :read
179
+ state :closed
180
+ state :awaiting_response
181
+ state :junk
182
+ end
183
+ end
184
+
185
+ c = Conversation.create!
186
+ [:needs_attention?, :read?, :closed?, :awaiting_response?, :junk?].each do |query|
187
+ assert c.state_machines[:default].respond_to?(query)
188
+ end
189
+ end
190
+
191
+ def test_event_methods_created
192
+ Conversation.class_eval do
193
+ acts_as_state_machine :initial => :needs_attention do
194
+ state :needs_attention
195
+ state :read
196
+ state :closed
197
+ state :awaiting_response
198
+ state :junk
199
+
200
+ event(:new_message) {}
201
+ event(:view) {}
202
+ event(:reply) {}
203
+ event(:close) {}
204
+ event(:junk, :note => "finished") {}
205
+ event(:unjunk) {}
206
+ end
207
+ end
208
+
209
+ c = Conversation.create!
210
+ [:new_message!, :view!, :reply!, :close!, :junk!, :unjunk!].each do |event|
211
+ assert c.state_machines[:default].respond_to?(event)
212
+ end
213
+ end
214
+
215
+ def test_transition_table
216
+ Conversation.class_eval do
217
+ acts_as_state_machine :initial => :needs_attention do
218
+ state :needs_attention
219
+ state :read
220
+ state :closed
221
+ state :awaiting_response
222
+ state :junk
223
+
224
+ event :new_message do
225
+ transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
226
+ end
227
+ end
228
+ end
229
+
230
+ sm = Conversation.state_machines[:default]
231
+ tt = sm.transition_table
232
+ assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :read, :to => :needs_attention))
233
+ assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :closed, :to => :needs_attention))
234
+ assert tt[:new_message].include?(SupportingClasses::StateTransition.new(sm, :from => :awaiting_response, :to => :needs_attention))
235
+ end
236
+
237
+ def test_next_state_for_event
238
+ Conversation.class_eval do
239
+ acts_as_state_machine :initial => :needs_attention do
240
+ state :needs_attention
241
+ state :read
242
+
243
+ event :view do
244
+ transitions :to => :read, :from => [:needs_attention, :read]
245
+ end
246
+ end
247
+ end
248
+
249
+ c = Conversation.create!
250
+ assert_equal :read, c.state_machines[:default].next_state_for_event(:view)
251
+ end
252
+
253
+ def test_change_state
254
+ Conversation.class_eval do
255
+ acts_as_state_machine :initial => :needs_attention do
256
+ state :needs_attention
257
+ state :read
258
+
259
+ event :view do
260
+ transitions :to => :read, :from => [:needs_attention, :read]
261
+ end
262
+ end
263
+ end
264
+
265
+ c = Conversation.create!
266
+ c.state_machines[:default].view!
267
+ assert c.state_machines[:default].read?
268
+ end
269
+
270
+ def test_can_go_from_read_to_closed_because_guard_passes
271
+ Conversation.class_eval do
272
+ acts_as_state_machine :initial => :needs_attention do
273
+ state :needs_attention
274
+ state :read
275
+ state :closed
276
+ state :awaiting_response
277
+
278
+ event :view do
279
+ transitions :to => :read, :from => [:needs_attention, :read]
280
+ end
281
+
282
+ event :reply do
283
+ transitions :to => :awaiting_response, :from => [:read, :closed]
284
+ end
285
+
286
+ event :close do
287
+ transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
288
+ end
289
+ end
290
+ end
291
+
292
+ c = Conversation.create!
293
+ c.can_close = true
294
+ c.state_machines[:default].view!
295
+ c.state_machines[:default].reply!
296
+ c.state_machines[:default].close!
297
+ assert_equal :closed, c.state_machines[:default].current_state
298
+ end
299
+
300
+ def test_cannot_go_from_read_to_closed_because_of_guard
301
+ Conversation.class_eval do
302
+ acts_as_state_machine :initial => :needs_attention do
303
+ state :needs_attention
304
+ state :read
305
+ state :closed
306
+ state :awaiting_response
307
+
308
+ event :view do
309
+ transitions :to => :read, :from => [:needs_attention, :read]
310
+ end
311
+
312
+ event :reply do
313
+ transitions :to => :awaiting_response, :from => [:read, :closed]
314
+ end
315
+
316
+ event :close do
317
+ transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
318
+ transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
319
+ end
320
+ end
321
+ end
322
+
323
+ c = Conversation.create!
324
+ c.can_close = false
325
+ c.state_machines[:default].view!
326
+ c.state_machines[:default].reply!
327
+ c.state_machines[:default].close!
328
+ assert_equal :read, c.state_machines[:default].current_state
329
+ end
330
+
331
+ def test_ignore_invalid_events
332
+ Conversation.class_eval do
333
+ acts_as_state_machine :initial => :needs_attention do
334
+ state :needs_attention
335
+ state :read
336
+ state :closed
337
+ state :awaiting_response
338
+ state :junk
339
+
340
+ event :new_message do
341
+ transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
342
+ end
343
+
344
+ event :view do
345
+ transitions :to => :read, :from => [:needs_attention, :read]
346
+ end
347
+
348
+ event :junk, :note => "finished" do
349
+ transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
350
+ end
351
+ end
352
+ end
353
+
354
+ c = Conversation.create
355
+ c.state_machines[:default].view!
356
+ c.state_machines[:default].junk!
357
+
358
+ # This is the invalid event
359
+ c.state_machines[:default].new_message!
360
+ assert_equal :junk, c.state_machines[:default].current_state
361
+ end
362
+
363
+ def test_entry_action_executed
364
+ Conversation.class_eval do
365
+ acts_as_state_machine :initial => :needs_attention do
366
+ state :needs_attention
367
+ state :read, :enter => :read_enter_action
368
+
369
+ event :view do
370
+ transitions :to => :read, :from => [:needs_attention, :read]
371
+ end
372
+ end
373
+ end
374
+
375
+ c = Conversation.create!
376
+ c.read_enter = false
377
+ c.state_machines[:default].view!
378
+ assert c.read_enter
379
+ end
380
+
381
+ def test_after_actions_executed
382
+ Conversation.class_eval do
383
+ acts_as_state_machine :initial => :needs_attention do
384
+ state :needs_attention
385
+ state :closed, :after => :closed_after_action
386
+ state :read, :enter => :read_enter_action,
387
+ :exit => Proc.new { |o| o.read_exit = true },
388
+ :after => [:read_after_first_action, :read_after_second_action]
389
+
390
+ event :view do
391
+ transitions :to => :read, :from => [:needs_attention, :read]
392
+ end
393
+
394
+ event :close do
395
+ transitions :to => :closed, :from => [:read, :awaiting_response]
396
+ end
397
+ end
398
+ end
399
+
400
+ c = Conversation.create!
401
+
402
+ c.read_after_first = false
403
+ c.read_after_second = false
404
+ c.closed_after = false
405
+
406
+ c.state_machines[:default].view!
407
+ assert c.read_after_first
408
+ assert c.read_after_second
409
+
410
+ c.can_close = true
411
+ c.state_machines[:default].close!
412
+
413
+ assert c.closed_after
414
+ assert_equal :closed, c.state_machines[:default].current_state
415
+ end
416
+
417
+ def test_after_actions_not_run_on_loopback_transition
418
+ Conversation.class_eval do
419
+ acts_as_state_machine :initial => :needs_attention do
420
+ state :needs_attention
421
+ state :closed, :after => :closed_after_action
422
+ state :read, :after => [:read_after_first_action, :read_after_second_action]
423
+
424
+ event :view do
425
+ transitions :to => :read, :from => :needs_attention
426
+ end
427
+
428
+ event :close do
429
+ transitions :to => :closed, :from => :read
430
+ end
431
+ end
432
+ end
433
+
434
+ c = Conversation.create!
435
+
436
+ c.state_machines[:default].view!
437
+ c.read_after_first = false
438
+ c.read_after_second = false
439
+ c.state_machines[:default].view!
440
+
441
+ assert !c.read_after_first
442
+ assert !c.read_after_second
443
+
444
+ c.can_close = true
445
+
446
+ c.state_machines[:default].close!
447
+ c.closed_after = false
448
+ c.state_machines[:default].close!
449
+
450
+ assert !c.closed_after
451
+ end
452
+
453
+ def test_exit_action_executed
454
+ Conversation.class_eval do
455
+ acts_as_state_machine :initial => :needs_attention do
456
+ state :junk
457
+ state :needs_attention
458
+ state :read, :exit => lambda { |o| o.read_exit = true }
459
+
460
+ event :view do
461
+ transitions :to => :read, :from => :needs_attention
462
+ end
463
+
464
+ event :junk, :note => "finished" do
465
+ transitions :to => :junk, :from => :read
466
+ end
467
+ end
468
+ end
469
+
470
+ c = Conversation.create!
471
+ c.read_exit = false
472
+ c.state_machines[:default].view!
473
+ c.state_machines[:default].junk!
474
+ assert c.read_exit
475
+ end
476
+
477
+ def test_entry_and_exit_not_run_on_loopback_transition
478
+ Conversation.class_eval do
479
+ acts_as_state_machine :initial => :needs_attention do
480
+ state :needs_attention
481
+ state :read, :exit => lambda { |o| o.read_exit = true }
482
+
483
+ event :view do
484
+ transitions :to => :read, :from => [:needs_attention, :read]
485
+ end
486
+ end
487
+ end
488
+
489
+ c = Conversation.create!
490
+ c.state_machines[:default].view!
491
+ c.read_enter = false
492
+ c.read_exit = false
493
+ c.state_machines[:default].view!
494
+ assert !c.read_enter
495
+ assert !c.read_exit
496
+ end
497
+
498
+ def test_entry_and_after_actions_called_for_initial_state
499
+ Conversation.class_eval do
500
+ acts_as_state_machine :initial => :needs_attention do
501
+ state :needs_attention, :enter => lambda { |o| o.needs_attention_enter = true },
502
+ :after => lambda { |o| o.needs_attention_after = true }
503
+ end
504
+ end
505
+
506
+ c = Conversation.create!
507
+ assert c.needs_attention_enter
508
+ assert c.needs_attention_after
509
+ end
510
+
511
+ def test_run_transition_action_is_private
512
+ Conversation.class_eval do
513
+ acts_as_state_machine :initial => :needs_attention do
514
+ state :needs_attention
515
+ end
516
+ end
517
+
518
+ c = Conversation.create!
519
+ assert_raises(NoMethodError) { c.state_machines[:default].run_transition_action :foo }
520
+ end
521
+
522
+ def test_find_all_in_state
523
+ Conversation.class_eval do
524
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
525
+ state :needs_attention
526
+ state :read
527
+ end
528
+ end
529
+
530
+ cs = Conversation.find_in_state(:default, :all, :read)
531
+ assert_equal 2, cs.size
532
+ end
533
+
534
+ def test_find_first_in_state
535
+ Conversation.class_eval do
536
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
537
+ state :needs_attention
538
+ state :read
539
+ end
540
+ end
541
+
542
+ c = Conversation.find_in_state(:default, :first, :read)
543
+ assert_equal conversations(:first).id, c.id
544
+ end
545
+
546
+ def test_find_all_in_state_with_conditions
547
+ Conversation.class_eval do
548
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
549
+ state :needs_attention
550
+ state :read
551
+ end
552
+ end
553
+
554
+ cs = Conversation.find_in_state(:default, :all, :read, :conditions => ['subject = ?', conversations(:second).subject])
555
+
556
+ assert_equal 1, cs.size
557
+ assert_equal conversations(:second).id, cs.first.id
558
+ end
559
+
560
+ def test_find_first_in_state_with_conditions
561
+ Conversation.class_eval do
562
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
563
+ state :needs_attention
564
+ state :read
565
+ end
566
+ end
567
+
568
+ c = Conversation.find_in_state(:default, :first, :read, :conditions => ['subject = ?', conversations(:second).subject])
569
+ assert_equal conversations(:second).id, c.id
570
+ end
571
+
572
+ def test_count_in_state
573
+ Conversation.class_eval do
574
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
575
+ state :needs_attention
576
+ state :read
577
+ end
578
+ end
579
+
580
+ cnt0 = Conversation.count(:conditions => ['state_machine = ?', 'read'])
581
+ cnt = Conversation.count_in_state(:default, :read)
582
+
583
+ assert_equal cnt0, cnt
584
+ end
585
+
586
+ def test_count_in_state_with_conditions
587
+ Conversation.class_eval do
588
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
589
+ state :needs_attention
590
+ state :read
591
+ end
592
+ end
593
+
594
+ cnt0 = Conversation.count(:conditions => ['state_machine = ? AND subject = ?', 'read', 'Foo'])
595
+ cnt = Conversation.count_in_state(:default, :read, :conditions => ['subject = ?', 'Foo'])
596
+
597
+ assert_equal cnt0, cnt
598
+ end
599
+
600
+ def test_find_in_invalid_state_raises_exception
601
+ Conversation.class_eval do
602
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
603
+ state :needs_attention
604
+ state :read
605
+ end
606
+ end
607
+
608
+ assert_raises(InvalidState) do
609
+ Conversation.find_in_state(:default, :all, :dead)
610
+ end
611
+ end
612
+
613
+ def test_count_in_invalid_state_raises_exception
614
+ Conversation.class_eval do
615
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
616
+ state :needs_attention
617
+ state :read
618
+ end
619
+ end
620
+
621
+ assert_raise(InvalidState) do
622
+ Conversation.count_in_state(:default, :dead)
623
+ end
624
+ end
625
+
626
+ def test_can_access_events_via_event_table
627
+ Conversation.class_eval do
628
+ acts_as_state_machine :initial => :needs_attention, :column => "state_machine" do
629
+ state :needs_attention
630
+ state :junk
631
+
632
+ event :junk, :note => "finished" do
633
+ transitions :to => :junk, :from => :needs_attention
634
+ end
635
+ end
636
+ end
637
+
638
+ event = Conversation.state_machines[:default].event_table[:junk]
639
+ assert_equal :junk, event.name
640
+ assert_equal "finished", event.opts[:note]
641
+ end
642
+
643
+ def test_custom_state_values
644
+ Conversation.class_eval do
645
+ acts_as_state_machine :initial => "NEEDS_ATTENTION", :column => "state_machine" do
646
+ state :needs_attention, :value => "NEEDS_ATTENTION"
647
+ state :read, :value => "READ"
648
+
649
+ event :view do
650
+ transitions :to => "READ", :from => ["NEEDS_ATTENTION", "READ"]
651
+ end
652
+ end
653
+ end
654
+
655
+ c = Conversation.create!
656
+ assert_equal "NEEDS_ATTENTION", c.state_machine
657
+ assert c.state_machines[:default].needs_attention?
658
+ c.state_machines[:default].view!
659
+ assert_equal "READ", c.state_machine
660
+ assert c.state_machines[:default].read?
661
+ end
662
+
663
+
664
+ def test_can_define_multiple_state_machines
665
+ Conversation.class_eval do
666
+ acts_as_state_machine :name => 'default', :initial => :needs_attention, :column => "state_machine" do
667
+ state :needs_attention
668
+ state :junk
669
+
670
+ event :junk do
671
+ transitions :to => :junk, :from => :needs_attention
672
+ end
673
+ end
674
+
675
+ acts_as_state_machine :name => 'user', :initial => :pending, :column => "another_state_machine" do
676
+ state :pending
677
+ state :active
678
+
679
+ event :activate do
680
+ transitions :to => :active, :from => :pending
681
+ end
682
+ end
683
+ end
684
+
685
+ c = Conversation.create!
686
+ assert_equal 'needs_attention', c.state_machine
687
+ assert_equal 'pending', c.another_state_machine
688
+ assert c.state_machines[:default].needs_attention?
689
+ assert c.state_machines[:user].pending?
690
+ c.state_machines[:default].junk!
691
+ assert c.state_machines[:default].junk?
692
+ assert c.state_machines[:user].pending?
693
+ c.state_machines[:user].activate!
694
+ assert c.state_machines[:default].junk?
695
+ assert c.state_machines[:user].active?
696
+ end
697
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aq1018-acts_as_multiple_state_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Qian
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-08 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model
17
+ email: aaron [a] ekohe [d] com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - History.txt
24
+ - Manifest.txt
25
+ - README.txt
26
+ files:
27
+ - History.txt
28
+ - Manifest.txt
29
+ - README.txt
30
+ - Rakefile
31
+ - rails/init.rb
32
+ - lib/acts_as_multiple_state_machines.rb
33
+ - lib/acts/as/multiple/state_machines/active_record_extension.rb
34
+ - lib/acts/as/multiple/state_machines/exceptions.rb
35
+ - lib/acts/as/multiple/state_machines/supporting_classes.rb
36
+ - lib/acts/as/multiple/state_machines/supporting_classes/event.rb
37
+ - lib/acts/as/multiple/state_machines/supporting_classes/state.rb
38
+ - lib/acts/as/multiple/state_machines/supporting_classes/state_machine.rb
39
+ - lib/acts/as/multiple/state_machines/supporting_classes/state_machine_factory.rb
40
+ - lib/acts/as/multiple/state_machines/supporting_classes/state_transition.rb
41
+ - test/test_acts_as_multiple_state_machines.rb
42
+ - test/fixtures/conversations.yml
43
+ has_rdoc: true
44
+ homepage: http://acts-as-multiple-state-machines.rubyforge.com
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: similar to acts_as_state_machine, but provides multiple State Machines per ActiveRecord Model
70
+ test_files:
71
+ - test/fixtures/conversations.yml
72
+ - test/test_acts_as_multiple_state_machines.rb