tastebook-acts_as_state_machine 3.0.2

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,31 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'tastebook-acts_as_state_machine'
3
+ s.version = '3.0.2'
4
+ s.date = '2013-03-26'
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 = 'ssinghi@kreeti.com'
11
+ s.homepage = 'http://github.com/tastebook/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', ['>= 3.1']
18
+ s.require_paths = ["lib"]
19
+
20
+ s.files = ["CHANGELOG",
21
+ "MIT-LICENSE",
22
+ "README",
23
+ "Rakefile",
24
+ "TODO",
25
+ "acts_as_state_machine.gemspec",
26
+ "lib/acts_as_state_machine.rb"]
27
+
28
+ s.test_files = ["test/fixtures",
29
+ "test/fixtures/conversations.yml",
30
+ "test/test_acts_as_state_machine.rb"]
31
+ end
@@ -0,0 +1,269 @@
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
+ class State
16
+ attr_reader :name, :value
17
+
18
+ def initialize(name, options)
19
+ @name = name.to_sym
20
+ @value = (options[:value] || @name).to_s
21
+ @after = Array(options[:after])
22
+ @enter = options[:enter]
23
+ @exit = options[:exit]
24
+ end
25
+
26
+ def entering(record, event, *args)
27
+ record.send(:run_transition_action, @enter, event, *args)
28
+ end
29
+
30
+ def entered(record, event, *args)
31
+ @after.each { |action| record.send(:run_transition_action, action, event, *args) }
32
+ end
33
+
34
+ def exited(record, event, *args)
35
+ record.send(:run_transition_action, @exit, event, *args)
36
+ end
37
+ end
38
+
39
+ class StateTransition
40
+ attr_reader :from, :to, :opts, :event
41
+
42
+ def initialize(options)
43
+ @from = options[:from].to_s
44
+ @to = options[:to].to_s
45
+ @guard = options[:guard]
46
+ @event = options[:event]
47
+ @opts = options
48
+ end
49
+
50
+ def guard(obj)
51
+ @guard ? obj.send(:run_guard_action, @guard) : true
52
+ end
53
+
54
+ def perform(record, *args)
55
+ return false unless guard(record)
56
+ loopback = record.current_state.to_s == to
57
+ states = record.class._states
58
+ next_state = states[to]
59
+ old_state = states[record.current_state.to_s]
60
+
61
+ next_state.entering(record, event, *args) unless loopback
62
+
63
+ record.update_attribute(record.class.state_column, next_state.value)
64
+
65
+ next_state.entered(record, event, *args) unless loopback
66
+ old_state.exited(record, event, *args) unless loopback
67
+ true
68
+ end
69
+
70
+ def ==(obj)
71
+ @from == obj.from && @to == obj.to
72
+ end
73
+ end
74
+
75
+ class Event
76
+ attr_reader :name
77
+ attr_reader :transitions
78
+ attr_reader :opts
79
+
80
+ def initialize(name, opts, transition_table, &block)
81
+ @name = name.to_sym
82
+ @transitions = transition_table[@name] = []
83
+ instance_eval(&block) if block
84
+ @opts = opts
85
+ @opts.freeze
86
+ @transitions.freeze
87
+ freeze
88
+ end
89
+
90
+ def next_states(record)
91
+ @transitions.select { |t| t.from == record.current_state.to_s }
92
+ end
93
+
94
+ def fire(record, *args)
95
+ next_states(record).each do |transition|
96
+ break true if transition.perform(record, *args)
97
+ end
98
+ end
99
+
100
+ def transitions(trans_opts)
101
+ Array(trans_opts[:from]).each do |s|
102
+ @transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym, :event => self}))
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ module ActMacro
109
+ # Configuration options are
110
+ #
111
+ # * +column+ - specifies the column name to use for keeping the state (default: state)
112
+ # * +initial+ - specifies an initial state for newly created objects (required)
113
+ def acts_as_state_machine(options = {})
114
+ class_eval do
115
+ extend ClassMethods
116
+ include InstanceMethods
117
+
118
+ raise NoInitialState unless options[:initial]
119
+
120
+ class_attribute :_states
121
+ self._states = {}
122
+
123
+ class_attribute :initial_state
124
+ self.initial_state = options[:initial]
125
+
126
+ class_attribute :transition_table
127
+ self.transition_table = {}
128
+
129
+ class_attribute :event_table
130
+ self.event_table = {}
131
+
132
+ class_attribute :state_column
133
+ self.state_column = options[:column] || 'state'
134
+
135
+ before_create :set_initial_state
136
+ after_create :run_initial_state_actions
137
+ end
138
+ end
139
+ end
140
+
141
+ module InstanceMethods
142
+ def set_initial_state #:nodoc:
143
+ write_attribute self.class.state_column, self.class.initial_state.to_s
144
+ end
145
+
146
+ def run_initial_state_actions
147
+ initial = self.class._states[self.class.initial_state.to_s]
148
+ initial.entering(self, nil)
149
+ initial.entered(self, nil)
150
+ end
151
+
152
+ # Returns the current state the object is in, as a Ruby symbol.
153
+ def current_state
154
+ self.send(self.class.state_column).to_sym
155
+ end
156
+
157
+ # Returns what the next state for a given event would be, as a Ruby symbol.
158
+ def next_state_for_event(event)
159
+ ns = next_states_for_event(event)
160
+ ns.empty? ? nil : ns.first.to.to_sym
161
+ end
162
+
163
+ def next_states_for_event(event)
164
+ self.class.transition_table[event.to_sym].select do |s|
165
+ s.from == current_state.to_s
166
+ end
167
+ end
168
+
169
+ private
170
+ def run_guard_action(action)
171
+ Symbol === action ? self.method(action).call : action.call(self) if action
172
+ end
173
+
174
+ def run_transition_action(action, event, *args)
175
+ Symbol === action ? self.method(action).call(event, *args) : action.call(self, event, *args) if action
176
+ end
177
+ end
178
+
179
+ module ClassMethods
180
+ # Returns an array of all known states.
181
+ def states
182
+ _states.keys.collect { |state| state.to_sym }
183
+ end
184
+
185
+ # Define an event. This takes a block which describes all valid transitions
186
+ # for this event.
187
+ #
188
+ # Example:
189
+ #
190
+ # class Order < ActiveRecord::Base
191
+ # acts_as_state_machine :initial => :open
192
+ #
193
+ # state :open
194
+ # state :closed
195
+ #
196
+ # event :close_order do
197
+ # transitions :to => :closed, :from => :open
198
+ # end
199
+ # end
200
+ #
201
+ # +transitions+ takes a hash where <tt>:to</tt> is the state to transition
202
+ # to and <tt>:from</tt> is a state (or Array of states) from which this
203
+ # event can be fired.
204
+ #
205
+ # This creates an instance method used for firing the event. The method
206
+ # created is the name of the event followed by an exclamation point (!).
207
+ # Example: <tt>order.close_order!</tt>.
208
+ def event(event, opts={}, &block)
209
+ e = SupportingClasses::Event.new(event, opts, transition_table, &block)
210
+ self.event_table = event_table.merge(event.to_sym => e)
211
+ define_method("#{event.to_s}!") { |*args| raise InvalidState unless e.fire(self, *args) }
212
+ end
213
+
214
+ # Define a state of the system. +state+ can take an optional Proc object
215
+ # which will be executed every time the system transitions into that
216
+ # state. The proc will be passed the current object.
217
+ #
218
+ # Example:
219
+ #
220
+ # class Order < ActiveRecord::Base
221
+ # acts_as_state_machine :initial => :open
222
+ #
223
+ # state :open
224
+ # state :closed, Proc.new { |o| Mailer.send_notice(o) }
225
+ # end
226
+ def state(name, opts={})
227
+ state = SupportingClasses::State.new(name, opts)
228
+ self._states = _states.merge(state.value => state)
229
+
230
+ define_method("#{state.name}?") { current_state.to_s == state.value }
231
+ end
232
+
233
+ # Wraps ActiveRecord::Base.find to conveniently find all records in
234
+ # a given state. Options:
235
+ #
236
+ # * +number+ - This is just :first or :all from ActiveRecord +find+
237
+ # * +state+ - The state to find
238
+ # * +args+ - The rest of the args are passed down to ActiveRecord +find+
239
+ def find_in_state(number, state, *args)
240
+ _state_scope(state).find(number, *args)
241
+ end
242
+
243
+ # Wraps ActiveRecord::Base.count to conveniently count all records in
244
+ # a given state. Options:
245
+ #
246
+ # * +state+ - The state to find
247
+ # * +args+ - The rest of the args are passed down to ActiveRecord +find+
248
+ def count_in_state(state, *args)
249
+ _state_scope(state).count(*args)
250
+ end
251
+
252
+ # Wraps ActiveRecord::Base.calculate to conveniently calculate all records in
253
+ # a given state. Options:
254
+ #
255
+ # * +state+ - The state to find
256
+ # * +args+ - The rest of the args are passed down to ActiveRecord +calculate+
257
+ def calculate_in_state(state, *args)
258
+ _state_scope(state).calculate(*args)
259
+ end
260
+
261
+ protected
262
+ def _state_scope(state)
263
+ raise InvalidState unless states.include?(state.to_sym)
264
+ where(state_column => state.to_s)
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,11 @@
1
+ first:
2
+ id: 1
3
+ state_machine: read
4
+ subject: This is a test
5
+ closed: false
6
+
7
+ second:
8
+ id: 2
9
+ state_machine: read
10
+ subject: Foo
11
+ closed: false
@@ -0,0 +1,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
+ self._states = {}
108
+ self.finitial_state = nil
109
+ self.transition_table = {}
110
+ self.event_table = {}
111
+ self.state_column = "state"
112
+
113
+ # Clear out any callbacks that were set by acts_as_state_machine.
114
+ reset_callbacks :before_create
115
+ reset_callbacks :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,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tastebook-acts_as_state_machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - RailsJedi
9
+ - Scott Barron
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-03-26 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ requirement: &2151819800 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '3.1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2151819800
26
+ description: This act gives an Active Record model the ability to act as a finite
27
+ state machine (FSM).
28
+ email: ssinghi@kreeti.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files:
32
+ - README
33
+ files:
34
+ - CHANGELOG
35
+ - MIT-LICENSE
36
+ - README
37
+ - Rakefile
38
+ - TODO
39
+ - acts_as_state_machine.gemspec
40
+ - lib/acts_as_state_machine.rb
41
+ - test/fixtures/conversations.yml
42
+ - test/test_acts_as_state_machine.rb
43
+ homepage: http://github.com/tastebook/acts_as_state_machine
44
+ licenses: []
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.17
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Allows ActiveRecord models to define states and transition actions between
69
+ them
70
+ test_files:
71
+ - test/fixtures/conversations.yml
72
+ - test/test_acts_as_state_machine.rb