acts_as_state_machine 2.1.3
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/CHANGELOG +16 -0
- data/MIT-LICENSE +20 -0
- data/README +33 -0
- data/Rakefile +28 -0
- data/TODO +11 -0
- data/acts_as_state_machine.gemspec +32 -0
- data/init.rb +1 -0
- data/lib/acts_as_state_machine.rb +277 -0
- data/rails/init.rb +1 -0
- data/test/fixtures/conversations.yml +11 -0
- data/test/test_acts_as_state_machine.rb +631 -0
- metadata +75 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
* 2.1.1
|
2
|
+
packaged it up as a gem [Jacques Crocker]
|
3
|
+
|
4
|
+
* trunk *
|
5
|
+
break with true value [Kaspar Schiess]
|
6
|
+
|
7
|
+
* 2.1 *
|
8
|
+
After actions [Saimon Moore]
|
9
|
+
|
10
|
+
* 2.0 * (2006-01-20 15:26:28 -0500)
|
11
|
+
Enter / Exit actions
|
12
|
+
Transition guards
|
13
|
+
Guards and actions can be a symbol pointing to a method or a Proc
|
14
|
+
|
15
|
+
* 1.0 * (2006-01-15 12:16:55 -0500)
|
16
|
+
Initial Release
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006 Scott Barron
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
= Acts As State Machine
|
2
|
+
|
3
|
+
This act gives an Active Record model the ability to act as a finite state
|
4
|
+
machine (FSM).
|
5
|
+
|
6
|
+
Acquire via subversion at:
|
7
|
+
|
8
|
+
http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk
|
9
|
+
|
10
|
+
If prompted, use the user/pass anonymous/anonymous.
|
11
|
+
|
12
|
+
== Example
|
13
|
+
|
14
|
+
class Order < ActiveRecord::Base
|
15
|
+
acts_as_state_machine :initial => :opened
|
16
|
+
|
17
|
+
state :opened
|
18
|
+
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
|
19
|
+
state :returned
|
20
|
+
|
21
|
+
event :close do
|
22
|
+
transitions :to => :closed, :from => :opened
|
23
|
+
end
|
24
|
+
|
25
|
+
event :return do
|
26
|
+
transitions :to => :returned, :from => :closed
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
o = Order.create
|
31
|
+
o.close! # notice is sent by mailer
|
32
|
+
o.return!
|
33
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => [:clean_db, :test]
|
7
|
+
|
8
|
+
desc 'Remove the stale db file'
|
9
|
+
task :clean_db do
|
10
|
+
`rm -f #{File.dirname(__FILE__)}/test/state_machine.sqlite.db`
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Test the acts as state machine plugin.'
|
14
|
+
Rake::TestTask.new(:test) do |t|
|
15
|
+
t.libs << 'lib'
|
16
|
+
t.pattern = 'test/**/test_*.rb'
|
17
|
+
t.verbose = true
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Generate documentation for the acts as state machine plugin.'
|
21
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'rdoc'
|
23
|
+
rdoc.title = 'Acts As State Machine'
|
24
|
+
rdoc.options << '--line-numbers --inline-source'
|
25
|
+
rdoc.rdoc_files.include('README')
|
26
|
+
rdoc.rdoc_files.include('TODO')
|
27
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
28
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
* Currently invalid events are ignored, create an option so that they can be
|
2
|
+
ignored or raise an exception.
|
3
|
+
|
4
|
+
* Query for a list of possible next states.
|
5
|
+
|
6
|
+
* Make listing states optional since they can be inferred from the events.
|
7
|
+
Only required to list a state if you want to define a transition block for it.
|
8
|
+
|
9
|
+
* Real transition actions
|
10
|
+
|
11
|
+
* Default states
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'acts_as_state_machine'
|
3
|
+
s.version = '2.1.3'
|
4
|
+
s.date = '2010-02-14'
|
5
|
+
|
6
|
+
s.summary = "Allows ActiveRecord models to define states and transition actions between them"
|
7
|
+
s.description = "This act gives an Active Record model the ability to act as a finite state machine (FSM)."
|
8
|
+
|
9
|
+
s.authors = ['RailsJedi', 'Scott Barron']
|
10
|
+
s.email = 'railsjedi@gmail.com'
|
11
|
+
s.homepage = 'http://github.com/jcnetdev/acts_as_state_machine'
|
12
|
+
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.rdoc_options = ["--main", "README"]
|
15
|
+
s.extra_rdoc_files = ["README"]
|
16
|
+
|
17
|
+
s.add_dependency 'activerecord', ['>= 2.1']
|
18
|
+
|
19
|
+
s.files = ["CHANGELOG",
|
20
|
+
"MIT-LICENSE",
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"TODO",
|
24
|
+
"acts_as_state_machine.gemspec",
|
25
|
+
"init.rb",
|
26
|
+
"lib/acts_as_state_machine.rb",
|
27
|
+
"rails/init.rb"]
|
28
|
+
|
29
|
+
s.test_files = ["test/fixtures",
|
30
|
+
"test/fixtures/conversations.yml",
|
31
|
+
"test/test_acts_as_state_machine.rb"]
|
32
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module ScottBarron #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module StateMachine #:nodoc:
|
4
|
+
class InvalidState < Exception #:nodoc:
|
5
|
+
end
|
6
|
+
class NoInitialState < Exception #:nodoc:
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(base) #:nodoc:
|
10
|
+
base.extend ActMacro
|
11
|
+
end
|
12
|
+
|
13
|
+
module SupportingClasses
|
14
|
+
# Default transition action. Always returns true.
|
15
|
+
NOOP = lambda { |o| true }
|
16
|
+
|
17
|
+
class State
|
18
|
+
attr_reader :name, :value
|
19
|
+
|
20
|
+
def initialize(name, options)
|
21
|
+
@name = name.to_sym
|
22
|
+
@value = (options[:value] || @name).to_s
|
23
|
+
@after = Array(options[:after])
|
24
|
+
@enter = options[:enter] || NOOP
|
25
|
+
@exit = options[:exit] || NOOP
|
26
|
+
end
|
27
|
+
|
28
|
+
def entering(record)
|
29
|
+
record.send(:run_transition_action, @enter)
|
30
|
+
end
|
31
|
+
|
32
|
+
def entered(record)
|
33
|
+
@after.each { |action| record.send(:run_transition_action, action) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def exited(record)
|
37
|
+
record.send(:run_transition_action, @exit)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class StateTransition
|
42
|
+
attr_reader :from, :to, :opts
|
43
|
+
|
44
|
+
def initialize(options)
|
45
|
+
@from = options[:from].to_s
|
46
|
+
@to = options[:to].to_s
|
47
|
+
@guard = options[:guard] || NOOP
|
48
|
+
@opts = options
|
49
|
+
end
|
50
|
+
|
51
|
+
def guard(obj)
|
52
|
+
@guard ? obj.send(:run_transition_action, @guard) : true
|
53
|
+
end
|
54
|
+
|
55
|
+
def perform(record)
|
56
|
+
return false unless guard(record)
|
57
|
+
loopback = record.current_state.to_s == to
|
58
|
+
states = record.class.read_inheritable_attribute(:states)
|
59
|
+
next_state = states[to]
|
60
|
+
old_state = states[record.current_state.to_s]
|
61
|
+
|
62
|
+
next_state.entering(record) unless loopback
|
63
|
+
|
64
|
+
record.update_attribute(record.class.state_column, next_state.value)
|
65
|
+
|
66
|
+
next_state.entered(record) unless loopback
|
67
|
+
old_state.exited(record) unless loopback
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def ==(obj)
|
72
|
+
@from == obj.from && @to == obj.to
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Event
|
77
|
+
attr_reader :name
|
78
|
+
attr_reader :transitions
|
79
|
+
attr_reader :opts
|
80
|
+
|
81
|
+
def initialize(name, opts, transition_table, &block)
|
82
|
+
@name = name.to_sym
|
83
|
+
@transitions = transition_table[@name] = []
|
84
|
+
instance_eval(&block) if block
|
85
|
+
@opts = opts
|
86
|
+
@opts.freeze
|
87
|
+
@transitions.freeze
|
88
|
+
freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
def next_states(record)
|
92
|
+
@transitions.select { |t| t.from == record.current_state.to_s }
|
93
|
+
end
|
94
|
+
|
95
|
+
def fire(record)
|
96
|
+
next_states(record).each do |transition|
|
97
|
+
break true if transition.perform(record)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def transitions(trans_opts)
|
102
|
+
Array(trans_opts[:from]).each do |s|
|
103
|
+
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module ActMacro
|
110
|
+
# Configuration options are
|
111
|
+
#
|
112
|
+
# * +column+ - specifies the column name to use for keeping the state (default: state)
|
113
|
+
# * +initial+ - specifies an initial state for newly created objects (required)
|
114
|
+
def acts_as_state_machine(options = {})
|
115
|
+
class_eval do
|
116
|
+
extend ClassMethods
|
117
|
+
include InstanceMethods
|
118
|
+
|
119
|
+
raise NoInitialState unless options[:initial]
|
120
|
+
|
121
|
+
write_inheritable_attribute :states, {}
|
122
|
+
write_inheritable_attribute :initial_state, options[:initial]
|
123
|
+
write_inheritable_attribute :transition_table, {}
|
124
|
+
write_inheritable_attribute :event_table, {}
|
125
|
+
write_inheritable_attribute :state_column, options[:column] || 'state'
|
126
|
+
|
127
|
+
class_inheritable_reader :initial_state
|
128
|
+
class_inheritable_reader :state_column
|
129
|
+
class_inheritable_reader :transition_table
|
130
|
+
class_inheritable_reader :event_table
|
131
|
+
|
132
|
+
before_create :set_initial_state
|
133
|
+
after_create :run_initial_state_actions
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module InstanceMethods
|
139
|
+
def set_initial_state #:nodoc:
|
140
|
+
write_attribute self.class.state_column, self.class.initial_state.to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
def run_initial_state_actions
|
144
|
+
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_s]
|
145
|
+
initial.entering(self)
|
146
|
+
initial.entered(self)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns the current state the object is in, as a Ruby symbol.
|
150
|
+
def current_state
|
151
|
+
self.send(self.class.state_column).to_sym
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns what the next state for a given event would be, as a Ruby symbol.
|
155
|
+
def next_state_for_event(event)
|
156
|
+
ns = next_states_for_event(event)
|
157
|
+
ns.empty? ? nil : ns.first.to.to_sym
|
158
|
+
end
|
159
|
+
|
160
|
+
def next_states_for_event(event)
|
161
|
+
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
|
162
|
+
s.from == current_state.to_s
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def run_transition_action(action)
|
167
|
+
Symbol === action ? self.method(action).call : action.call(self)
|
168
|
+
end
|
169
|
+
private :run_transition_action
|
170
|
+
end
|
171
|
+
|
172
|
+
module ClassMethods
|
173
|
+
# Returns an array of all known states.
|
174
|
+
def states
|
175
|
+
read_inheritable_attribute(:states).keys.collect { |state| state.to_sym }
|
176
|
+
end
|
177
|
+
|
178
|
+
# Define an event. This takes a block which describes all valid transitions
|
179
|
+
# for this event.
|
180
|
+
#
|
181
|
+
# Example:
|
182
|
+
#
|
183
|
+
# class Order < ActiveRecord::Base
|
184
|
+
# acts_as_state_machine :initial => :open
|
185
|
+
#
|
186
|
+
# state :open
|
187
|
+
# state :closed
|
188
|
+
#
|
189
|
+
# event :close_order do
|
190
|
+
# transitions :to => :closed, :from => :open
|
191
|
+
# end
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
|
195
|
+
# to and <tt>:from</tt> is a state (or Array of states) from which this
|
196
|
+
# event can be fired.
|
197
|
+
#
|
198
|
+
# This creates an instance method used for firing the event. The method
|
199
|
+
# created is the name of the event followed by an exclamation point (!).
|
200
|
+
# Example: <tt>order.close_order!</tt>.
|
201
|
+
def event(event, opts={}, &block)
|
202
|
+
tt = read_inheritable_attribute(:transition_table)
|
203
|
+
|
204
|
+
e = SupportingClasses::Event.new(event, opts, tt, &block)
|
205
|
+
write_inheritable_hash(:event_table, event.to_sym => e)
|
206
|
+
define_method("#{event.to_s}!") { e.fire(self) }
|
207
|
+
end
|
208
|
+
|
209
|
+
# Define a state of the system. +state+ can take an optional Proc object
|
210
|
+
# which will be executed every time the system transitions into that
|
211
|
+
# state. The proc will be passed the current object.
|
212
|
+
#
|
213
|
+
# Example:
|
214
|
+
#
|
215
|
+
# class Order < ActiveRecord::Base
|
216
|
+
# acts_as_state_machine :initial => :open
|
217
|
+
#
|
218
|
+
# state :open
|
219
|
+
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
|
220
|
+
# end
|
221
|
+
def state(name, opts={})
|
222
|
+
state = SupportingClasses::State.new(name, opts)
|
223
|
+
write_inheritable_hash(:states, state.value => state)
|
224
|
+
|
225
|
+
define_method("#{state.name}?") { current_state.to_s == state.value }
|
226
|
+
end
|
227
|
+
|
228
|
+
# Wraps ActiveRecord::Base.find to conveniently find all records in
|
229
|
+
# a given state. Options:
|
230
|
+
#
|
231
|
+
# * +number+ - This is just :first or :all from ActiveRecord +find+
|
232
|
+
# * +state+ - The state to find
|
233
|
+
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
|
234
|
+
def find_in_state(number, state, *args)
|
235
|
+
with_state_scope state do
|
236
|
+
find(number, *args)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Wraps ActiveRecord::Base.count to conveniently count all records in
|
241
|
+
# a given state. Options:
|
242
|
+
#
|
243
|
+
# * +state+ - The state to find
|
244
|
+
# * +args+ - The rest of the args are passed down to ActiveRecord +find+
|
245
|
+
def count_in_state(state, *args)
|
246
|
+
with_state_scope state do
|
247
|
+
count(*args)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
|
252
|
+
# a given state. Options:
|
253
|
+
#
|
254
|
+
# * +state+ - The state to find
|
255
|
+
# * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
|
256
|
+
def calculate_in_state(state, *args)
|
257
|
+
with_state_scope state do
|
258
|
+
calculate(*args)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
protected
|
263
|
+
def with_state_scope(state)
|
264
|
+
raise InvalidState unless states.include?(state.to_sym)
|
265
|
+
|
266
|
+
with_scope :find => {:conditions => ["#{table_name}.#{state_column} = ?", state.to_s]} do
|
267
|
+
yield if block_given?
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
ActiveRecord::Base.class_eval do
|
276
|
+
include ScottBarron::Acts::StateMachine
|
277
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'acts_as_state_machine'
|
@@ -0,0 +1,631 @@
|
|
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__) + "/../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 => "state_machine.sqlite3.db"
|
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"] || "sqlite")
|
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
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Test::Unit::TestCase
|
59
|
+
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
60
|
+
self.use_transactional_fixtures = true
|
61
|
+
self.use_instantiated_fixtures = false
|
62
|
+
|
63
|
+
def create_fixtures(*table_names, &block)
|
64
|
+
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Conversation < ActiveRecord::Base
|
69
|
+
attr_writer :can_close
|
70
|
+
attr_accessor :read_enter, :read_exit,
|
71
|
+
:needs_attention_enter, :needs_attention_after,
|
72
|
+
:read_after_first, :read_after_second,
|
73
|
+
:closed_after
|
74
|
+
|
75
|
+
# How's THAT for self-documenting? ;-)
|
76
|
+
def always_true
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def can_close?
|
81
|
+
!!@can_close
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_enter_action
|
85
|
+
self.read_enter = true
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_after_first_action
|
89
|
+
self.read_after_first = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def read_after_second_action
|
93
|
+
self.read_after_second = true
|
94
|
+
end
|
95
|
+
|
96
|
+
def closed_after_action
|
97
|
+
self.closed_after = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class ActsAsStateMachineTest < Test::Unit::TestCase
|
102
|
+
include ScottBarron::Acts::StateMachine
|
103
|
+
fixtures :conversations
|
104
|
+
|
105
|
+
def teardown
|
106
|
+
Conversation.class_eval do
|
107
|
+
write_inheritable_attribute :states, {}
|
108
|
+
write_inheritable_attribute :initial_state, nil
|
109
|
+
write_inheritable_attribute :transition_table, {}
|
110
|
+
write_inheritable_attribute :event_table, {}
|
111
|
+
write_inheritable_attribute :state_column, "state"
|
112
|
+
|
113
|
+
# Clear out any callbacks that were set by acts_as_state_machine.
|
114
|
+
write_inheritable_attribute :before_create, []
|
115
|
+
write_inheritable_attribute :after_create, []
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_no_initial_value_raises_exception
|
120
|
+
assert_raises(NoInitialState) do
|
121
|
+
Conversation.class_eval { acts_as_state_machine }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_state_column
|
126
|
+
Conversation.class_eval do
|
127
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
128
|
+
state :needs_attention
|
129
|
+
end
|
130
|
+
|
131
|
+
assert_equal "state_machine", Conversation.state_column
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_initial_state_value
|
135
|
+
Conversation.class_eval do
|
136
|
+
acts_as_state_machine :initial => :needs_attention
|
137
|
+
state :needs_attention
|
138
|
+
end
|
139
|
+
|
140
|
+
assert_equal :needs_attention, Conversation.initial_state
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_initial_state
|
144
|
+
Conversation.class_eval do
|
145
|
+
acts_as_state_machine :initial => :needs_attention
|
146
|
+
state :needs_attention
|
147
|
+
end
|
148
|
+
|
149
|
+
c = Conversation.create!
|
150
|
+
assert_equal :needs_attention, c.current_state
|
151
|
+
assert c.needs_attention?
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_states_were_set
|
155
|
+
Conversation.class_eval do
|
156
|
+
acts_as_state_machine :initial => :needs_attention
|
157
|
+
state :needs_attention
|
158
|
+
state :read
|
159
|
+
state :closed
|
160
|
+
state :awaiting_response
|
161
|
+
state :junk
|
162
|
+
end
|
163
|
+
|
164
|
+
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |state|
|
165
|
+
assert Conversation.states.include?(state)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_query_methods_created
|
170
|
+
Conversation.class_eval do
|
171
|
+
acts_as_state_machine :initial => :needs_attention
|
172
|
+
state :needs_attention
|
173
|
+
state :read
|
174
|
+
state :closed
|
175
|
+
state :awaiting_response
|
176
|
+
state :junk
|
177
|
+
end
|
178
|
+
|
179
|
+
c = Conversation.create!
|
180
|
+
[:needs_attention?, :read?, :closed?, :awaiting_response?, :junk?].each do |query|
|
181
|
+
assert c.respond_to?(query)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_event_methods_created
|
186
|
+
Conversation.class_eval do
|
187
|
+
acts_as_state_machine :initial => :needs_attention
|
188
|
+
state :needs_attention
|
189
|
+
state :read
|
190
|
+
state :closed
|
191
|
+
state :awaiting_response
|
192
|
+
state :junk
|
193
|
+
|
194
|
+
event(:new_message) {}
|
195
|
+
event(:view) {}
|
196
|
+
event(:reply) {}
|
197
|
+
event(:close) {}
|
198
|
+
event(:junk, :note => "finished") {}
|
199
|
+
event(:unjunk) {}
|
200
|
+
end
|
201
|
+
|
202
|
+
c = Conversation.create!
|
203
|
+
[:new_message!, :view!, :reply!, :close!, :junk!, :unjunk!].each do |event|
|
204
|
+
assert c.respond_to?(event)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_transition_table
|
209
|
+
Conversation.class_eval do
|
210
|
+
acts_as_state_machine :initial => :needs_attention
|
211
|
+
state :needs_attention
|
212
|
+
state :read
|
213
|
+
state :closed
|
214
|
+
state :awaiting_response
|
215
|
+
state :junk
|
216
|
+
|
217
|
+
event :new_message do
|
218
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
tt = Conversation.transition_table
|
223
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
|
224
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
|
225
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
|
226
|
+
end
|
227
|
+
|
228
|
+
def test_next_state_for_event
|
229
|
+
Conversation.class_eval do
|
230
|
+
acts_as_state_machine :initial => :needs_attention
|
231
|
+
state :needs_attention
|
232
|
+
state :read
|
233
|
+
|
234
|
+
event :view do
|
235
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
c = Conversation.create!
|
240
|
+
assert_equal :read, c.next_state_for_event(:view)
|
241
|
+
end
|
242
|
+
|
243
|
+
def test_change_state
|
244
|
+
Conversation.class_eval do
|
245
|
+
acts_as_state_machine :initial => :needs_attention
|
246
|
+
state :needs_attention
|
247
|
+
state :read
|
248
|
+
|
249
|
+
event :view do
|
250
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
c = Conversation.create!
|
255
|
+
c.view!
|
256
|
+
assert c.read?
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_can_go_from_read_to_closed_because_guard_passes
|
260
|
+
Conversation.class_eval do
|
261
|
+
acts_as_state_machine :initial => :needs_attention
|
262
|
+
state :needs_attention
|
263
|
+
state :read
|
264
|
+
state :closed
|
265
|
+
state :awaiting_response
|
266
|
+
|
267
|
+
event :view do
|
268
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
269
|
+
end
|
270
|
+
|
271
|
+
event :reply do
|
272
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
273
|
+
end
|
274
|
+
|
275
|
+
event :close do
|
276
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
c = Conversation.create!
|
281
|
+
c.can_close = true
|
282
|
+
c.view!
|
283
|
+
c.reply!
|
284
|
+
c.close!
|
285
|
+
assert_equal :closed, c.current_state
|
286
|
+
end
|
287
|
+
|
288
|
+
def test_cannot_go_from_read_to_closed_because_of_guard
|
289
|
+
Conversation.class_eval do
|
290
|
+
acts_as_state_machine :initial => :needs_attention
|
291
|
+
state :needs_attention
|
292
|
+
state :read
|
293
|
+
state :closed
|
294
|
+
state :awaiting_response
|
295
|
+
|
296
|
+
event :view do
|
297
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
298
|
+
end
|
299
|
+
|
300
|
+
event :reply do
|
301
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
302
|
+
end
|
303
|
+
|
304
|
+
event :close do
|
305
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
306
|
+
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
c = Conversation.create!
|
311
|
+
c.can_close = false
|
312
|
+
c.view!
|
313
|
+
c.reply!
|
314
|
+
c.close!
|
315
|
+
assert_equal :read, c.current_state
|
316
|
+
end
|
317
|
+
|
318
|
+
def test_ignore_invalid_events
|
319
|
+
Conversation.class_eval do
|
320
|
+
acts_as_state_machine :initial => :needs_attention
|
321
|
+
state :needs_attention
|
322
|
+
state :read
|
323
|
+
state :closed
|
324
|
+
state :awaiting_response
|
325
|
+
state :junk
|
326
|
+
|
327
|
+
event :new_message do
|
328
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
329
|
+
end
|
330
|
+
|
331
|
+
event :view do
|
332
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
333
|
+
end
|
334
|
+
|
335
|
+
event :junk, :note => "finished" do
|
336
|
+
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
c = Conversation.create
|
341
|
+
c.view!
|
342
|
+
c.junk!
|
343
|
+
|
344
|
+
# This is the invalid event
|
345
|
+
c.new_message!
|
346
|
+
assert_equal :junk, c.current_state
|
347
|
+
end
|
348
|
+
|
349
|
+
def test_entry_action_executed
|
350
|
+
Conversation.class_eval do
|
351
|
+
acts_as_state_machine :initial => :needs_attention
|
352
|
+
state :needs_attention
|
353
|
+
state :read, :enter => :read_enter_action
|
354
|
+
|
355
|
+
event :view do
|
356
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
c = Conversation.create!
|
361
|
+
c.read_enter = false
|
362
|
+
c.view!
|
363
|
+
assert c.read_enter
|
364
|
+
end
|
365
|
+
|
366
|
+
def test_after_actions_executed
|
367
|
+
Conversation.class_eval do
|
368
|
+
acts_as_state_machine :initial => :needs_attention
|
369
|
+
state :needs_attention
|
370
|
+
state :closed, :after => :closed_after_action
|
371
|
+
state :read, :enter => :read_enter_action,
|
372
|
+
:exit => Proc.new { |o| o.read_exit = true },
|
373
|
+
:after => [:read_after_first_action, :read_after_second_action]
|
374
|
+
|
375
|
+
event :view do
|
376
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
377
|
+
end
|
378
|
+
|
379
|
+
event :close do
|
380
|
+
transitions :to => :closed, :from => [:read, :awaiting_response]
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
c = Conversation.create!
|
385
|
+
|
386
|
+
c.read_after_first = false
|
387
|
+
c.read_after_second = false
|
388
|
+
c.closed_after = false
|
389
|
+
|
390
|
+
c.view!
|
391
|
+
assert c.read_after_first
|
392
|
+
assert c.read_after_second
|
393
|
+
|
394
|
+
c.can_close = true
|
395
|
+
c.close!
|
396
|
+
|
397
|
+
assert c.closed_after
|
398
|
+
assert_equal :closed, c.current_state
|
399
|
+
end
|
400
|
+
|
401
|
+
def test_after_actions_not_run_on_loopback_transition
|
402
|
+
Conversation.class_eval do
|
403
|
+
acts_as_state_machine :initial => :needs_attention
|
404
|
+
state :needs_attention
|
405
|
+
state :closed, :after => :closed_after_action
|
406
|
+
state :read, :after => [:read_after_first_action, :read_after_second_action]
|
407
|
+
|
408
|
+
event :view do
|
409
|
+
transitions :to => :read, :from => :needs_attention
|
410
|
+
end
|
411
|
+
|
412
|
+
event :close do
|
413
|
+
transitions :to => :closed, :from => :read
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
c = Conversation.create!
|
418
|
+
|
419
|
+
c.view!
|
420
|
+
c.read_after_first = false
|
421
|
+
c.read_after_second = false
|
422
|
+
c.view!
|
423
|
+
|
424
|
+
assert !c.read_after_first
|
425
|
+
assert !c.read_after_second
|
426
|
+
|
427
|
+
c.can_close = true
|
428
|
+
|
429
|
+
c.close!
|
430
|
+
c.closed_after = false
|
431
|
+
c.close!
|
432
|
+
|
433
|
+
assert !c.closed_after
|
434
|
+
end
|
435
|
+
|
436
|
+
def test_exit_action_executed
|
437
|
+
Conversation.class_eval do
|
438
|
+
acts_as_state_machine :initial => :needs_attention
|
439
|
+
state :junk
|
440
|
+
state :needs_attention
|
441
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
442
|
+
|
443
|
+
event :view do
|
444
|
+
transitions :to => :read, :from => :needs_attention
|
445
|
+
end
|
446
|
+
|
447
|
+
event :junk, :note => "finished" do
|
448
|
+
transitions :to => :junk, :from => :read
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
c = Conversation.create!
|
453
|
+
c.read_exit = false
|
454
|
+
c.view!
|
455
|
+
c.junk!
|
456
|
+
assert c.read_exit
|
457
|
+
end
|
458
|
+
|
459
|
+
def test_entry_and_exit_not_run_on_loopback_transition
|
460
|
+
Conversation.class_eval do
|
461
|
+
acts_as_state_machine :initial => :needs_attention
|
462
|
+
state :needs_attention
|
463
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
464
|
+
|
465
|
+
event :view do
|
466
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
c = Conversation.create!
|
471
|
+
c.view!
|
472
|
+
c.read_enter = false
|
473
|
+
c.read_exit = false
|
474
|
+
c.view!
|
475
|
+
assert !c.read_enter
|
476
|
+
assert !c.read_exit
|
477
|
+
end
|
478
|
+
|
479
|
+
def test_entry_and_after_actions_called_for_initial_state
|
480
|
+
Conversation.class_eval do
|
481
|
+
acts_as_state_machine :initial => :needs_attention
|
482
|
+
state :needs_attention, :enter => lambda { |o| o.needs_attention_enter = true },
|
483
|
+
:after => lambda { |o| o.needs_attention_after = true }
|
484
|
+
end
|
485
|
+
|
486
|
+
c = Conversation.create!
|
487
|
+
assert c.needs_attention_enter
|
488
|
+
assert c.needs_attention_after
|
489
|
+
end
|
490
|
+
|
491
|
+
def test_run_transition_action_is_private
|
492
|
+
Conversation.class_eval do
|
493
|
+
acts_as_state_machine :initial => :needs_attention
|
494
|
+
state :needs_attention
|
495
|
+
end
|
496
|
+
|
497
|
+
c = Conversation.create!
|
498
|
+
assert_raises(NoMethodError) { c.run_transition_action :foo }
|
499
|
+
end
|
500
|
+
|
501
|
+
def test_find_all_in_state
|
502
|
+
Conversation.class_eval do
|
503
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
504
|
+
state :needs_attention
|
505
|
+
state :read
|
506
|
+
end
|
507
|
+
|
508
|
+
cs = Conversation.find_in_state(:all, :read)
|
509
|
+
assert_equal 2, cs.size
|
510
|
+
end
|
511
|
+
|
512
|
+
def test_find_first_in_state
|
513
|
+
Conversation.class_eval do
|
514
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
515
|
+
state :needs_attention
|
516
|
+
state :read
|
517
|
+
end
|
518
|
+
|
519
|
+
c = Conversation.find_in_state(:first, :read)
|
520
|
+
assert_equal conversations(:first).id, c.id
|
521
|
+
end
|
522
|
+
|
523
|
+
def test_find_all_in_state_with_conditions
|
524
|
+
Conversation.class_eval do
|
525
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
526
|
+
state :needs_attention
|
527
|
+
state :read
|
528
|
+
end
|
529
|
+
|
530
|
+
cs = Conversation.find_in_state(:all, :read, :conditions => ['subject = ?', conversations(:second).subject])
|
531
|
+
|
532
|
+
assert_equal 1, cs.size
|
533
|
+
assert_equal conversations(:second).id, cs.first.id
|
534
|
+
end
|
535
|
+
|
536
|
+
def test_find_first_in_state_with_conditions
|
537
|
+
Conversation.class_eval do
|
538
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
539
|
+
state :needs_attention
|
540
|
+
state :read
|
541
|
+
end
|
542
|
+
|
543
|
+
c = Conversation.find_in_state(:first, :read, :conditions => ['subject = ?', conversations(:second).subject])
|
544
|
+
assert_equal conversations(:second).id, c.id
|
545
|
+
end
|
546
|
+
|
547
|
+
def test_count_in_state
|
548
|
+
Conversation.class_eval do
|
549
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
550
|
+
state :needs_attention
|
551
|
+
state :read
|
552
|
+
end
|
553
|
+
|
554
|
+
cnt0 = Conversation.count(:conditions => ['state_machine = ?', 'read'])
|
555
|
+
cnt = Conversation.count_in_state(:read)
|
556
|
+
|
557
|
+
assert_equal cnt0, cnt
|
558
|
+
end
|
559
|
+
|
560
|
+
def test_count_in_state_with_conditions
|
561
|
+
Conversation.class_eval do
|
562
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
563
|
+
state :needs_attention
|
564
|
+
state :read
|
565
|
+
end
|
566
|
+
|
567
|
+
cnt0 = Conversation.count(:conditions => ['state_machine = ? AND subject = ?', 'read', 'Foo'])
|
568
|
+
cnt = Conversation.count_in_state(:read, :conditions => ['subject = ?', 'Foo'])
|
569
|
+
|
570
|
+
assert_equal cnt0, cnt
|
571
|
+
end
|
572
|
+
|
573
|
+
def test_find_in_invalid_state_raises_exception
|
574
|
+
Conversation.class_eval do
|
575
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
576
|
+
state :needs_attention
|
577
|
+
state :read
|
578
|
+
end
|
579
|
+
|
580
|
+
assert_raises(InvalidState) do
|
581
|
+
Conversation.find_in_state(:all, :dead)
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def test_count_in_invalid_state_raises_exception
|
586
|
+
Conversation.class_eval do
|
587
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
588
|
+
state :needs_attention
|
589
|
+
state :read
|
590
|
+
end
|
591
|
+
|
592
|
+
assert_raise(InvalidState) do
|
593
|
+
Conversation.count_in_state(:dead)
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
def test_can_access_events_via_event_table
|
598
|
+
Conversation.class_eval do
|
599
|
+
acts_as_state_machine :initial => :needs_attention, :column => "state_machine"
|
600
|
+
state :needs_attention
|
601
|
+
state :junk
|
602
|
+
|
603
|
+
event :junk, :note => "finished" do
|
604
|
+
transitions :to => :junk, :from => :needs_attention
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
event = Conversation.event_table[:junk]
|
609
|
+
assert_equal :junk, event.name
|
610
|
+
assert_equal "finished", event.opts[:note]
|
611
|
+
end
|
612
|
+
|
613
|
+
def test_custom_state_values
|
614
|
+
Conversation.class_eval do
|
615
|
+
acts_as_state_machine :initial => "NEEDS_ATTENTION", :column => "state_machine"
|
616
|
+
state :needs_attention, :value => "NEEDS_ATTENTION"
|
617
|
+
state :read, :value => "READ"
|
618
|
+
|
619
|
+
event :view do
|
620
|
+
transitions :to => "READ", :from => ["NEEDS_ATTENTION", "READ"]
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
c = Conversation.create!
|
625
|
+
assert_equal "NEEDS_ATTENTION", c.state_machine
|
626
|
+
assert c.needs_attention?
|
627
|
+
c.view!
|
628
|
+
assert_equal "READ", c.state_machine
|
629
|
+
assert c.read?
|
630
|
+
end
|
631
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_state_machine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- RailsJedi
|
8
|
+
- Scott Barron
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2010-02-14 00:00:00 +01:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activerecord
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "2.1"
|
25
|
+
version:
|
26
|
+
description: This act gives an Active Record model the ability to act as a finite state machine (FSM).
|
27
|
+
email: railsjedi@gmail.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- README
|
34
|
+
files:
|
35
|
+
- CHANGELOG
|
36
|
+
- MIT-LICENSE
|
37
|
+
- README
|
38
|
+
- Rakefile
|
39
|
+
- TODO
|
40
|
+
- acts_as_state_machine.gemspec
|
41
|
+
- init.rb
|
42
|
+
- lib/acts_as_state_machine.rb
|
43
|
+
- rails/init.rb
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://github.com/jcnetdev/acts_as_state_machine
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --main
|
51
|
+
- README
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Allows ActiveRecord models to define states and transition actions between them
|
73
|
+
test_files:
|
74
|
+
- test/fixtures/conversations.yml
|
75
|
+
- test/test_acts_as_state_machine.rb
|