simple_state_machine 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,28 +5,55 @@ A simple DSL to decorate existing methods with state transition guards.
5
5
  Instead of using a DSL to define events, SimpleStateMachine decorates methods
6
6
  to help you encapsulate state and guard state transitions.
7
7
 
8
- == Example usage
8
+ It supports exception rescuing, google chart visualization and mountable state_machines.
9
9
 
10
- Write a method, arguments are allowed:
10
+ == Basic example
11
+
12
+ class LampSwitch
13
+
14
+ extend SimpleStateMachine
15
+
16
+ def initialize
17
+ self.state = 'off'
18
+ end
19
+
20
+ def push_switch
21
+ puts "pushed switch"
22
+ end
23
+ event :push_switch, :off => :on,
24
+ :on => :off
25
+
26
+ end
27
+
28
+ lamp = LampSwitch.new
29
+ lamp.state # => 'off'
30
+ lamp.off? # => true
31
+ lamp.push_switch # => 'pushed switch'
32
+ lamp.state # => 'on'
33
+ lamp.on? # => true
34
+ lamp.push_switch # => 'pushed switch'
35
+ lamp.off? # => true
36
+
37
+
38
+ == Basic usage
39
+
40
+ Define your event as a method, arguments are allowed:
11
41
 
12
42
  def activate_account(activation_code)
13
43
  # call other methods, no need to add these in callbacks
14
- ...
15
- log.debug "Try to activate account with #{activation_code}"
44
+ ..
16
45
  end
17
46
 
18
47
  Now mark the method as an event and specify how the state should transition
19
- when the method is called. In this example, the activate_account method will
20
- set the state to :active if the initial state is :pending.
48
+ when the method is called. If we want the state to change from :pending to :active we write:
21
49
 
22
50
  event :activate_account, :pending => :active
23
51
 
52
+ That's it!
53
+ You can now call activate_account and the state will automatically change.
54
+ If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is raised.
24
55
 
25
- That's it!
26
- You can now call the method and the state will automatically change.
27
- If the state change is not allowed, a SimpleStateMachine::Error is raised.
28
-
29
- === Example usage with ActiveRecord / ActiveModel
56
+ === Using ActiveRecord / ActiveModel validations
30
57
  When using ActiveRecord / ActiveModel you can add an error to the errors object.
31
58
  This will prevent the state from being changed.
32
59
 
@@ -36,7 +63,9 @@ This will prevent the state from being changed.
36
63
  end
37
64
  end
38
65
 
39
- === Example usage with exceptions
66
+ activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
67
+
68
+ === Catching exceptions
40
69
  You can rescue exceptions and specify the failure state
41
70
 
42
71
  def download_data
@@ -45,40 +74,13 @@ You can rescue exceptions and specify the failure state
45
74
  event :download_data, :pending => :downloaded,
46
75
  Service::ConnectionError => :download_failed
47
76
 
48
- == More complete implementation
49
-
50
- To add a state machine:
51
- - extend SimpleStateMachine
52
- - set the initial state
53
- - turn methods into events
54
-
55
- class LampWithHotelSwitch
56
-
57
- extend SimpleStateMachine
58
-
59
- def initialize
60
- self.state = 'off'
61
- end
62
-
63
- def push_switch_1
64
- puts "pushed switch 1 #{state}"
65
- end
66
- event :push_switch_1, :off => :on,
67
- :on => :off
77
+ download_data! # catches Service::ConnectionError
78
+ state # => "download_failed"
68
79
 
69
- # define another event
70
- # note that implementation of :push_switch_2 is optional
71
- event :push_switch_2, :off => :on,
72
- :on => :off
80
+ === Catching all from states
81
+ If an event should transition from all states you can use :all
73
82
 
74
- end
75
-
76
- lamp = LampWithHotelSwitch.new
77
- lamp.state # => 'off'
78
- lamp.push_switch_1
79
- lamp.state # => 'on'
80
- lamp.push_siwtch_2
81
- lamp.state # => 'off'
83
+ event :suspend, :all => :suspended
82
84
 
83
85
  == ActiveRecord Example
84
86
 
@@ -92,17 +94,17 @@ To add a state machine with ActiveRecord persistence:
92
94
  extend SimpleStateMachine::ActiveRecord
93
95
 
94
96
  def after_initialize
95
- self.ssm_state ||= 'new'
97
+ self.ssm_state ||= 'pending'
96
98
  # if you get an ActiveRecord::MissingAttributeError
97
99
  # you'll probably need to do (http://bit.ly/35q23b):
98
- # write_attribute(:ssm_state, "new") unless read_attribute(:ssm_state)
100
+ # write_attribute(:ssm_state, "pending") unless read_attribute(:ssm_state)
99
101
  end
100
102
 
101
103
  def invite
102
104
  self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
103
105
  #send_activation_email
104
106
  end
105
- event :invite, :new => :invited
107
+ event :invite, :pending => :invited
106
108
 
107
109
  def confirm_invitation activation_code
108
110
  if self.activation_code != activation_code
@@ -110,26 +112,49 @@ To add a state machine with ActiveRecord persistence:
110
112
  end
111
113
  end
112
114
  event :confirm_invitation, :invited => :active
113
-
114
- # :all can be used to catch all from states
115
- event :suspend, :all => :suspended
116
115
  end
117
116
 
118
117
  This generates the following event methods
119
118
  - invite!
120
119
  - confirm_invitation!
121
- - suspend!
120
+
122
121
  And the following methods to query the state:
122
+ - pending?
123
123
  - invited?
124
124
  - active?
125
- - suspended?
126
125
 
127
126
  If you want to be more verbose you can also use:
128
127
  - invite_and_save (behave like ActiveRecord save)
129
128
  - invite_and_save! (is equal to invite! and behaves like save!)
130
129
 
130
+ == Mountable Example
131
+
132
+ You can define your state machine in a seperate class:
133
+
134
+ class MyStateMachine < SimpleStateMachine::StateMachineDefinition
135
+ def initialize(subject)
136
+ self.lazy_decorator = lambda { SimpleStateMachine::Decorator.new(subject) }
137
+ add_transition(:invite, :new, :invited)
138
+ add_transition(:confirm_invitation, :invited, :active)
139
+ end
140
+ end
141
+
142
+ class User < ActiveRecord::Base
143
+
144
+ extend SimpleStateMachine::Mountable
145
+ self.state_machine_definition = MyStateMachine.new self
146
+
147
+ def after_initialize
148
+ self.ssm_state ||= 'new'
149
+ end
150
+
151
+ end
152
+
153
+
154
+ == Generating google chart visualizations
155
+
156
+ If your using rails you get rake tasks for generating a graphiz google chart of the state machine.
131
157
 
132
- This code was just released, we do not claim it to be stable.
133
158
 
134
159
  == Note on Patches/Pull Requests
135
160
 
@@ -17,7 +17,7 @@ module SimpleStateMachine::ActiveRecord
17
17
  unless @subject.method_defined?(event_name_and_save)
18
18
  @subject.send(:define_method, event_name_and_save) do |*args|
19
19
  old_state = self.send(self.class.state_machine_definition.state_method)
20
- send event_name, *args
20
+ send "with_managed_state_#{event_name}", *args
21
21
  if !self.errors.entries.empty?
22
22
  self.send("#{self.class.state_machine_definition.state_method}=", old_state)
23
23
  return false
@@ -35,7 +35,7 @@ module SimpleStateMachine::ActiveRecord
35
35
  unless @subject.method_defined?(event_name_and_save_bang)
36
36
  @subject.send(:define_method, event_name_and_save_bang) do |*args|
37
37
  old_state = self.send(self.class.state_machine_definition.state_method)
38
- send event_name, *args
38
+ send "with_managed_state_#{event_name}", *args
39
39
  if !self.errors.entries.empty?
40
40
  self.send("#{self.class.state_machine_definition.state_method}=", old_state)
41
41
  raise ActiveRecord::RecordInvalid.new(self)
@@ -47,18 +47,26 @@ module SimpleStateMachine::ActiveRecord
47
47
  raise #re raise
48
48
  end
49
49
  end
50
- @subject.send :alias_method, "#{transition.event_name}!", event_name_and_save_bang
50
+ @subject.send :alias_method, "#{transition.event_name}!", event_name_and_save_bang
51
51
  end
52
52
  end
53
-
54
- private
55
53
 
56
- def define_state_setter_method; end
54
+ protected
57
55
 
58
- def define_state_getter_method; end
56
+ def alias_event_methods event_name
57
+ @subject.send(:define_method, "cannot_call_#{event_name}") do |*args|
58
+ raise "You cannot call #{event_name}. Use #{event_name}! instead"
59
+ end
60
+ @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
61
+ @subject.send :alias_method, event_name, "cannot_call_#{event_name}"
62
+ end
63
+
64
+ def define_state_setter_method; end
65
+
66
+ def define_state_getter_method; end
59
67
 
60
68
  end
61
-
69
+
62
70
  def state_machine_definition
63
71
  unless @state_machine_definition
64
72
  @state_machine_definition = SimpleStateMachine::StateMachineDefinition.new
@@ -1,7 +1,7 @@
1
1
  module SimpleStateMachine
2
2
 
3
3
  require 'cgi'
4
-
4
+
5
5
  class IllegalStateTransitionError < ::RuntimeError
6
6
  end
7
7
 
@@ -26,10 +26,10 @@ module SimpleStateMachine
26
26
  include Mountable
27
27
 
28
28
  ##
29
- # Adds state machine methods to the extended class
29
+ # Adds state machine methods to the extended class
30
30
  module Extendable
31
31
 
32
- # mark the method as an event and specify how the state should transition
32
+ # mark the method as an event and specify how the state should transition
33
33
  def event event_name, state_transitions
34
34
  state_transitions.each do |froms, to|
35
35
  [froms].flatten.each do |from|
@@ -53,7 +53,7 @@ module SimpleStateMachine
53
53
  end
54
54
  end
55
55
  include Inheritable
56
-
56
+
57
57
  ##
58
58
  # Defines state machine transitions
59
59
  class StateMachineDefinition
@@ -67,18 +67,18 @@ module SimpleStateMachine
67
67
  def transitions
68
68
  @transitions ||= []
69
69
  end
70
-
70
+
71
71
  def add_transition event_name, from, to
72
72
  transition = Transition.new(event_name, from, to)
73
73
  transitions << transition
74
74
  decorator.decorate(transition)
75
75
  end
76
-
76
+
77
77
  def state_method
78
78
  @state_method ||= :state
79
- end
79
+ end
80
80
 
81
- # Human readable format: old_state.event! => new_state
81
+ # Human readable format: old_state.event! => new_state
82
82
  def to_s
83
83
  transitions.map(&:to_s).join("\n")
84
84
  end
@@ -94,7 +94,7 @@ module SimpleStateMachine
94
94
  "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape to_graphiz_dot}}"
95
95
  end
96
96
  end
97
-
97
+
98
98
  ##
99
99
  # Defines the state machine used by the instance
100
100
  class StateMachine
@@ -145,9 +145,9 @@ module SimpleStateMachine
145
145
  illegal_event_callback event_name
146
146
  end
147
147
  end
148
-
148
+
149
149
  private
150
-
150
+
151
151
  def state_machine_definition
152
152
  @subject.class.state_machine_definition
153
153
  end
@@ -164,12 +164,12 @@ module SimpleStateMachine
164
164
  def illegal_event_callback event_name
165
165
  raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
166
166
  end
167
-
167
+
168
168
  end
169
169
 
170
170
  ##
171
171
  # Defines transitions for events
172
- class Transition
172
+ class Transition
173
173
  attr_reader :event_name, :from, :to
174
174
  def initialize(event_name, from, to)
175
175
  @event_name = event_name.to_s
@@ -194,7 +194,7 @@ module SimpleStateMachine
194
194
  def to_graphiz_dot
195
195
  %("#{from}"->"#{to}"[label=#{event_name}])
196
196
  end
197
-
197
+
198
198
 
199
199
  private
200
200
 
@@ -256,8 +256,7 @@ module SimpleStateMachine
256
256
  send("without_managed_state_#{event_name}", *args)
257
257
  end
258
258
  end
259
- @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
260
- @subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
259
+ alias_event_methods event_name
261
260
  end
262
261
  end
263
262
 
@@ -274,7 +273,7 @@ module SimpleStateMachine
274
273
  @subject.send(:attr_reader, state_method)
275
274
  end
276
275
  end
277
-
276
+
278
277
  def any_method_defined?(method)
279
278
  @subject.method_defined?(method) ||
280
279
  @subject.protected_method_defined?(method) ||
@@ -283,6 +282,11 @@ module SimpleStateMachine
283
282
 
284
283
  protected
285
284
 
285
+ def alias_event_methods event_name
286
+ @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
287
+ @subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
288
+ end
289
+
286
290
  def state_method
287
291
  @subject.state_machine_definition.state_method
288
292
  end
@@ -1,3 +1,3 @@
1
1
  module SimpleStateMachine
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.2"
3
3
  end
@@ -12,7 +12,7 @@ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
12
12
  :database => ":memory:")
13
13
 
14
14
  def setup_db
15
- ActiveRecord::Schema.define(:version => 1) do
15
+ ActiveRecord::Schema.define(:version => 1) do
16
16
  create_table :users do |t|
17
17
  t.column :id, :integer
18
18
  t.column :name, :string
@@ -38,7 +38,7 @@ class Ticket < ActiveRecord::Base
38
38
  extend SimpleStateMachine::ActiveRecord
39
39
 
40
40
  state_machine_definition.state_method = :ssm_state
41
-
41
+
42
42
  def after_initialize
43
43
  self.ssm_state ||= 'open'
44
44
  end
@@ -47,21 +47,21 @@ class Ticket < ActiveRecord::Base
47
47
  end
48
48
 
49
49
  describe ActiveRecord do
50
-
50
+
51
51
  before do
52
52
  setup_db
53
53
  end
54
-
54
+
55
55
  after do
56
56
  teardown_db
57
57
  end
58
-
58
+
59
59
  it "has a default state" do
60
60
  User.new.should be_new
61
61
  end
62
-
62
+
63
63
  # TODO needs nesting/grouping, seems to have some duplication
64
-
64
+
65
65
  describe "event_and_save" do
66
66
  it "persists transitions" do
67
67
  user = User.create!(:name => 'name')
@@ -88,9 +88,9 @@ describe ActiveRecord do
88
88
 
89
89
  it "raises an error if an invalid state_transition is called" do
90
90
  user = User.create!(:name => 'name')
91
- expect {
92
- user.confirm_invitation_and_save 'abc'
93
- }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
91
+ expect {
92
+ user.confirm_invitation_and_save 'abc'
93
+ }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
94
94
  "You cannot 'confirm_invitation' when state is 'new'")
95
95
  end
96
96
 
@@ -133,9 +133,9 @@ describe ActiveRecord do
133
133
 
134
134
  it "raises an error if an invalid state_transition is called" do
135
135
  user = User.create!(:name => 'name')
136
- expect {
137
- user.confirm_invitation_and_save! 'abc'
138
- }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
136
+ expect {
137
+ user.confirm_invitation_and_save! 'abc'
138
+ }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
139
139
  "You cannot 'confirm_invitation' when state is 'new'")
140
140
  end
141
141
 
@@ -143,9 +143,9 @@ describe ActiveRecord do
143
143
  user = User.new
144
144
  user.should be_new
145
145
  user.should_not be_valid
146
- expect {
147
- user.invite_and_save!
148
- }.to raise_error(ActiveRecord::RecordInvalid,
146
+ expect {
147
+ user.invite_and_save!
148
+ }.to raise_error(ActiveRecord::RecordInvalid,
149
149
  "Validation failed: Name can't be blank")
150
150
  user.should be_new
151
151
  end
@@ -154,9 +154,9 @@ describe ActiveRecord do
154
154
  user = User.create!(:name => 'name')
155
155
  user.invite_and_save!
156
156
  user.should be_invited
157
- expect {
158
- user.confirm_invitation_and_save!('x')
159
- }.to raise_error(ActiveRecord::RecordInvalid,
157
+ expect {
158
+ user.confirm_invitation_and_save!('x')
159
+ }.to raise_error(ActiveRecord::RecordInvalid,
160
160
  "Validation failed: Activation code is invalid")
161
161
  user.should be_invited
162
162
  end
@@ -165,19 +165,9 @@ describe ActiveRecord do
165
165
 
166
166
  describe "event" do
167
167
 
168
- it "does not persist transitions" do
168
+ it "raises an error" do
169
169
  user = User.create!(:name => 'name')
170
- user.invite.should == true
171
- User.find(user.id).should_not be_invited
172
- User.find(user.id).activation_code.should be_nil
173
- end
174
-
175
- it "returns false and keeps state if record is invalid" do
176
- user = User.new
177
- user.should be_new
178
- user.should_not be_valid
179
- user.invite.should == false
180
- user.should be_new
170
+ expect { user.invite }.to raise_error(RuntimeError, "You cannot call invite. Use invite! instead")
181
171
  end
182
172
 
183
173
  end
@@ -202,11 +192,11 @@ describe ActiveRecord do
202
192
  end
203
193
 
204
194
  describe 'custom state method' do
205
-
195
+
206
196
  it "persists transitions" do
207
197
  ticket = Ticket.create!
208
198
  ticket.should be_open
209
- ticket.close.should == true
199
+ ticket.close!.should == true
210
200
  ticket.should be_closed
211
201
  end
212
202
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- hash: 9
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 5
9
- - 1
10
- version: 0.5.1
9
+ - 2
10
+ version: 0.5.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Marek de Heus