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 CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- transitions (0.0.9)
4
+ transitions (0.0.10)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.rdoc CHANGED
@@ -1,41 +1,88 @@
1
- = transitions
1
+ = What is transitions?
2
2
 
3
- The gem is based on Rick Olson's code of ActiveModel::StateMachine,
4
- axed from ActiveModel in {this
5
- commit}[http://github.com/rails/rails/commit/db49c706b62e7ea2ab93f05399dbfddf5087ee0c].
3
+ transitions is a ruby state machine implementation based on Rick Olsons
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
- == Installation
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
- If you're using Rails + ActiveRecord + Bundler
9
+ == Quick Example
10
10
 
11
- # in your Gemfile
12
- gem "transitions", :require => ["transitions", "active_record/transitions"]
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
- # in your AR models that will use the state machine
15
- include ::Transitions
16
- include ActiveRecord::Transitions
33
+ == Using on_transition
17
34
 
18
- state_machine do
19
- state :available # first one is initial state
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
- event :discontinue do
24
- transitions :to => :discontinue, :from => [:available, :out_of_stock], :on_transition => :do_discontinue
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
- == Usage
42
+ == Using with Rails
35
43
 
36
- Krzysiek Heród (aka {Netizer}[http://github.com/netizer]) wrote a nice {blog
37
- post}[http://dev.netizer.pl/transitions-state-machine-for-rails-3.html]
38
- about using Transitions in ActiveRecord.
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
- before_validation :set_initial_state
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
@@ -27,7 +27,7 @@ require "transitions/state_transition"
27
27
  require "transitions/version"
28
28
 
29
29
  module Transitions
30
- class InvalidTransition < Exception
30
+ class InvalidTransition < StandardError
31
31
 
32
32
  end
33
33
 
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Transitions
2
- VERSION = "0.0.9"
2
+ VERSION = "0.0.10"
3
3
  end
@@ -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) { |t| t.string :state }
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: false
5
- segments:
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: 2010-09-21 00:00:00 +02:00
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: -568409294339076591
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.3.7
131
+ rubygems_version: 1.6.2
149
132
  signing_key:
150
133
  specification_version: 3
151
134
  summary: State machine extracted from ActiveModel