aq1018-acts_as_multiple_state_machines 0.1

Sign up to get free protection for your applications and to get access to all the features.
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