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 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