transitions 0.0.9 → 0.0.10
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/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
|