simple_state_machine 0.5.3 → 0.6.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.rdoc +23 -0
  3. data/README.rdoc +154 -100
  4. data/lib/simple_state_machine/active_record.rb +2 -62
  5. data/lib/simple_state_machine/decorator/active_record.rb +68 -0
  6. data/lib/simple_state_machine/decorator/default.rb +91 -0
  7. data/lib/simple_state_machine/railtie.rb +1 -1
  8. data/lib/simple_state_machine/simple_state_machine.rb +8 -251
  9. data/lib/simple_state_machine/state_machine.rb +88 -0
  10. data/lib/simple_state_machine/state_machine_definition.rb +72 -0
  11. data/lib/simple_state_machine/tools/graphviz.rb +21 -0
  12. data/lib/simple_state_machine/tools/inspector.rb +44 -0
  13. data/lib/simple_state_machine/transition.rb +40 -0
  14. data/lib/simple_state_machine/version.rb +1 -1
  15. data/lib/simple_state_machine.rb +13 -3
  16. data/lib/tasks/graphviz.rake +31 -0
  17. metadata +37 -150
  18. data/.gitignore +0 -1
  19. data/.rspec +0 -3
  20. data/Gemfile +0 -4
  21. data/Rakefile +0 -28
  22. data/autotest/discover.rb +0 -1
  23. data/examples/conversation.rb +0 -33
  24. data/examples/lamp.rb +0 -21
  25. data/examples/relationship.rb +0 -87
  26. data/examples/traffic_light.rb +0 -17
  27. data/examples/user.rb +0 -37
  28. data/lib/tasks/graphiz.rake +0 -13
  29. data/rails/graphiz.rake +0 -16
  30. data/simple_state_machine.gemspec +0 -31
  31. data/spec/active_record_spec.rb +0 -223
  32. data/spec/decorator_spec.rb +0 -195
  33. data/spec/examples_spec.rb +0 -60
  34. data/spec/mountable_spec.rb +0 -24
  35. data/spec/simple_state_machine_spec.rb +0 -129
  36. data/spec/spec_helper.rb +0 -7
  37. data/spec/state_machine_definition_spec.rb +0 -89
  38. data/spec/state_machine_spec.rb +0 -26
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 03d0976fcf4c2dccd328f82cb00f8b5050f2cc052b578ae98d0227aca37afb5b
4
+ data.tar.gz: 193ae0211b9f82a386317032a2fe3ee245d263801cfab69981c3c6d9cfbe2053
5
+ SHA512:
6
+ metadata.gz: 4e31fd218c64886a4e5fba9334d4b5b71c9bdcbc1fd3216df488a3edc25d6d9a846336887c700623c84b401fc89a6a4afdaa02a0a7ad0d7e3e9456c6bcfdba2e
7
+ data.tar.gz: 4659c64c0b4422967418ca9289ce97e259defd2c4bcbd621e51e37422b2d036b2c8bede71b8b59c97a81137aa076a11403d47c6de2231314272b611b0d7cc08a
data/Changelog.rdoc ADDED
@@ -0,0 +1,23 @@
1
+ == 0.6.1
2
+ - Run against latest rubies and activerecord
3
+ - Remove old rubygems version requirement
4
+ - Remove some trailing whitespace [Petrik]
5
+ - Use `expect` instead if old `should` notation [Petrik]
6
+ - Drop support for Ruby 1.9 [Petrik]
7
+
8
+ == 0.6.0
9
+ === Enhancements
10
+ - Define decorated methods as #{event_name}_with_managed_state instead of with_managed_state_#{event_name} [Petrik]
11
+ - Added raised_error method to access caught Errors [Petrik]
12
+ - Added support for a default error state [Marek de Heus]
13
+ - Removed transaction support, you can still add it manually, see active_record_spec.rb [Marek de Heus]
14
+ - Allow catching errors for subclasses of the error [Petrik]
15
+ - Added gem install instructions [Marek de Heus]
16
+ - Added define_event method to simplify mountable state machine definitions [Petrik]
17
+ - Added end_states method so we can check the state machine correctness [Petrik]
18
+ - Added begin_states method so we can check the state machine correctness [Petrik]
19
+
20
+ === Bugfixes
21
+ - Fixed bug in Inspector, from state can now be a Error class [Marek de Heus]
22
+ - Make sure from and end states are uniq [Petrik]
23
+ - fixed typos (graphiz -> graphviz) (redmar-master) [rjk]
data/README.rdoc CHANGED
@@ -1,43 +1,29 @@
1
1
  = SimpleStateMachine
2
2
 
3
+ {<img src="https://github.com/mdh/ssm/actions/workflows/build.yml/badge.svg" />}[https://github.com/mdh/ssm/actions?query=workflow%3A.github%2Fworkflows%2Fbuild.yml+branch%3Amaster++]
4
+
3
5
  A simple DSL to decorate existing methods with state transition guards.
4
6
 
5
7
  Instead of using a DSL to define events, SimpleStateMachine decorates methods
6
8
  to help you encapsulate state and guard state transitions.
7
9
 
8
- It supports exception rescuing, google chart visualization and mountable state_machines.
9
-
10
- == Basic example
11
-
12
- class LampSwitch
13
-
14
- extend SimpleStateMachine
10
+ It supports exception rescuing, google chart visualization and mountable state machines.
15
11
 
16
- def initialize
17
- self.state = 'off'
18
- end
12
+ == Usage
19
13
 
20
- def push_switch
21
- puts "pushed switch"
22
- end
23
- event :push_switch, :off => :on,
24
- :on => :off
14
+ Define an event and specify how the state should transition. If we want the state to change
15
+ from *pending* to *active* we write:
25
16
 
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
17
+ event :activate_account, :pending => :active
36
18
 
19
+ That's it. You can now call *activate_account* and the state will automatically change.
20
+ If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is
21
+ raised.
37
22
 
38
- == Basic usage
23
+ === Methods with arguments
39
24
 
40
- Define your event as a method, arguments are allowed:
25
+ If you want to pass arguments and call other methods before the state transition, define your
26
+ event as a method.
41
27
 
42
28
  def activate_account(activation_code)
43
29
  # call other methods, no need to add these in callbacks
@@ -45,120 +31,187 @@ Define your event as a method, arguments are allowed:
45
31
  end
46
32
 
47
33
  Now mark the method as an event and specify how the state should transition
48
- when the method is called. If we want the state to change from :pending to :active we write:
34
+ when the method is called.
49
35
 
50
36
  event :activate_account, :pending => :active
51
37
 
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.
55
38
 
56
- === Using ActiveRecord / ActiveModel validations
57
- When using ActiveRecord / ActiveModel you can add an error to the errors object.
58
- This will prevent the state from being changed.
59
-
60
- def activate_account(activation_code)
61
- if activation_code_invalid?(activation_code)
62
- errors.add(:activation_code, 'Invalid')
63
- end
64
- end
65
39
 
66
- activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
40
+ == Basic example
67
41
 
68
- === Catching exceptions
69
- You can rescue exceptions and specify the failure state
42
+ class LampSwitch
43
+
44
+ extend SimpleStateMachine
45
+
46
+ def initialize
47
+ self.state = 'off'
48
+ end
49
+
50
+ event :push_switch, :off => :on,
51
+ :on => :off
70
52
 
71
- def download_data
72
- Service.download_data
73
53
  end
74
- event :download_data, :pending => :downloaded,
75
- Service::ConnectionError => :download_failed
76
54
 
77
- download_data # catches Service::ConnectionError
78
- state # => "download_failed"
55
+ lamp = LampSwitch.new
56
+ lamp.state # => 'off'
57
+ lamp.off? # => true
58
+ lamp.push_switch #
59
+ lamp.state # => 'on'
60
+ lamp.on? # => true
61
+ lamp.push_switch #
62
+ lamp.off? # => true
79
63
 
80
- === Catching all from states
81
- If an event should transition from all states you can use :all
82
64
 
83
- event :suspend, :all => :suspended
65
+ == ActiveRecord
84
66
 
85
- == ActiveRecord Example
67
+ For ActiveRecord methods are decorated with state transition guards _and_ persistence.
68
+ Methods marked as events behave like ActiveRecord *save* and *save!*.
86
69
 
87
- To add a state machine with ActiveRecord persistence:
88
- - extend SimpleStateMachine::ActiveRecord,
89
- - set the initial state in after_initialize,
90
- - turn methods into events
70
+ === Example
71
+
72
+ To add a state machine to an ActiveRecord class, you will have to:
73
+ * extend SimpleStateMachine::ActiveRecord,
74
+ * set the initial state in after_initialize,
75
+ * turn methods into events
91
76
 
92
77
  class User < ActiveRecord::Base
93
-
78
+
94
79
  extend SimpleStateMachine::ActiveRecord
95
80
 
96
- def after_initialize
97
- self.ssm_state ||= 'pending'
98
- # if you get an ActiveRecord::MissingAttributeError
99
- # you'll probably need to do (http://bit.ly/35q23b):
100
- # write_attribute(:ssm_state, "pending") unless read_attribute(:ssm_state)
81
+ after_initialize do
82
+ self.state ||= 'pending'
101
83
  end
102
-
84
+
103
85
  def invite
104
86
  self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
105
- #send_activation_email
106
87
  end
107
88
  event :invite, :pending => :invited
108
-
109
- def confirm_invitation activation_code
110
- if self.activation_code != activation_code
111
- errors.add 'activation_code', 'is invalid'
112
- end
113
- end
114
- event :confirm_invitation, :invited => :active
115
89
  end
116
90
 
117
- This generates the following event methods
118
- - invite
119
- - invite!
120
- - confirm_invitation!
91
+ user = User.new
92
+ user.pending? # => true
93
+ user.invite # => true
94
+ user.invited? # => true
95
+ user.activation_code # => 'SOMEDIGEST'
121
96
 
122
- And the following methods to query the state:
123
- - pending?
124
- - invited?
125
- - active?
97
+ For the invite method this generates the following event methods
98
+ * *invite* (behaves like ActiveRecord save )
99
+ * *invite!* (behaves like ActiveRecord save!)
126
100
 
127
101
  If you want to be more verbose you can also use:
128
- - invite_and_save (is equal to invite and behaves like ActiveRecord save)
129
- - invite_and_save! (is equal to invite! and behaves like save!)
102
+ * *invite_and_save* (alias for invite)
103
+ * *invite_and_save!* (alias for invite!)
130
104
 
131
- == Mountable Example
132
105
 
133
- You can define your state machine in a seperate class:
106
+ === Using ActiveRecord / ActiveModel validations
107
+
108
+ When using ActiveRecord / ActiveModel you can add an error to the errors object.
109
+ This will prevent the state from being changed.
110
+
111
+ If we add an activate_account method to User
134
112
 
135
- class MyStateMachine < SimpleStateMachine::StateMachineDefinition
136
- def initialize(subject)
137
- self.lazy_decorator = lambda { SimpleStateMachine::Decorator.new(subject) }
138
- add_transition(:invite, :new, :invited)
139
- add_transition(:confirm_invitation, :invited, :active)
113
+ class User < ActiveRecord::Base
114
+ ...
115
+ def activate_account(activation_code)
116
+ if activation_code_invalid?(activation_code)
117
+ errors.add(:activation_code, 'Invalid')
118
+ end
140
119
  end
120
+ event :activate_account, :invited => :confirmed
121
+ ...
141
122
  end
142
123
 
143
- class User < ActiveRecord::Base
144
-
145
- extend SimpleStateMachine::Mountable
146
- self.state_machine_definition = MyStateMachine.new self
124
+ user.confirm_invitation!('INVALID') # => raises ActiveRecord::RecordInvalid,
125
+ # "Validation failed: Activation code is invalid"
126
+ user.confirmed? # => false
127
+ user.confirm_invitation!('VALID')
128
+ user.confirmed? # => true
147
129
 
148
- def after_initialize
149
- self.ssm_state ||= 'new'
150
- end
151
-
130
+ == Mountable StateMachines
131
+
132
+ If you like to separate your state machine from your model class, you can do so as following:
133
+
134
+ class MyStateMachine < SimpleStateMachine::StateMachineDefinition
135
+
136
+ event :invite, :new => :invited
137
+ event :confirm_invitation, :invited => :active
138
+
139
+ def decorator_class
140
+ SimpleStateMachine::Decorator::Default
141
+ end
142
+ end
143
+
144
+ class User < ActiveRecord::Base
145
+
146
+ extend SimpleStateMachine::Mountable
147
+ mount_state_machine MyStateMachine
148
+
149
+ after_initialize do
150
+ self.state ||= 'new'
152
151
  end
153
152
 
153
+ end
154
+
154
155
 
155
- == Generating google chart visualizations
156
+ == Transitions
156
157
 
157
- If your using rails you get rake tasks for generating a graphiz google chart of the state machine.
158
+ === Catching all from states
159
+ If an event should transition from all other defined states, you can use the *:all* state:
160
+
161
+ event :suspend, :all => :suspended
162
+
163
+
164
+ === Catching exceptions
165
+
166
+ You can let the state machine handle exceptions by specifying the failure state for an Error:
167
+
168
+ def download_data
169
+ raise Service::ConnectionError, "Uhoh"
170
+ end
171
+ event :download_data, Service::ConnectionError => :download_failed
158
172
 
173
+ download_data # catches Service::ConnectionError
174
+ state # => "download_failed"
175
+ state_machine.raised_error # "Uhoh"
176
+
177
+
178
+ === Default error state
179
+
180
+ To automatically catch all exceptions to a default error state use default_error_state:
181
+
182
+ state_machine_definition.default_error_state = :failed
183
+
184
+ == Transactions
185
+
186
+ If you want to run events in transactions run them in a transaction block:
187
+
188
+ user.transaction { user.invite! }
189
+
190
+ == Tools
191
+
192
+ ===Generating state diagrams
193
+
194
+ When using Rails/ActiveRecord you can generate a state diagram of the state machine via the
195
+ built in rake tasks.
196
+ For details run:
197
+
198
+ rake -T ssm
199
+
200
+ A Googlechart example:
201
+ http://tinyurl.com/79xztr6
202
+
203
+ == Installation
204
+
205
+ Use gem install:
206
+
207
+ gem install simple_state_machine
208
+
209
+ Or add it to your Gemfile:
210
+
211
+ gem 'simple_state_machine'
159
212
 
160
213
  == Note on Patches/Pull Requests
161
-
214
+
162
215
  * Fork the project.
163
216
  * Make your feature addition or bug fix.
164
217
  * Add tests for it. This is important so I don't break it in a
@@ -167,6 +220,7 @@ If your using rails you get rake tasks for generating a graphiz google chart of
167
220
  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
168
221
  * Send me a pull request. Bonus points for topic branches.
169
222
 
223
+
170
224
  == Copyright
171
225
 
172
226
  Copyright (c) 2010 Marek & Petrik. See LICENSE for details.
@@ -4,71 +4,11 @@ module SimpleStateMachine::ActiveRecord
4
4
  include SimpleStateMachine::Extendable
5
5
  include SimpleStateMachine::Inheritable
6
6
 
7
- class Decorator < SimpleStateMachine::Decorator
8
-
9
- # decorates subject with:
10
- # * {event_name}_and_save
11
- # * {event_name}_and_save!
12
- # * {event_name}!
13
- # * {event_name}
14
- def decorate transition
15
- super transition
16
- event_name = transition.event_name.to_s
17
- event_name_and_save = "#{event_name}_and_save"
18
- unless @subject.method_defined?(event_name_and_save)
19
- @subject.send(:define_method, event_name_and_save) do |*args|
20
- old_state = self.send(self.class.state_machine_definition.state_method)
21
- send "with_managed_state_#{event_name}", *args
22
- if !self.errors.entries.empty?
23
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
24
- return false
25
- else
26
- if save
27
- return true
28
- else
29
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
30
- return false
31
- end
32
- end
33
- end
34
- @subject.send :alias_method, "#{transition.event_name}", event_name_and_save
35
- end
36
- event_name_and_save_bang = "#{event_name_and_save}!"
37
- unless @subject.method_defined?(event_name_and_save_bang)
38
- @subject.send(:define_method, event_name_and_save_bang) do |*args|
39
- old_state = self.send(self.class.state_machine_definition.state_method)
40
- send "with_managed_state_#{event_name}", *args
41
- if !self.errors.entries.empty?
42
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
43
- raise ActiveRecord::RecordInvalid.new(self)
44
- end
45
- begin
46
- save!
47
- rescue ActiveRecord::RecordInvalid
48
- self.send("#{self.class.state_machine_definition.state_method}=", old_state)
49
- raise #re raise
50
- end
51
- end
52
- @subject.send :alias_method, "#{transition.event_name}!", event_name_and_save_bang
53
- end
54
- end
55
-
56
- protected
57
-
58
- def alias_event_methods event_name
59
- @subject.send :alias_method, "without_managed_state_#{event_name}", event_name
60
- end
61
-
62
- def define_state_setter_method; end
63
-
64
- def define_state_getter_method; end
65
-
66
- end
67
-
68
7
  def state_machine_definition
69
8
  unless @state_machine_definition
70
9
  @state_machine_definition = SimpleStateMachine::StateMachineDefinition.new
71
- @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
10
+ @state_machine_definition.decorator_class = SimpleStateMachine::Decorator::ActiveRecord
11
+ @state_machine_definition.subject = self
72
12
  end
73
13
  @state_machine_definition
74
14
  end
@@ -0,0 +1,68 @@
1
+ module SimpleStateMachine
2
+ module Decorator
3
+ class ActiveRecord < SimpleStateMachine::Decorator::Default
4
+
5
+ # decorates subject with:
6
+ # * {event_name}_and_save
7
+ # * {event_name}_and_save!
8
+ # * {event_name}!
9
+ # * {event_name}
10
+ def decorate transition
11
+ super transition
12
+ event_name = transition.event_name.to_s
13
+ decorate_save event_name
14
+ decorate_save! event_name
15
+ end
16
+
17
+ protected
18
+
19
+ def decorate_save event_name
20
+ event_name_and_save = "#{event_name}_and_save"
21
+ unless @subject.method_defined?(event_name_and_save)
22
+ @subject.send(:define_method, event_name_and_save) do |*args|
23
+ old_state = self.send(self.class.state_machine_definition.state_method)
24
+ send "#{event_name}_with_managed_state", *args
25
+ if self.errors.entries.empty? && save
26
+ true
27
+ else
28
+ self.send("#{self.class.state_machine_definition.state_method}=", old_state)
29
+ false
30
+ end
31
+ end
32
+ @subject.send :alias_method, "#{event_name}", event_name_and_save
33
+ end
34
+ end
35
+
36
+ def decorate_save! event_name
37
+ event_name_and_save_bang = "#{event_name}_and_save!"
38
+ unless @subject.method_defined?(event_name_and_save_bang)
39
+ @subject.send(:define_method, event_name_and_save_bang) do |*args|
40
+ old_state = self.send(self.class.state_machine_definition.state_method)
41
+ send "#{event_name}_with_managed_state", *args
42
+ if self.errors.entries.empty?
43
+ begin
44
+ save!
45
+ rescue ::ActiveRecord::RecordInvalid
46
+ self.send("#{self.class.state_machine_definition.state_method}=", old_state)
47
+ raise #re raise
48
+ end
49
+ else
50
+ self.send("#{self.class.state_machine_definition.state_method}=", old_state)
51
+ raise ::ActiveRecord::RecordInvalid.new(self)
52
+ end
53
+ end
54
+ @subject.send :alias_method, "#{event_name}!", event_name_and_save_bang
55
+ end
56
+ end
57
+
58
+ def alias_event_methods event_name
59
+ @subject.send :alias_method, "#{event_name}_without_managed_state", event_name
60
+ end
61
+
62
+ def define_state_setter_method; end
63
+
64
+ def define_state_getter_method; end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,91 @@
1
+ module SimpleStateMachine
2
+ module Decorator
3
+ ##
4
+ # Decorates @subject with methods to access the state machine
5
+ class Default
6
+
7
+ attr_writer :subject
8
+ def initialize(subject)
9
+ @subject = subject
10
+ end
11
+
12
+ def decorate transition
13
+ define_state_machine_method
14
+ define_state_getter_method
15
+ define_state_setter_method
16
+
17
+ define_state_helper_method(transition.from)
18
+ define_state_helper_method(transition.to)
19
+ define_event_method(transition.event_name)
20
+ decorate_event_method(transition.event_name)
21
+ end
22
+
23
+ private
24
+
25
+ def define_state_machine_method
26
+ unless any_method_defined?("state_machine")
27
+ @subject.send(:define_method, "state_machine") do
28
+ @state_machine ||= StateMachine.new(self)
29
+ end
30
+ end
31
+ end
32
+
33
+ def define_state_helper_method state
34
+ unless any_method_defined?("#{state.to_s}?")
35
+ @subject.send(:define_method, "#{state.to_s}?") do
36
+ self.send(self.class.state_machine_definition.state_method) == state.to_s
37
+ end
38
+ end
39
+ end
40
+
41
+ def define_event_method event_name
42
+ unless any_method_defined?("#{event_name}")
43
+ @subject.send(:define_method, "#{event_name}") {}
44
+ end
45
+ end
46
+
47
+ def decorate_event_method event_name
48
+ # TODO put in transaction for activeRecord?
49
+ unless @subject.method_defined?("#{event_name}_with_managed_state")
50
+ @subject.send(:define_method, "#{event_name}_with_managed_state") do |*args|
51
+ return state_machine.transition(event_name) do
52
+ send("#{event_name}_without_managed_state", *args)
53
+ end
54
+ end
55
+ alias_event_methods event_name
56
+ end
57
+ end
58
+
59
+ def define_state_setter_method
60
+ unless any_method_defined?("#{state_method}=")
61
+ @subject.send(:define_method, "#{state_method}=") do |new_state|
62
+ instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
63
+ end
64
+ end
65
+ end
66
+
67
+ def define_state_getter_method
68
+ unless any_method_defined?(state_method)
69
+ @subject.send(:attr_reader, state_method)
70
+ end
71
+ end
72
+
73
+ def any_method_defined?(method)
74
+ @subject.method_defined?(method) ||
75
+ @subject.protected_method_defined?(method) ||
76
+ @subject.private_method_defined?(method)
77
+ end
78
+
79
+ protected
80
+
81
+ def alias_event_methods event_name
82
+ @subject.send :alias_method, "#{event_name}_without_managed_state", event_name
83
+ @subject.send :alias_method, event_name, "#{event_name}_with_managed_state"
84
+ end
85
+
86
+ def state_method
87
+ @subject.state_machine_definition.state_method
88
+ end
89
+ end
90
+ end
91
+ end
@@ -4,7 +4,7 @@ require 'rails'
4
4
  module SimpleStateMachine
5
5
  class Railtie < Rails::Railtie
6
6
  rake_tasks do
7
- load "tasks/graphiz.rake"
7
+ load "tasks/graphviz.rake"
8
8
  end
9
9
  end
10
10
  end