transitions 0.1.5 → 0.1.6
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.md +7 -0
- data/README.rdoc +34 -1
- data/lib/active_model/transitions.rb +38 -7
- data/lib/transitions/event.rb +12 -6
- data/lib/transitions/state_transition.rb +3 -3
- data/lib/transitions/version.rb +1 -1
- data/test/active_record/test_active_record.rb +156 -0
- data/test/event/test_event.rb +25 -0
- data/test/state_transition/test_state_transition_guard_check.rb +10 -8
- metadata +3 -3
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# 1.0.6
|
2
|
+
|
3
|
+
* (troessner) Revert 'Fixing set_initial_state with Mongoid' because of https://github.com/troessner/transitions/issues/76
|
4
|
+
* (divins) Multiple success callbacks
|
5
|
+
* (cstrahan) Pass additional args to guard function
|
6
|
+
* (cmw) Support for configurable column names
|
7
|
+
|
1
8
|
# 1.0.5
|
2
9
|
|
3
10
|
* (troessner) Fix unhelpful error message when event can not be fired.
|
data/README.rdoc
CHANGED
@@ -44,7 +44,7 @@ This goes into your Gemfile:
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
In this example we assume that you are in a rails project using Bundler, which would
|
47
|
+
In this example we assume that you are in a rails project using Bundler, which would automatically require `transitions`.
|
48
48
|
If this is not the case for you you have to add
|
49
49
|
|
50
50
|
require 'transitions'
|
@@ -98,6 +98,17 @@ you can use this feature a la:
|
|
98
98
|
>> Order.pick_line_items
|
99
99
|
=> [#<Order id: 3, state: "pick_line_items", description: nil, created_at: "2011-08-23 15:48:46", updated_at: "2011-08-23 15:48:46">]
|
100
100
|
|
101
|
+
==== Using <tt>guard</tt>
|
102
|
+
|
103
|
+
Each event definition takes an optional "guard" argument, which acts as a predicate for the transition.
|
104
|
+
You can pass in a Symbol, a String, or a Proc like this:
|
105
|
+
|
106
|
+
event :discontinue do
|
107
|
+
transitions :to => :discontinued, :from => [:available, :out_of_stock], :guard => :can_discontinue
|
108
|
+
end
|
109
|
+
|
110
|
+
Any arguments passed to the event method will be passed on to the <tt>guard</tt> predicate.
|
111
|
+
|
101
112
|
==== Using <tt>on_transition</tt>
|
102
113
|
|
103
114
|
Each event definition takes an optional "on_transition" argument, which allows you to execute methods on transition.
|
@@ -107,6 +118,8 @@ You can pass in a Symbol, a String, a Proc or an Array containing method names a
|
|
107
118
|
transitions :to => :discontinued, :from => [:available, :out_of_stock], :on_transition => [:do_discontinue, :notify_clerk]
|
108
119
|
end
|
109
120
|
|
121
|
+
Any arguments passed to the event method will be passed on to the <tt>on_transition</tt> callback.
|
122
|
+
|
110
123
|
==== Using <tt>success</tt>
|
111
124
|
|
112
125
|
In case you need to trigger a method call after a successful transition you can use <tt>success</tt>:
|
@@ -122,6 +135,12 @@ perfom some more complex success callbacks:
|
|
122
135
|
transitions :to => :discontinued, :from => [:available, :out_of_stock]
|
123
136
|
end
|
124
137
|
|
138
|
+
If you need it, you can even call multiple methods or lambdas just passing an array:
|
139
|
+
|
140
|
+
event :discontinue, :success => [:notify_admin, lambda { |order| AdminNotifier.notify_about_discontinued_order(order) }] do
|
141
|
+
transitions :to => :discontinued, :from => [:available, :out_of_stock]
|
142
|
+
end
|
143
|
+
|
125
144
|
==== Timestamps
|
126
145
|
|
127
146
|
If you'd like to note the time of a state change, Transitions comes with timestamps free!
|
@@ -167,6 +186,20 @@ You can easily get a listing of all available states:
|
|
167
186
|
state :closed
|
168
187
|
end
|
169
188
|
|
189
|
+
=== Configuring a different column name with ActiveRecord
|
190
|
+
|
191
|
+
To use a different column than <tt>state</tt> to track it's value simply do this:
|
192
|
+
|
193
|
+
class Product < ActiveRecord::Base
|
194
|
+
include Transitions
|
195
|
+
|
196
|
+
state_machine :attribute_name => :different_column do
|
197
|
+
|
198
|
+
...
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
170
203
|
=== Documentation, Guides & Examples
|
171
204
|
|
172
205
|
- {Online API Documentation}[http://rdoc.info/github/troessner/transitions/master/Transitions]
|
@@ -25,9 +25,31 @@ module ActiveModel
|
|
25
25
|
extend ActiveSupport::Concern
|
26
26
|
|
27
27
|
included do
|
28
|
+
class ::Transitions::Machine
|
29
|
+
unless instance_methods.include?(:new_initialize) || instance_methods.include?(:new_update)
|
30
|
+
attr_reader :attribute_name
|
31
|
+
alias :old_initialize :initialize
|
32
|
+
alias :old_update :update
|
33
|
+
|
34
|
+
def new_initialize(*args, &block)
|
35
|
+
@attribute_name = :state
|
36
|
+
old_initialize(*args, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def new_update(options = {}, &block)
|
40
|
+
@attribute_name = options[:attribute_name] if options.key?(:attribute_name)
|
41
|
+
old_update(options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
alias :initialize :new_initialize
|
45
|
+
alias :update :new_update
|
46
|
+
else
|
47
|
+
puts "WARNING: Transitions::Machine#new_update or Transitions::Machine#new_initialize already defined. This can possibly break ActiveModel::Transitions."
|
48
|
+
end
|
49
|
+
end
|
28
50
|
include ::Transitions
|
29
51
|
after_initialize :set_initial_state
|
30
|
-
|
52
|
+
validate :state_presence
|
31
53
|
validate :state_inclusion
|
32
54
|
end
|
33
55
|
|
@@ -43,6 +65,10 @@ module ActiveModel
|
|
43
65
|
|
44
66
|
protected
|
45
67
|
|
68
|
+
def transitions_state_column_name
|
69
|
+
self.class.state_machine.attribute_name
|
70
|
+
end
|
71
|
+
|
46
72
|
def write_state(state)
|
47
73
|
prev_state = current_state
|
48
74
|
write_state_without_persistence(state)
|
@@ -55,22 +81,27 @@ module ActiveModel
|
|
55
81
|
def write_state_without_persistence(state)
|
56
82
|
ivar = self.class.get_state_machine.current_state_variable
|
57
83
|
instance_variable_set(ivar, state)
|
58
|
-
self
|
84
|
+
self[transitions_state_column_name] = state.to_s
|
59
85
|
end
|
60
86
|
|
61
87
|
def read_state
|
62
|
-
self
|
88
|
+
self[transitions_state_column_name] && self[transitions_state_column_name].to_sym
|
63
89
|
end
|
64
90
|
|
65
91
|
def set_initial_state
|
66
|
-
self
|
92
|
+
self[transitions_state_column_name] ||= self.class.get_state_machine.initial_state.to_s if self.has_attribute?(transitions_state_column_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def state_presence
|
96
|
+
unless self[transitions_state_column_name].present?
|
97
|
+
self.errors.add(transitions_state_column_name, :presence)
|
98
|
+
end
|
67
99
|
end
|
68
100
|
|
69
101
|
def state_inclusion
|
70
|
-
unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self.
|
71
|
-
self.errors.add(
|
102
|
+
unless self.class.get_state_machine.states.map{|s| s.name.to_s }.include?(self[transitions_state_column_name].to_s)
|
103
|
+
self.errors.add(transitions_state_column_name, :inclusion, :value => self[transitions_state_column_name])
|
72
104
|
end
|
73
105
|
end
|
74
106
|
end
|
75
107
|
end
|
76
|
-
|
data/lib/transitions/event.rb
CHANGED
@@ -49,7 +49,7 @@ module Transitions
|
|
49
49
|
next_state = nil
|
50
50
|
transitions.each do |transition|
|
51
51
|
next if to_state && !Array(transition.to).include?(to_state)
|
52
|
-
if transition.perform(obj)
|
52
|
+
if transition.perform(obj, *args)
|
53
53
|
next_state = to_state || Array(transition.to).first
|
54
54
|
transition.execute(obj, *args)
|
55
55
|
break
|
@@ -78,7 +78,7 @@ module Transitions
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def update(options = {}, &block)
|
81
|
-
@success =
|
81
|
+
@success = build_success_callback(options[:success]) if options.key?(:success)
|
82
82
|
self.timestamp = options[:timestamp] if options[:timestamp]
|
83
83
|
instance_eval(&block) if block
|
84
84
|
self
|
@@ -129,12 +129,18 @@ module Transitions
|
|
129
129
|
end
|
130
130
|
end
|
131
131
|
|
132
|
-
def
|
133
|
-
case
|
132
|
+
def build_success_callback(callback_names)
|
133
|
+
case callback_names
|
134
|
+
when Array
|
135
|
+
lambda do |record|
|
136
|
+
callback_names.each do |callback|
|
137
|
+
build_success_callback(callback).call(record)
|
138
|
+
end
|
139
|
+
end
|
134
140
|
when Proc
|
135
|
-
|
141
|
+
callback_names
|
136
142
|
when Symbol
|
137
|
-
lambda { |record| record.send(
|
143
|
+
lambda { |record| record.send(callback_names) }
|
138
144
|
end
|
139
145
|
end
|
140
146
|
|
@@ -29,12 +29,12 @@ module Transitions
|
|
29
29
|
@options = opts
|
30
30
|
end
|
31
31
|
|
32
|
-
def perform(obj)
|
32
|
+
def perform(obj, *args)
|
33
33
|
case @guard
|
34
34
|
when Symbol, String
|
35
|
-
obj.send(@guard)
|
35
|
+
obj.send(@guard, *args)
|
36
36
|
when Proc
|
37
|
-
@guard.call(obj)
|
37
|
+
@guard.call(obj, *args)
|
38
38
|
else
|
39
39
|
true
|
40
40
|
end
|
data/lib/transitions/version.rb
CHANGED
@@ -11,6 +11,17 @@ end
|
|
11
11
|
|
12
12
|
set_up_db CreateTrafficLights
|
13
13
|
|
14
|
+
class CreateDifferentTrafficLights < ActiveRecord::Migration
|
15
|
+
def self.up
|
16
|
+
create_table(:different_traffic_lights) do |t|
|
17
|
+
t.string :different_state
|
18
|
+
t.string :name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
set_up_db CreateDifferentTrafficLights
|
24
|
+
|
14
25
|
class TrafficLight < ActiveRecord::Base
|
15
26
|
include ActiveModel::Transitions
|
16
27
|
|
@@ -147,3 +158,148 @@ class TestActiveRecord < Test::Unit::TestCase
|
|
147
158
|
assert_equal "red", @light.reload.state
|
148
159
|
end
|
149
160
|
end
|
161
|
+
|
162
|
+
class TestNewActiveRecord < TestActiveRecord
|
163
|
+
|
164
|
+
def setup
|
165
|
+
set_up_db CreateTrafficLights
|
166
|
+
@light = TrafficLight.new
|
167
|
+
end
|
168
|
+
|
169
|
+
test "new active records defaults current state to the initial state" do
|
170
|
+
assert_equal :off, @light.current_state
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
class TestScopes < Test::Unit::TestCase
|
176
|
+
test "scope returns correct object" do
|
177
|
+
@light = TrafficLight.create!
|
178
|
+
assert_respond_to TrafficLight, :off
|
179
|
+
assert_equal TrafficLight.off.first, @light
|
180
|
+
assert TrafficLight.red.empty?
|
181
|
+
end
|
182
|
+
|
183
|
+
test "scopes exist" do
|
184
|
+
assert_respond_to TrafficLight, :off
|
185
|
+
assert_respond_to TrafficLight, :red
|
186
|
+
assert_respond_to TrafficLight, :green
|
187
|
+
assert_respond_to TrafficLight, :yellow
|
188
|
+
end
|
189
|
+
|
190
|
+
test 'scopes are only generated if we explicitly say so' do
|
191
|
+
assert_not_respond_to LightBulb, :off
|
192
|
+
assert_not_respond_to LightBulb, :on
|
193
|
+
end
|
194
|
+
|
195
|
+
test 'scope generation raises an exception if we try to overwrite an existing method' do
|
196
|
+
assert_raise(Transitions::InvalidMethodOverride) {
|
197
|
+
class Light < ActiveRecord::Base
|
198
|
+
include ActiveModel::Transitions
|
199
|
+
|
200
|
+
state_machine :auto_scopes => true do
|
201
|
+
state :new
|
202
|
+
state :broken
|
203
|
+
end
|
204
|
+
end
|
205
|
+
}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class DifferentTrafficLight < ActiveRecord::Base
|
210
|
+
include ActiveModel::Transitions
|
211
|
+
|
212
|
+
state_machine :attribute_name => :different_state, :auto_scopes => true do
|
213
|
+
state :off
|
214
|
+
|
215
|
+
state :red
|
216
|
+
state :green
|
217
|
+
state :yellow
|
218
|
+
|
219
|
+
event :red_on do
|
220
|
+
transitions :to => :red, :from => [:yellow]
|
221
|
+
end
|
222
|
+
|
223
|
+
event :green_on do
|
224
|
+
transitions :to => :green, :from => [:red]
|
225
|
+
end
|
226
|
+
|
227
|
+
event :yellow_on do
|
228
|
+
transitions :to => :yellow, :from => [:green]
|
229
|
+
end
|
230
|
+
|
231
|
+
event :reset do
|
232
|
+
transitions :to => :red, :from => [:off]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class TestActiveRecordWithDifferentColumnName < Test::Unit::TestCase
|
238
|
+
def setup
|
239
|
+
set_up_db CreateDifferentTrafficLights
|
240
|
+
@light = DifferentTrafficLight.create!
|
241
|
+
end
|
242
|
+
|
243
|
+
test "new record has the initial state set" do
|
244
|
+
@light = DifferentTrafficLight.new
|
245
|
+
assert_equal "off", @light.different_state
|
246
|
+
end
|
247
|
+
|
248
|
+
test "states initial state" do
|
249
|
+
assert @light.off?
|
250
|
+
assert_equal :off, @light.current_state
|
251
|
+
end
|
252
|
+
|
253
|
+
test "transition to a valid state" do
|
254
|
+
@light.reset
|
255
|
+
assert @light.red?
|
256
|
+
assert_equal :red, @light.current_state
|
257
|
+
|
258
|
+
@light.green_on
|
259
|
+
assert @light.green?
|
260
|
+
assert_equal :green, @light.current_state
|
261
|
+
end
|
262
|
+
|
263
|
+
test "transition does not persist state" do
|
264
|
+
@light.reset
|
265
|
+
assert_equal :red, @light.current_state
|
266
|
+
@light.reload
|
267
|
+
assert_equal "off", @light.different_state
|
268
|
+
end
|
269
|
+
|
270
|
+
test "transition does persists state" do
|
271
|
+
@light.reset!
|
272
|
+
assert_equal :red, @light.current_state
|
273
|
+
@light.reload
|
274
|
+
assert_equal "red", @light.different_state
|
275
|
+
end
|
276
|
+
|
277
|
+
test "transition to an invalid state" do
|
278
|
+
assert_raise(Transitions::InvalidTransition) { @light.yellow_on }
|
279
|
+
assert_equal :off, @light.current_state
|
280
|
+
end
|
281
|
+
|
282
|
+
test "transition with wrong state will not validate" do
|
283
|
+
for s in @light.class.state_machine.states
|
284
|
+
@light.different_state = s.name
|
285
|
+
assert @light.valid?
|
286
|
+
end
|
287
|
+
@light.different_state = "invalid_one"
|
288
|
+
assert_false @light.valid?
|
289
|
+
end
|
290
|
+
|
291
|
+
test "reloading model resets current state" do
|
292
|
+
@light.reset
|
293
|
+
assert @light.red?
|
294
|
+
@light.update_attribute(:different_state, 'green')
|
295
|
+
assert @light.reload.green?, "reloaded state should come from database, not instance variable"
|
296
|
+
end
|
297
|
+
|
298
|
+
test "calling non-bang event updates state attribute" do
|
299
|
+
@light.reset!
|
300
|
+
assert @light.red?
|
301
|
+
@light.green_on
|
302
|
+
assert_equal "green", @light.different_state
|
303
|
+
assert_equal "red", @light.reload.different_state
|
304
|
+
end
|
305
|
+
end
|
data/test/event/test_event.rb
CHANGED
@@ -5,6 +5,7 @@ class TestEvent < Test::Unit::TestCase
|
|
5
5
|
@state_name = :close_order
|
6
6
|
@success_as_symbol = :success_callback
|
7
7
|
@success_as_lambda = lambda { |record| record.success_callback }
|
8
|
+
@success_as_array = [@success_as_symbol, @success_as_lambda]
|
8
9
|
end
|
9
10
|
|
10
11
|
def event_with_symbol_success_callback
|
@@ -20,6 +21,12 @@ class TestEvent < Test::Unit::TestCase
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
24
|
+
def event_with_array_success_callback
|
25
|
+
@event = Transitions::Event.new(nil, @state_name, {:success => @success_as_array}) do
|
26
|
+
transitions :to => :closed, :from => [:open, :received]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
23
30
|
test "should set the name" do
|
24
31
|
assert_equal @state_name, new_event.name
|
25
32
|
end
|
@@ -39,6 +46,24 @@ class TestEvent < Test::Unit::TestCase
|
|
39
46
|
assert_respond_to event_with_lambda_success_callback.success, :call
|
40
47
|
end
|
41
48
|
|
49
|
+
test "should build a block which calls the given success_callback lambda on the passed record instance" do
|
50
|
+
record = mock("SomeRecordToGetCalled")
|
51
|
+
record.expects(:success_callback)
|
52
|
+
|
53
|
+
event_with_lambda_success_callback.success.call(record)
|
54
|
+
end
|
55
|
+
|
56
|
+
test "should set the success callback with an array" do
|
57
|
+
assert_respond_to event_with_array_success_callback.success, :call
|
58
|
+
end
|
59
|
+
|
60
|
+
test "should build a block which calls the given success_callback array on the passed record instance for each callback" do
|
61
|
+
record = mock("SomeRecordToGetCalled")
|
62
|
+
record.expects(:success_callback).twice
|
63
|
+
|
64
|
+
event_with_array_success_callback.success.call(record)
|
65
|
+
end
|
66
|
+
|
42
67
|
test "should create StateTransitions" do
|
43
68
|
Transitions::StateTransition.expects(:new).with(:to => :closed, :from => :open)
|
44
69
|
Transitions::StateTransition.expects(:new).with(:to => :closed, :from => :received)
|
@@ -1,11 +1,13 @@
|
|
1
1
|
require "helper"
|
2
2
|
|
3
3
|
class TestStateTransitionGuardCheck < Test::Unit::TestCase
|
4
|
+
args = [:foo, "bar"]
|
5
|
+
|
4
6
|
test "should return true of there is no guard" do
|
5
7
|
opts = {:from => "foo", :to => "bar"}
|
6
8
|
st = Transitions::StateTransition.new(opts)
|
7
9
|
|
8
|
-
assert st.perform(nil)
|
10
|
+
assert st.perform(nil, *args)
|
9
11
|
end
|
10
12
|
|
11
13
|
test "should call the method on the object if guard is a symbol" do
|
@@ -13,9 +15,9 @@ class TestStateTransitionGuardCheck < Test::Unit::TestCase
|
|
13
15
|
st = Transitions::StateTransition.new(opts)
|
14
16
|
|
15
17
|
obj = stub
|
16
|
-
obj.expects(:test_guard)
|
18
|
+
obj.expects(:test_guard).with(*args)
|
17
19
|
|
18
|
-
st.perform(obj)
|
20
|
+
st.perform(obj, *args)
|
19
21
|
end
|
20
22
|
|
21
23
|
test "should call the method on the object if guard is a string" do
|
@@ -23,18 +25,18 @@ class TestStateTransitionGuardCheck < Test::Unit::TestCase
|
|
23
25
|
st = Transitions::StateTransition.new(opts)
|
24
26
|
|
25
27
|
obj = stub
|
26
|
-
obj.expects(:test_guard)
|
28
|
+
obj.expects(:test_guard).with(*args)
|
27
29
|
|
28
|
-
st.perform(obj)
|
30
|
+
st.perform(obj, *args)
|
29
31
|
end
|
30
32
|
|
31
33
|
test "should call the proc passing the object if the guard is a proc" do
|
32
|
-
opts = {:from => "foo", :to => "bar", :guard => Proc.new {|o| o.test_guard}}
|
34
|
+
opts = {:from => "foo", :to => "bar", :guard => Proc.new {|o, *args| o.test_guard(*args)}}
|
33
35
|
st = Transitions::StateTransition.new(opts)
|
34
36
|
|
35
37
|
obj = stub
|
36
|
-
obj.expects(:test_guard)
|
38
|
+
obj.expects(:test_guard).with(*args)
|
37
39
|
|
38
|
-
st.perform(obj)
|
40
|
+
st.perform(obj, *args)
|
39
41
|
end
|
40
42
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: transitions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-02-28 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: bundler
|
@@ -179,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
179
179
|
version: '0'
|
180
180
|
segments:
|
181
181
|
- 0
|
182
|
-
hash: -
|
182
|
+
hash: -1024202641
|
183
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
184
|
none: false
|
185
185
|
requirements:
|