mongoid_state_machine 0.1
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/.gitignore +6 -0
- data/Gemfile +3 -0
- data/README.rdoc +64 -0
- data/Rakefile +11 -0
- data/TODO.rdoc +11 -0
- data/lib/mongoid/state_machine.rb +231 -0
- data/mongoid_state_machine.gemspec +24 -0
- data/test/mongoid/test_state_machine.rb +489 -0
- metadata +123 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
= Acts As State Machine for Mongoid
|
|
2
|
+
|
|
3
|
+
It's a customized plugin from http://github.com/omghax/acts_as_state_machine that works smoothly through Mongoid.
|
|
4
|
+
|
|
5
|
+
I only removed the Class Methods that were using the method with_scope (ActiveRecord) because it's not supported in Mongoid (not a problem for me)
|
|
6
|
+
|
|
7
|
+
== Installation
|
|
8
|
+
|
|
9
|
+
Add the gem into your Gemfile
|
|
10
|
+
|
|
11
|
+
gem "mongoid_state_machine", :require => "mongoid/state_machine"
|
|
12
|
+
|
|
13
|
+
And run `bundle install`
|
|
14
|
+
|
|
15
|
+
== Example
|
|
16
|
+
|
|
17
|
+
class Order
|
|
18
|
+
include Mongoid::Document
|
|
19
|
+
|
|
20
|
+
include Mongoid::StateMachine
|
|
21
|
+
|
|
22
|
+
state_machine :initial => :opened
|
|
23
|
+
|
|
24
|
+
state :opened
|
|
25
|
+
state :closed, :enter => Proc.new {|o| Mailer.send_notice(o)}
|
|
26
|
+
state :returned
|
|
27
|
+
|
|
28
|
+
event :close do
|
|
29
|
+
transitions :to => :closed, :from => :opened
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
event :return do
|
|
33
|
+
transitions :to => :returned, :from => :closed
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
o = Order.create
|
|
38
|
+
o.close! # notice is sent by mailer
|
|
39
|
+
o.return!
|
|
40
|
+
|
|
41
|
+
== Acknowledge
|
|
42
|
+
This project was originally developed by Scott Barron
|
|
43
|
+
|
|
44
|
+
== License
|
|
45
|
+
Copyright (c) 2010 Bruno Azisaka Maciel
|
|
46
|
+
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of this software and associated documentation files (the
|
|
49
|
+
"Software"), to deal in the Software without restriction, including
|
|
50
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
51
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
52
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
53
|
+
the following conditions:
|
|
54
|
+
|
|
55
|
+
The above copyright notice and this permission notice shall be
|
|
56
|
+
included in all copies or substantial portions of the Software.
|
|
57
|
+
|
|
58
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
59
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
60
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
61
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
62
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
63
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
64
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/TODO.rdoc
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,231 @@
|
|
|
1
|
+
module Mongoid
|
|
2
|
+
module StateMachine #:nodoc:
|
|
3
|
+
|
|
4
|
+
VERSION = "0.1"
|
|
5
|
+
|
|
6
|
+
class InvalidState < Exception #:nodoc:
|
|
7
|
+
end
|
|
8
|
+
class NoInitialState < Exception #:nodoc:
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.included(base) #:nodoc:
|
|
12
|
+
base.extend ActMacro
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module SupportingClasses
|
|
16
|
+
# Default transition action. Always returns true.
|
|
17
|
+
NOOP = lambda { |o| true }
|
|
18
|
+
|
|
19
|
+
class State
|
|
20
|
+
attr_reader :name, :value
|
|
21
|
+
|
|
22
|
+
def initialize(name, options)
|
|
23
|
+
@name = name.to_sym
|
|
24
|
+
@value = (options[:value] || @name).to_s
|
|
25
|
+
@after = Array(options[:after])
|
|
26
|
+
@enter = options[:enter] || NOOP
|
|
27
|
+
@exit = options[:exit] || NOOP
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entering(record)
|
|
31
|
+
record.send(:run_transition_action, @enter)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def entered(record)
|
|
35
|
+
@after.each { |action| record.send(:run_transition_action, action) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def exited(record)
|
|
39
|
+
record.send(:run_transition_action, @exit)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class StateTransition
|
|
44
|
+
attr_reader :from, :to, :opts
|
|
45
|
+
|
|
46
|
+
def initialize(options)
|
|
47
|
+
@from = options[:from].to_s
|
|
48
|
+
@to = options[:to].to_s
|
|
49
|
+
@guard = options[:guard] || NOOP
|
|
50
|
+
@opts = options
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def guard(obj)
|
|
54
|
+
@guard ? obj.send(:run_transition_action, @guard) : true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def perform(record)
|
|
58
|
+
return false unless guard(record)
|
|
59
|
+
loopback = record.current_state.to_s == to
|
|
60
|
+
states = record.class.read_inheritable_attribute(:states)
|
|
61
|
+
next_state = states[to]
|
|
62
|
+
old_state = states[record.current_state.to_s]
|
|
63
|
+
|
|
64
|
+
next_state.entering(record) unless loopback
|
|
65
|
+
|
|
66
|
+
next_state.entered(record) unless loopback
|
|
67
|
+
old_state.exited(record) unless loopback
|
|
68
|
+
|
|
69
|
+
record.send("#{record.class.state_column}=", next_state.value)
|
|
70
|
+
record.save
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ==(obj)
|
|
74
|
+
@from == obj.from && @to == obj.to
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Event
|
|
79
|
+
attr_reader :name
|
|
80
|
+
attr_reader :transitions
|
|
81
|
+
attr_reader :opts
|
|
82
|
+
|
|
83
|
+
def initialize(name, opts, transition_table, &block)
|
|
84
|
+
@name = name.to_sym
|
|
85
|
+
@transitions = transition_table[@name] = []
|
|
86
|
+
instance_eval(&block) if block
|
|
87
|
+
@opts = opts
|
|
88
|
+
@opts.freeze
|
|
89
|
+
@transitions.freeze
|
|
90
|
+
freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def next_states(record)
|
|
94
|
+
@transitions.select { |t| t.from == record.current_state.to_s }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def fire(record)
|
|
98
|
+
next_states(record).each do |transition|
|
|
99
|
+
break true if transition.perform(record)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def transitions(trans_opts)
|
|
104
|
+
Array(trans_opts[:from]).each do |s|
|
|
105
|
+
@transitions << SupportingClasses::StateTransition.new(trans_opts.merge({:from => s.to_sym}))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
module ActMacro
|
|
112
|
+
# Configuration options are
|
|
113
|
+
#
|
|
114
|
+
# * +column+ - specifies the column name to use for keeping the state (default: state)
|
|
115
|
+
# * +initial+ - specifies an initial state for newly created objects (required)
|
|
116
|
+
def state_machine(options = {})
|
|
117
|
+
class_eval do
|
|
118
|
+
extend ClassMethods
|
|
119
|
+
include InstanceMethods
|
|
120
|
+
|
|
121
|
+
raise NoInitialState unless options[:initial]
|
|
122
|
+
|
|
123
|
+
write_inheritable_attribute :states, {}
|
|
124
|
+
write_inheritable_attribute :initial_state, options[:initial]
|
|
125
|
+
write_inheritable_attribute :transition_table, {}
|
|
126
|
+
write_inheritable_attribute :event_table, {}
|
|
127
|
+
write_inheritable_attribute :state_column, options[:column] || 'state'
|
|
128
|
+
|
|
129
|
+
class_inheritable_reader :initial_state
|
|
130
|
+
class_inheritable_reader :state_column
|
|
131
|
+
class_inheritable_reader :transition_table
|
|
132
|
+
class_inheritable_reader :event_table
|
|
133
|
+
|
|
134
|
+
before_create :set_initial_state
|
|
135
|
+
after_create :run_initial_state_actions
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
module InstanceMethods
|
|
141
|
+
def set_initial_state #:nodoc:
|
|
142
|
+
send("#{self.class.state_column}=", self.class.initial_state.to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def run_initial_state_actions
|
|
146
|
+
initial = self.class.read_inheritable_attribute(:states)[self.class.initial_state.to_s]
|
|
147
|
+
initial.entering(self)
|
|
148
|
+
initial.entered(self)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns the current state the object is in, as a Ruby symbol.
|
|
152
|
+
def current_state
|
|
153
|
+
self.send(self.class.state_column).to_sym
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns what the next state for a given event would be, as a Ruby symbol.
|
|
157
|
+
def next_state_for_event(event)
|
|
158
|
+
ns = next_states_for_event(event)
|
|
159
|
+
ns.empty? ? nil : ns.first.to.to_sym
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def next_states_for_event(event)
|
|
163
|
+
self.class.read_inheritable_attribute(:transition_table)[event.to_sym].select do |s|
|
|
164
|
+
s.from == current_state.to_s
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def run_transition_action(action)
|
|
169
|
+
Symbol === action ? self.method(action).call : action.call(self)
|
|
170
|
+
end
|
|
171
|
+
private :run_transition_action
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
module ClassMethods
|
|
175
|
+
# Returns an array of all known states.
|
|
176
|
+
def states
|
|
177
|
+
read_inheritable_attribute(:states).keys.collect { |state| state.to_sym }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Define an event. This takes a block which describes all valid transitions
|
|
181
|
+
# for this event.
|
|
182
|
+
#
|
|
183
|
+
# Example:
|
|
184
|
+
#
|
|
185
|
+
# class Order
|
|
186
|
+
# acts_as_state_machine :initial => :open
|
|
187
|
+
#
|
|
188
|
+
# state :open
|
|
189
|
+
# state :closed
|
|
190
|
+
#
|
|
191
|
+
# event :close_order do
|
|
192
|
+
# transitions :to => :closed, :from => :open
|
|
193
|
+
# end
|
|
194
|
+
# end
|
|
195
|
+
#
|
|
196
|
+
# +transitions+ takes a hash where <tt>:to</tt> is the state to transition
|
|
197
|
+
# to and <tt>:from</tt> is a state (or Array of states) from which this
|
|
198
|
+
# event can be fired.
|
|
199
|
+
#
|
|
200
|
+
# This creates an instance method used for firing the event. The method
|
|
201
|
+
# created is the name of the event followed by an exclamation point (!).
|
|
202
|
+
# Example: <tt>order.close_order!</tt>.
|
|
203
|
+
def event(event, opts={}, &block)
|
|
204
|
+
tt = read_inheritable_attribute(:transition_table)
|
|
205
|
+
|
|
206
|
+
e = SupportingClasses::Event.new(event, opts, tt, &block)
|
|
207
|
+
write_inheritable_hash(:event_table, event.to_sym => e)
|
|
208
|
+
define_method("#{event.to_s}!") { e.fire(self) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Define a state of the system. +state+ can take an optional Proc object
|
|
212
|
+
# which will be executed every time the system transitions into that
|
|
213
|
+
# state. The proc will be passed the current object.
|
|
214
|
+
#
|
|
215
|
+
# Example:
|
|
216
|
+
#
|
|
217
|
+
# class Order
|
|
218
|
+
# acts_as_state_machine :initial => :open
|
|
219
|
+
#
|
|
220
|
+
# state :open
|
|
221
|
+
# state :closed, Proc.new { |o| Mailer.send_notice(o) }
|
|
222
|
+
# end
|
|
223
|
+
def state(name, opts={})
|
|
224
|
+
state = SupportingClasses::State.new(name, opts)
|
|
225
|
+
write_inheritable_hash(:states, state.value => state)
|
|
226
|
+
|
|
227
|
+
define_method("#{state.name}?") { current_state.to_s == state.value }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require File.expand_path("../lib/mongoid/state_machine", __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = "mongoid_state_machine"
|
|
5
|
+
s.version = Mongoid::StateMachine::VERSION
|
|
6
|
+
s.platform = Gem::Platform::RUBY
|
|
7
|
+
s.authors = ["Bruno Azisaka Maciel"]
|
|
8
|
+
s.email = ["bruno@azisaka.com.br"]
|
|
9
|
+
s.homepage = "https://github.com/azisaka/mongoid_state_machine"
|
|
10
|
+
s.summary = "A fork from the original State Machine to run on top of Mongoid"
|
|
11
|
+
s.description = "A fork from the original State Machine to run on top of Mongoid"
|
|
12
|
+
|
|
13
|
+
s.required_rubygems_version = ">= 1.3.6"
|
|
14
|
+
s.add_dependency "mongoid", ">= 2.0.0.beta.19"
|
|
15
|
+
|
|
16
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
|
17
|
+
s.add_development_dependency "rake", ">= 0"
|
|
18
|
+
|
|
19
|
+
s.files = `git ls-files`.split("\n")
|
|
20
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
|
21
|
+
s.require_path = 'lib'
|
|
22
|
+
|
|
23
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
24
|
+
end
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
require "rubygems"
|
|
2
|
+
require "test/unit"
|
|
3
|
+
|
|
4
|
+
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
|
5
|
+
require "init"
|
|
6
|
+
|
|
7
|
+
connection = Mongo::Connection.new
|
|
8
|
+
Mongoid.database = connection.db('state_machine_test')
|
|
9
|
+
|
|
10
|
+
class Conversation
|
|
11
|
+
include Mongoid::Document
|
|
12
|
+
include Mongoid::StateMachine
|
|
13
|
+
|
|
14
|
+
attr_writer :can_close
|
|
15
|
+
attr_accessor :read_enter, :read_exit,
|
|
16
|
+
:needs_attention_enter, :needs_attention_after,
|
|
17
|
+
:read_after_first, :read_after_second,
|
|
18
|
+
:closed_after
|
|
19
|
+
|
|
20
|
+
field :state
|
|
21
|
+
field :state_machine
|
|
22
|
+
|
|
23
|
+
# How's THAT for self-documenting? ;-)
|
|
24
|
+
def always_true
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def can_close?
|
|
29
|
+
!!@can_close
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def read_enter_action
|
|
33
|
+
self.read_enter = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read_after_first_action
|
|
37
|
+
self.read_after_first = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_after_second_action
|
|
41
|
+
self.read_after_second = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def closed_after_action
|
|
45
|
+
self.closed_after = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Mongoid::StateMachineTest < Test::Unit::TestCase
|
|
50
|
+
include Mongoid::StateMachine
|
|
51
|
+
|
|
52
|
+
def after
|
|
53
|
+
Conversation.destroy_all
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def teardown
|
|
57
|
+
Conversation.class_eval do
|
|
58
|
+
write_inheritable_attribute :states, {}
|
|
59
|
+
write_inheritable_attribute :initial_state, nil
|
|
60
|
+
write_inheritable_attribute :transition_table, {}
|
|
61
|
+
write_inheritable_attribute :event_table, {}
|
|
62
|
+
write_inheritable_attribute :state_column, "state"
|
|
63
|
+
|
|
64
|
+
# Clear out any callbacks that were set by state_machine.
|
|
65
|
+
write_inheritable_attribute :before_create, []
|
|
66
|
+
write_inheritable_attribute :after_create, []
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_no_initial_value_raises_exception
|
|
71
|
+
assert_raises(NoInitialState) do
|
|
72
|
+
Conversation.class_eval do
|
|
73
|
+
state_machine
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_state_column
|
|
79
|
+
Conversation.class_eval do
|
|
80
|
+
state_machine :initial => :needs_attention, :column => "state_machine"
|
|
81
|
+
state :needs_attention
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
assert_equal "state_machine", Conversation.state_column
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_initial_state_value
|
|
88
|
+
Conversation.class_eval do
|
|
89
|
+
state_machine :initial => :needs_attention
|
|
90
|
+
state :needs_attention
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
assert_equal :needs_attention, Conversation.initial_state
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_initial_state
|
|
97
|
+
Conversation.class_eval do
|
|
98
|
+
state_machine :initial => :needs_attention
|
|
99
|
+
state :needs_attention
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
c = Conversation.create!
|
|
103
|
+
assert_equal :needs_attention, c.current_state
|
|
104
|
+
assert c.needs_attention?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_states_were_set
|
|
108
|
+
Conversation.class_eval do
|
|
109
|
+
state_machine :initial => :needs_attention
|
|
110
|
+
state :needs_attention
|
|
111
|
+
state :read
|
|
112
|
+
state :closed
|
|
113
|
+
state :awaiting_response
|
|
114
|
+
state :junk
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
[:needs_attention, :read, :closed, :awaiting_response, :junk].each do |state|
|
|
118
|
+
assert Conversation.states.include?(state)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_query_methods_created
|
|
123
|
+
Conversation.class_eval do
|
|
124
|
+
state_machine :initial => :needs_attention
|
|
125
|
+
state :needs_attention
|
|
126
|
+
state :read
|
|
127
|
+
state :closed
|
|
128
|
+
state :awaiting_response
|
|
129
|
+
state :junk
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
c = Conversation.create!
|
|
133
|
+
[:needs_attention?, :read?, :closed?, :awaiting_response?, :junk?].each do |query|
|
|
134
|
+
assert c.respond_to?(query)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_event_methods_created
|
|
139
|
+
Conversation.class_eval do
|
|
140
|
+
state_machine :initial => :needs_attention
|
|
141
|
+
state :needs_attention
|
|
142
|
+
state :read
|
|
143
|
+
state :closed
|
|
144
|
+
state :awaiting_response
|
|
145
|
+
state :junk
|
|
146
|
+
|
|
147
|
+
event(:new_message) {}
|
|
148
|
+
event(:view) {}
|
|
149
|
+
event(:reply) {}
|
|
150
|
+
event(:close) {}
|
|
151
|
+
event(:junk, :note => "finished") {}
|
|
152
|
+
event(:unjunk) {}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
c = Conversation.create!
|
|
156
|
+
[:new_message!, :view!, :reply!, :close!, :junk!, :unjunk!].each do |event|
|
|
157
|
+
assert c.respond_to?(event)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_transition_table
|
|
162
|
+
Conversation.class_eval do
|
|
163
|
+
state_machine :initial => :needs_attention
|
|
164
|
+
state :needs_attention
|
|
165
|
+
state :read
|
|
166
|
+
state :closed
|
|
167
|
+
state :awaiting_response
|
|
168
|
+
state :junk
|
|
169
|
+
|
|
170
|
+
event :new_message do
|
|
171
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
tt = Conversation.transition_table
|
|
176
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :read, :to => :needs_attention))
|
|
177
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :closed, :to => :needs_attention))
|
|
178
|
+
assert tt[:new_message].include?(SupportingClasses::StateTransition.new(:from => :awaiting_response, :to => :needs_attention))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_next_state_for_event
|
|
182
|
+
Conversation.class_eval do
|
|
183
|
+
state_machine :initial => :needs_attention
|
|
184
|
+
state :needs_attention
|
|
185
|
+
state :read
|
|
186
|
+
|
|
187
|
+
event :view do
|
|
188
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
c = Conversation.create!
|
|
193
|
+
assert_equal :read, c.next_state_for_event(:view)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def test_change_state
|
|
197
|
+
Conversation.class_eval do
|
|
198
|
+
state_machine :initial => :needs_attention
|
|
199
|
+
state :needs_attention
|
|
200
|
+
state :read
|
|
201
|
+
|
|
202
|
+
event :view do
|
|
203
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
c = Conversation.create!
|
|
208
|
+
c.view!
|
|
209
|
+
assert c.read?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_can_go_from_read_to_closed_because_guard_passes
|
|
213
|
+
Conversation.class_eval do
|
|
214
|
+
state_machine :initial => :needs_attention
|
|
215
|
+
state :needs_attention
|
|
216
|
+
state :read
|
|
217
|
+
state :closed
|
|
218
|
+
state :awaiting_response
|
|
219
|
+
|
|
220
|
+
event :view do
|
|
221
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
event :reply do
|
|
225
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
event :close do
|
|
229
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
c = Conversation.create!
|
|
234
|
+
c.can_close = true
|
|
235
|
+
c.view!
|
|
236
|
+
c.reply!
|
|
237
|
+
c.close!
|
|
238
|
+
assert_equal :closed, c.current_state
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_cannot_go_from_read_to_closed_because_of_guard
|
|
242
|
+
Conversation.class_eval do
|
|
243
|
+
state_machine :initial => :needs_attention
|
|
244
|
+
state :needs_attention
|
|
245
|
+
state :read
|
|
246
|
+
state :closed
|
|
247
|
+
state :awaiting_response
|
|
248
|
+
|
|
249
|
+
event :view do
|
|
250
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
event :reply do
|
|
254
|
+
transitions :to => :awaiting_response, :from => [:read, :closed]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
event :close do
|
|
258
|
+
transitions :to => :closed, :from => [:read, :awaiting_response], :guard => lambda { |o| o.can_close? }
|
|
259
|
+
transitions :to => :read, :from => [:read, :awaiting_response], :guard => :always_true
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
c = Conversation.create!
|
|
264
|
+
c.can_close = false
|
|
265
|
+
c.view!
|
|
266
|
+
c.reply!
|
|
267
|
+
c.close!
|
|
268
|
+
assert_equal :read, c.current_state
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_ignore_invalid_events
|
|
272
|
+
Conversation.class_eval do
|
|
273
|
+
state_machine :initial => :needs_attention
|
|
274
|
+
state :needs_attention
|
|
275
|
+
state :read
|
|
276
|
+
state :closed
|
|
277
|
+
state :awaiting_response
|
|
278
|
+
state :junk
|
|
279
|
+
|
|
280
|
+
event :new_message do
|
|
281
|
+
transitions :to => :needs_attention, :from => [:read, :closed, :awaiting_response]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
event :view do
|
|
285
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
event :junk, :note => "finished" do
|
|
289
|
+
transitions :to => :junk, :from => [:read, :closed, :awaiting_response]
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
c = Conversation.create
|
|
294
|
+
c.view!
|
|
295
|
+
c.junk!
|
|
296
|
+
|
|
297
|
+
# This is the invalid event
|
|
298
|
+
c.new_message!
|
|
299
|
+
assert_equal :junk, c.current_state
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_entry_action_executed
|
|
303
|
+
Conversation.class_eval do
|
|
304
|
+
state_machine :initial => :needs_attention
|
|
305
|
+
state :needs_attention
|
|
306
|
+
state :read, :enter => :read_enter_action
|
|
307
|
+
|
|
308
|
+
event :view do
|
|
309
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
c = Conversation.create!
|
|
314
|
+
c.read_enter = false
|
|
315
|
+
c.view!
|
|
316
|
+
assert c.read_enter
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def test_after_actions_executed
|
|
320
|
+
Conversation.class_eval do
|
|
321
|
+
state_machine :initial => :needs_attention
|
|
322
|
+
state :needs_attention
|
|
323
|
+
state :closed, :after => :closed_after_action
|
|
324
|
+
state :read, :enter => :read_enter_action,
|
|
325
|
+
:exit => Proc.new { |o| o.read_exit = true },
|
|
326
|
+
:after => [:read_after_first_action, :read_after_second_action]
|
|
327
|
+
|
|
328
|
+
event :view do
|
|
329
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
event :close do
|
|
333
|
+
transitions :to => :closed, :from => [:read, :awaiting_response]
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
c = Conversation.create!
|
|
338
|
+
|
|
339
|
+
c.read_after_first = false
|
|
340
|
+
c.read_after_second = false
|
|
341
|
+
c.closed_after = false
|
|
342
|
+
|
|
343
|
+
c.view!
|
|
344
|
+
assert c.read_after_first
|
|
345
|
+
assert c.read_after_second
|
|
346
|
+
|
|
347
|
+
c.can_close = true
|
|
348
|
+
c.close!
|
|
349
|
+
|
|
350
|
+
assert c.closed_after
|
|
351
|
+
assert_equal :closed, c.current_state
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def test_after_actions_not_run_on_loopback_transition
|
|
355
|
+
Conversation.class_eval do
|
|
356
|
+
state_machine :initial => :needs_attention
|
|
357
|
+
state :needs_attention
|
|
358
|
+
state :closed, :after => :closed_after_action
|
|
359
|
+
state :read, :after => [:read_after_first_action, :read_after_second_action]
|
|
360
|
+
|
|
361
|
+
event :view do
|
|
362
|
+
transitions :to => :read, :from => :needs_attention
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
event :close do
|
|
366
|
+
transitions :to => :closed, :from => :read
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
c = Conversation.create!
|
|
371
|
+
|
|
372
|
+
c.view!
|
|
373
|
+
c.read_after_first = false
|
|
374
|
+
c.read_after_second = false
|
|
375
|
+
c.view!
|
|
376
|
+
|
|
377
|
+
assert !c.read_after_first
|
|
378
|
+
assert !c.read_after_second
|
|
379
|
+
|
|
380
|
+
c.can_close = true
|
|
381
|
+
|
|
382
|
+
c.close!
|
|
383
|
+
c.closed_after = false
|
|
384
|
+
c.close!
|
|
385
|
+
|
|
386
|
+
assert !c.closed_after
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def test_exit_action_executed
|
|
390
|
+
Conversation.class_eval do
|
|
391
|
+
state_machine :initial => :needs_attention
|
|
392
|
+
state :junk
|
|
393
|
+
state :needs_attention
|
|
394
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
|
395
|
+
|
|
396
|
+
event :view do
|
|
397
|
+
transitions :to => :read, :from => :needs_attention
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
event :junk, :note => "finished" do
|
|
401
|
+
transitions :to => :junk, :from => :read
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
c = Conversation.create!
|
|
406
|
+
c.read_exit = false
|
|
407
|
+
c.view!
|
|
408
|
+
c.junk!
|
|
409
|
+
assert c.read_exit
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def test_entry_and_exit_not_run_on_loopback_transition
|
|
413
|
+
Conversation.class_eval do
|
|
414
|
+
state_machine :initial => :needs_attention
|
|
415
|
+
state :needs_attention
|
|
416
|
+
state :read, :exit => lambda { |o| o.read_exit = true }
|
|
417
|
+
|
|
418
|
+
event :view do
|
|
419
|
+
transitions :to => :read, :from => [:needs_attention, :read]
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
c = Conversation.create!
|
|
424
|
+
c.view!
|
|
425
|
+
c.read_enter = false
|
|
426
|
+
c.read_exit = false
|
|
427
|
+
c.view!
|
|
428
|
+
assert !c.read_enter
|
|
429
|
+
assert !c.read_exit
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_entry_and_after_actions_called_for_initial_state
|
|
433
|
+
Conversation.class_eval do
|
|
434
|
+
state_machine :initial => :needs_attention
|
|
435
|
+
state :needs_attention, :enter => lambda { |o| o.needs_attention_enter = true },
|
|
436
|
+
:after => lambda { |o| o.needs_attention_after = true }
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
c = Conversation.create!
|
|
440
|
+
assert c.needs_attention_enter
|
|
441
|
+
assert c.needs_attention_after
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def test_run_transition_action_is_private
|
|
445
|
+
Conversation.class_eval do
|
|
446
|
+
|
|
447
|
+
state_machine :initial => :needs_attention
|
|
448
|
+
state :needs_attention
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
c = Conversation.create!
|
|
452
|
+
assert_raises(NoMethodError) { c.run_transition_action :foo }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def test_can_access_events_via_event_table
|
|
456
|
+
Conversation.class_eval do
|
|
457
|
+
state_machine :initial => :needs_attention, :column => "state_machine"
|
|
458
|
+
state :needs_attention
|
|
459
|
+
state :junk
|
|
460
|
+
|
|
461
|
+
event :junk, :note => "finished" do
|
|
462
|
+
transitions :to => :junk, :from => :needs_attention
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
event = Conversation.event_table[:junk]
|
|
467
|
+
assert_equal :junk, event.name
|
|
468
|
+
assert_equal "finished", event.opts[:note]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def test_custom_state_values
|
|
472
|
+
Conversation.class_eval do
|
|
473
|
+
state_machine :initial => "NEEDS_ATTENTION", :column => "state_machine"
|
|
474
|
+
state :needs_attention, :value => "NEEDS_ATTENTION"
|
|
475
|
+
state :read, :value => "READ"
|
|
476
|
+
|
|
477
|
+
event :view do
|
|
478
|
+
transitions :to => "READ", :from => ["NEEDS_ATTENTION", "READ"]
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
c = Conversation.create!
|
|
483
|
+
assert_equal "NEEDS_ATTENTION", c.state_machine
|
|
484
|
+
assert c.needs_attention?
|
|
485
|
+
c.view!
|
|
486
|
+
assert_equal "READ", c.state_machine
|
|
487
|
+
assert c.read?
|
|
488
|
+
end
|
|
489
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mongoid_state_machine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 9
|
|
5
|
+
prerelease: false
|
|
6
|
+
segments:
|
|
7
|
+
- 0
|
|
8
|
+
- 1
|
|
9
|
+
version: "0.1"
|
|
10
|
+
platform: ruby
|
|
11
|
+
authors:
|
|
12
|
+
- Bruno Azisaka Maciel
|
|
13
|
+
autorequire:
|
|
14
|
+
bindir: bin
|
|
15
|
+
cert_chain: []
|
|
16
|
+
|
|
17
|
+
date: 2011-01-26 00:00:00 -02:00
|
|
18
|
+
default_executable:
|
|
19
|
+
dependencies:
|
|
20
|
+
- !ruby/object:Gem::Dependency
|
|
21
|
+
name: mongoid
|
|
22
|
+
prerelease: false
|
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
24
|
+
none: false
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
hash: 62196421
|
|
29
|
+
segments:
|
|
30
|
+
- 2
|
|
31
|
+
- 0
|
|
32
|
+
- 0
|
|
33
|
+
- beta
|
|
34
|
+
- 19
|
|
35
|
+
version: 2.0.0.beta.19
|
|
36
|
+
type: :runtime
|
|
37
|
+
version_requirements: *id001
|
|
38
|
+
- !ruby/object:Gem::Dependency
|
|
39
|
+
name: bundler
|
|
40
|
+
prerelease: false
|
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
42
|
+
none: false
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
hash: 23
|
|
47
|
+
segments:
|
|
48
|
+
- 1
|
|
49
|
+
- 0
|
|
50
|
+
- 0
|
|
51
|
+
version: 1.0.0
|
|
52
|
+
type: :development
|
|
53
|
+
version_requirements: *id002
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
prerelease: false
|
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
|
58
|
+
none: false
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
hash: 3
|
|
63
|
+
segments:
|
|
64
|
+
- 0
|
|
65
|
+
version: "0"
|
|
66
|
+
type: :development
|
|
67
|
+
version_requirements: *id003
|
|
68
|
+
description: A fork from the original State Machine to run on top of Mongoid
|
|
69
|
+
email:
|
|
70
|
+
- bruno@azisaka.com.br
|
|
71
|
+
executables: []
|
|
72
|
+
|
|
73
|
+
extensions: []
|
|
74
|
+
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
|
|
77
|
+
files:
|
|
78
|
+
- .gitignore
|
|
79
|
+
- Gemfile
|
|
80
|
+
- README.rdoc
|
|
81
|
+
- Rakefile
|
|
82
|
+
- TODO.rdoc
|
|
83
|
+
- lib/mongoid/state_machine.rb
|
|
84
|
+
- mongoid_state_machine.gemspec
|
|
85
|
+
- test/mongoid/test_state_machine.rb
|
|
86
|
+
has_rdoc: true
|
|
87
|
+
homepage: https://github.com/azisaka/mongoid_state_machine
|
|
88
|
+
licenses: []
|
|
89
|
+
|
|
90
|
+
post_install_message:
|
|
91
|
+
rdoc_options:
|
|
92
|
+
- --charset=UTF-8
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
none: false
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
hash: 3
|
|
101
|
+
segments:
|
|
102
|
+
- 0
|
|
103
|
+
version: "0"
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
none: false
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
hash: 23
|
|
110
|
+
segments:
|
|
111
|
+
- 1
|
|
112
|
+
- 3
|
|
113
|
+
- 6
|
|
114
|
+
version: 1.3.6
|
|
115
|
+
requirements: []
|
|
116
|
+
|
|
117
|
+
rubyforge_project:
|
|
118
|
+
rubygems_version: 1.3.7
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 3
|
|
121
|
+
summary: A fork from the original State Machine to run on top of Mongoid
|
|
122
|
+
test_files: []
|
|
123
|
+
|