tastebook-acts_as_state_machine 3.0.2

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