simple_state_machine 0.5.1 → 0.5.2

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.
@@ -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