transitions 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.rdoc +75 -28
- data/lib/active_record/transitions.rb +18 -3
- data/lib/transitions.rb +1 -1
- data/lib/transitions/state_transition.rb +9 -0
- data/lib/transitions/version.rb +1 -1
- data/test/test_active_record.rb +42 -2
- data/test/test_state_transition_callbacks.rb +43 -0
- metadata +6 -23
data/Gemfile.lock
CHANGED
data/README.rdoc
CHANGED
@@ -1,41 +1,88 @@
|
|
1
|
-
= transitions
|
1
|
+
= What is transitions?
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
commit}[http://github.com/rails/rails/commit/db49c706b62e7ea2ab93f05399dbfddf5087ee0c].
|
3
|
+
transitions is a ruby state machine implementation based on Rick Olson’s
|
4
|
+
ActiveModel::StateMachine. It was extracted from ActiveModel and turned
|
5
|
+
into a gem when it got the axe in commit {db49c706b}[http://github.com/rails/rails/commit/db49c706b62e7ea2ab93f05399dbfddf5087ee0c].
|
6
6
|
|
7
|
-
|
7
|
+
I really encourage you to try {state_machine}[https://github.com/pluginaweek/state_machine] before using this gem. Currently I have no time to maintain the gem, if you want to add some new features - contact with me.
|
8
8
|
|
9
|
-
|
9
|
+
== Quick Example
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
require 'transitions'
|
12
|
+
|
13
|
+
class Product
|
14
|
+
include Transitions
|
15
|
+
|
16
|
+
state_machine do
|
17
|
+
state :available # first one is initial state
|
18
|
+
state :out_of_stock, :exit => :exit_out_of_stock
|
19
|
+
state :discontinued, :enter => lambda { |product| product.cancel_orders }
|
20
|
+
|
21
|
+
event :discontinued do
|
22
|
+
transitions :to => :discontinued, :from => [:available, :out_of_stock], :on_transition => :do_discontinue
|
23
|
+
end
|
24
|
+
event :out_of_stock do
|
25
|
+
transitions :to => :out_of_stock, :from => [:available, :discontinued]
|
26
|
+
end
|
27
|
+
event :available do
|
28
|
+
transitions :to => :available, :from => [:out_of_stock], :guard => lambda { |product| product.in_stock > 0 }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
13
32
|
|
14
|
-
|
15
|
-
include ::Transitions
|
16
|
-
include ActiveRecord::Transitions
|
33
|
+
== Using on_transition
|
17
34
|
|
18
|
-
|
19
|
-
|
20
|
-
state :out_of_stock
|
21
|
-
state :discontinue
|
35
|
+
Each event definition takes an optional "on_transition" argument, which allows you to execute methods on transition.
|
36
|
+
You can pass in a Symbol, a String, a Proc or an Array containing method names as Symbol or String like this:
|
22
37
|
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
event :out_of_stock do
|
27
|
-
transitions :to => :out_of_stock, :from => [:available, :discontinue]
|
28
|
-
end
|
29
|
-
event :available do
|
30
|
-
transitions :to => :available, :from => [:out_of_stock], :on_transition => :send_alerts
|
31
|
-
end
|
38
|
+
event :discontinue do
|
39
|
+
transitions :to => :discontinued, :from => [:available, :out_of_stock], :on_transition => [:do_discontinue, :notify_clerk]
|
32
40
|
end
|
33
41
|
|
34
|
-
==
|
42
|
+
== Using with Rails
|
35
43
|
|
36
|
-
|
37
|
-
|
38
|
-
|
44
|
+
This goes into your Gemfile:
|
45
|
+
|
46
|
+
gem "transitions", :require => ["transitions", "active_record/transitions"]
|
47
|
+
|
48
|
+
… and this into your AR model:
|
49
|
+
|
50
|
+
include ActiveRecord::Transitions
|
51
|
+
|
52
|
+
=== A note about persistence
|
53
|
+
The property used to persist the models’ state is named <tt>state</tt> (really!),
|
54
|
+
which should be a string column wide enough to fit your longest state name.
|
55
|
+
It should also be mentioned that <tt>#save!</tt> is called after every successful event.
|
56
|
+
|
57
|
+
== Event execution flow
|
58
|
+
|
59
|
+
On an event, with our quick example product going from <tt>:available</tt> to
|
60
|
+
<tt>:discontinued</tt> it looks like this:
|
61
|
+
|
62
|
+
1. <tt>baby_ninja.discontinue!(:reason => :pirates)</tt>
|
63
|
+
2. call <tt>:exit</tt> handler of <tt>:available</tt> state
|
64
|
+
3. call <tt>:guard</tt> of <tt>:available to :discontinue</tt> transition within <tt>#discontinue</tt> event
|
65
|
+
4. call <tt>#event_failed(:event)</tt> and abort unless <tt>3.</tt> returned <tt>true</tt>
|
66
|
+
5. call <tt>:on_transition(:reason => :pirates)</tt> of <tt>:available to :discontinue</tt> transition within <tt>#discontinue</tt> event
|
67
|
+
6. call <tt>:enter</tt> handler of <tt>:discontinue</tt>
|
68
|
+
7. call <tt>#event_fired(:available, :discontinue)</tt>
|
69
|
+
8. call <tt>#write_state(machine, :discontinue)</tt>
|
70
|
+
9. call <tt>#write_state_without_persistence(machine, :discontinue)</tt>
|
71
|
+
10. call <tt>baby_ninja#:success</tt> handler method of <tt>#discontinue</tt> event
|
72
|
+
|
73
|
+
=== A note about events
|
74
|
+
|
75
|
+
When you declare an event <tt>discontinue</tt>, two methods are declared for
|
76
|
+
you: <tt>discontinue</tt> and <tt>discontinue!</tt>. Both events will call
|
77
|
+
<tt>write_state_without_persistence</tt> on successful transition, but only the
|
78
|
+
bang(!)-version will call <tt>write_state</tt>.
|
79
|
+
|
80
|
+
== Documentation, Guides & Examples
|
81
|
+
|
82
|
+
- {Online API Documentation}[http://rdoc.info/github/qoobaa/transitions/master/Transitions]
|
83
|
+
- Krzysiek Heród (aka {Netizer}[http://github.com/netizer]) wrote a nice
|
84
|
+
{blog post}[http://dev.netizer.pl/transitions-state-machine-for-rails-3.html]
|
85
|
+
about using Transitions in ActiveRecord.
|
39
86
|
|
40
87
|
== Copyright
|
41
88
|
|
@@ -26,24 +26,39 @@ module ActiveRecord
|
|
26
26
|
|
27
27
|
included do
|
28
28
|
include ::Transitions
|
29
|
-
|
29
|
+
after_initialize :set_initial_state
|
30
30
|
validates_presence_of :state
|
31
31
|
validate :state_inclusion
|
32
32
|
end
|
33
33
|
|
34
|
+
def reload
|
35
|
+
super.tap do
|
36
|
+
self.class.state_machines.values.each do |sm|
|
37
|
+
remove_instance_variable(sm.current_state_variable) if instance_variable_defined?(sm.current_state_variable)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
34
42
|
protected
|
35
43
|
|
36
44
|
def write_state(state_machine, state)
|
45
|
+
ivar = state_machine.current_state_variable
|
46
|
+
prev_state = current_state(state_machine.name)
|
47
|
+
instance_variable_set(ivar, state)
|
37
48
|
self.state = state.to_s
|
38
49
|
save!
|
50
|
+
rescue ActiveRecord::RecordInvalid
|
51
|
+
self.state = prev_state.to_s
|
52
|
+
instance_variable_set(ivar, prev_state)
|
53
|
+
raise
|
39
54
|
end
|
40
55
|
|
41
56
|
def read_state(state_machine)
|
42
|
-
self.state.to_sym
|
57
|
+
self.state && self.state.to_sym
|
43
58
|
end
|
44
59
|
|
45
60
|
def set_initial_state
|
46
|
-
self.state ||= self.class.state_machine.initial_state.to_s
|
61
|
+
self.state ||= self.class.state_machine.initial_state.to_s if self.has_attribute?(:state)
|
47
62
|
end
|
48
63
|
|
49
64
|
def state_inclusion
|
data/lib/transitions.rb
CHANGED
@@ -46,6 +46,15 @@ module Transitions
|
|
46
46
|
obj.send(@on_transition, *args)
|
47
47
|
when Proc
|
48
48
|
@on_transition.call(obj, *args)
|
49
|
+
when Array
|
50
|
+
@on_transition.each do |callback|
|
51
|
+
# Yes, we're passing always the same parameters for each callback in here.
|
52
|
+
# We should probably drop args altogether in case we get an array.
|
53
|
+
obj.send(callback, *args)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
# TODO We probably should check for this in the constructor and not that late.
|
57
|
+
raise ArgumentError, "You can only pass a Symbol, a String, a Proc or an Array to 'on_transition' - got #{@on_transition.class}." unless @on_transition.nil?
|
49
58
|
end
|
50
59
|
end
|
51
60
|
|
data/lib/transitions/version.rb
CHANGED
data/test/test_active_record.rb
CHANGED
@@ -3,7 +3,10 @@ require 'active_support/core_ext/module/aliasing'
|
|
3
3
|
|
4
4
|
class CreateTrafficLights < ActiveRecord::Migration
|
5
5
|
def self.up
|
6
|
-
create_table(:traffic_lights)
|
6
|
+
create_table(:traffic_lights) do |t|
|
7
|
+
t.string :state
|
8
|
+
t.string :name
|
9
|
+
end
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
@@ -43,6 +46,10 @@ class ValidatingTrafficLight < TrafficLight
|
|
43
46
|
validate {|t| errors.add(:base, 'This TrafficLight will never validate after creation') unless t.new_record? }
|
44
47
|
end
|
45
48
|
|
49
|
+
class ConditionalValidatingTrafficLight < TrafficLight
|
50
|
+
validates(:name, :presence => true, :if => :red?)
|
51
|
+
end
|
52
|
+
|
46
53
|
class TestActiveRecord < Test::Unit::TestCase
|
47
54
|
def setup
|
48
55
|
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
@@ -52,6 +59,11 @@ class TestActiveRecord < Test::Unit::TestCase
|
|
52
59
|
@light = TrafficLight.create!
|
53
60
|
end
|
54
61
|
|
62
|
+
test "new record has the initial state set" do
|
63
|
+
@light = TrafficLight.new
|
64
|
+
assert_equal "off", @light.state
|
65
|
+
end
|
66
|
+
|
55
67
|
test "states initial state" do
|
56
68
|
assert @light.off?
|
57
69
|
assert_equal :off, @light.current_state
|
@@ -104,9 +116,37 @@ class TestActiveRecord < Test::Unit::TestCase
|
|
104
116
|
end
|
105
117
|
|
106
118
|
test "transition raises exception when model validation fails" do
|
107
|
-
validating_light = ValidatingTrafficLight.create!
|
119
|
+
validating_light = ValidatingTrafficLight.create!(:name => 'Foobar')
|
108
120
|
assert_raise(ActiveRecord::RecordInvalid) do
|
109
121
|
validating_light.reset!
|
110
122
|
end
|
111
123
|
end
|
124
|
+
|
125
|
+
test "state query method used in a validation condition" do
|
126
|
+
validating_light = ConditionalValidatingTrafficLight.create!
|
127
|
+
assert_raise(ActiveRecord::RecordInvalid) do
|
128
|
+
validating_light.reset!
|
129
|
+
end
|
130
|
+
assert(validating_light.off?)
|
131
|
+
end
|
132
|
+
|
133
|
+
test "reloading model resets current state" do
|
134
|
+
@light.reset
|
135
|
+
assert @light.red?
|
136
|
+
@light.update_attribute(:state, 'green')
|
137
|
+
assert @light.reload.green?, "reloaded state should come from database, not instance variable"
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
class TestNewActiveRecord < TestActiveRecord
|
143
|
+
|
144
|
+
def setup
|
145
|
+
@light = TrafficLight.new
|
146
|
+
end
|
147
|
+
|
148
|
+
test "new active records defaults current state to the initial state" do
|
149
|
+
assert_equal :off, @light.current_state
|
150
|
+
end
|
151
|
+
|
112
152
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Car
|
4
|
+
include Transitions
|
5
|
+
|
6
|
+
state_machine do
|
7
|
+
state :parked
|
8
|
+
state :running
|
9
|
+
state :driving
|
10
|
+
|
11
|
+
event :turn_key do
|
12
|
+
transitions :from => :parked, :to => :running, :on_transition => :start_engine
|
13
|
+
end
|
14
|
+
|
15
|
+
event :start_driving do
|
16
|
+
transitions :from => :parked, :to => :driving, :on_transition => [:start_engine, :loosen_handbrake, :push_gas_pedal]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
%w!start_engine loosen_handbrake push_gas_pedal!.each do |m|
|
21
|
+
define_method(m){}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TestStateTransitionCallbacks < Test::Unit::TestCase
|
26
|
+
def setup
|
27
|
+
@car = Car.new
|
28
|
+
end
|
29
|
+
|
30
|
+
test "should execute callback defined via 'on_transition'" do
|
31
|
+
@car.expects(:start_engine)
|
32
|
+
@car.turn_key!
|
33
|
+
end
|
34
|
+
|
35
|
+
test "should execute multiple callbacks defined via 'on_transition' in the same order they were defined" do
|
36
|
+
on_transition_sequence = sequence('on_transition_sequence')
|
37
|
+
|
38
|
+
@car.expects(:start_engine).in_sequence(on_transition_sequence)
|
39
|
+
@car.expects(:loosen_handbrake).in_sequence(on_transition_sequence)
|
40
|
+
@car.expects(:push_gas_pedal).in_sequence(on_transition_sequence)
|
41
|
+
@car.start_driving!
|
42
|
+
end
|
43
|
+
end
|
metadata
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: transitions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 0
|
8
|
-
- 9
|
9
|
-
version: 0.0.9
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.10
|
10
6
|
platform: ruby
|
11
7
|
authors:
|
12
8
|
- "Jakub Ku\xC5\xBAma"
|
@@ -14,7 +10,7 @@ autorequire:
|
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
12
|
|
17
|
-
date:
|
13
|
+
date: 2011-08-11 00:00:00 +02:00
|
18
14
|
default_executable:
|
19
15
|
dependencies:
|
20
16
|
- !ruby/object:Gem::Dependency
|
@@ -24,8 +20,6 @@ dependencies:
|
|
24
20
|
requirements:
|
25
21
|
- - ~>
|
26
22
|
- !ruby/object:Gem::Version
|
27
|
-
segments:
|
28
|
-
- 1
|
29
23
|
version: "1"
|
30
24
|
type: :development
|
31
25
|
prerelease: false
|
@@ -37,8 +31,6 @@ dependencies:
|
|
37
31
|
requirements:
|
38
32
|
- - ~>
|
39
33
|
- !ruby/object:Gem::Version
|
40
|
-
segments:
|
41
|
-
- 2
|
42
34
|
version: "2"
|
43
35
|
type: :development
|
44
36
|
prerelease: false
|
@@ -50,8 +42,6 @@ dependencies:
|
|
50
42
|
requirements:
|
51
43
|
- - ">="
|
52
44
|
- !ruby/object:Gem::Version
|
53
|
-
segments:
|
54
|
-
- 0
|
55
45
|
version: "0"
|
56
46
|
type: :development
|
57
47
|
prerelease: false
|
@@ -63,8 +53,6 @@ dependencies:
|
|
63
53
|
requirements:
|
64
54
|
- - ">="
|
65
55
|
- !ruby/object:Gem::Version
|
66
|
-
segments:
|
67
|
-
- 0
|
68
56
|
version: "0"
|
69
57
|
type: :development
|
70
58
|
prerelease: false
|
@@ -76,8 +64,6 @@ dependencies:
|
|
76
64
|
requirements:
|
77
65
|
- - ~>
|
78
66
|
- !ruby/object:Gem::Version
|
79
|
-
segments:
|
80
|
-
- 3
|
81
67
|
version: "3"
|
82
68
|
type: :development
|
83
69
|
prerelease: false
|
@@ -112,6 +98,7 @@ files:
|
|
112
98
|
- test/test_machine.rb
|
113
99
|
- test/test_state.rb
|
114
100
|
- test/test_state_transition.rb
|
101
|
+
- test/test_state_transition_callbacks.rb
|
115
102
|
- test/test_state_transition_guard_check.rb
|
116
103
|
- transitions.gemspec
|
117
104
|
has_rdoc: true
|
@@ -128,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
115
|
requirements:
|
129
116
|
- - ">="
|
130
117
|
- !ruby/object:Gem::Version
|
131
|
-
hash:
|
118
|
+
hash: 347067155
|
132
119
|
segments:
|
133
120
|
- 0
|
134
121
|
version: "0"
|
@@ -137,15 +124,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
124
|
requirements:
|
138
125
|
- - ">="
|
139
126
|
- !ruby/object:Gem::Version
|
140
|
-
segments:
|
141
|
-
- 1
|
142
|
-
- 3
|
143
|
-
- 6
|
144
127
|
version: 1.3.6
|
145
128
|
requirements: []
|
146
129
|
|
147
130
|
rubyforge_project: transitions
|
148
|
-
rubygems_version: 1.
|
131
|
+
rubygems_version: 1.6.2
|
149
132
|
signing_key:
|
150
133
|
specification_version: 3
|
151
134
|
summary: State machine extracted from ActiveModel
|