simple_state_machine 0.5.3 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
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