acts_as_state_machine 2.1.3

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