spoonsix-ssm 0.1.4

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.
data/lib/event.rb ADDED
@@ -0,0 +1,22 @@
1
+ module SSM
2
+
3
+ class Event
4
+
5
+ attr_reader :name
6
+ attr_reader :block
7
+ attr_reader :transition
8
+
9
+ def initialize(name, transition=nil, &block) #:nodoc:
10
+ @name, @transition = name, transition
11
+ @block = block if block
12
+ end
13
+
14
+ # Compares this Event with another Event and returns true if
15
+ # either the Event is the same or the name of the Event is the same.
16
+ def equal(event)
17
+ self === event || @name === event.name
18
+ end
19
+
20
+ end
21
+
22
+ end
data/lib/ssm.rb ADDED
@@ -0,0 +1,353 @@
1
+ require File.join(File.dirname(__FILE__), 'state_machine')
2
+
3
+ # SSM - Simple State Machine mixin
4
+ #
5
+ # SSM is a mixin that adds finite-state machine behavior to a class.
6
+ #
7
+ # Example usage:
8
+ #
9
+ # class Door
10
+ # include SSM
11
+ #
12
+ # ssm_initial_state :closed
13
+ # ssm_state :opened
14
+ #
15
+ # ssm_event :open, :from => [:closed], :to => :opened do
16
+ # puts "Just opened door"
17
+ # end
18
+ #
19
+ # ssm_event :close, :from => [:opened], :to => :closed do
20
+ # puts "Closed door"
21
+ # end
22
+ #
23
+ # end
24
+ #
25
+ # door = Door.new
26
+ # door.open
27
+ # door.is?(:opened) #=> true
28
+ # door.close
29
+ #--
30
+ # Including SSM ensures that both class-level and instance-level methods are
31
+ # available (through the use of extend on self.included).
32
+ #
33
+ # Each include on a class creates a new StateMachine template for the class it is being
34
+ # included in. The meta methods will then help build that template StateMachine.
35
+ #
36
+ # Each time an instance of the Foo class is created, the template StateMachine is cloned,
37
+ # and attached to the new instance. The cloned StateMachine can not be manipulated at runtime.
38
+ # This ensures that as long as a class is not modified at runtime, all its instances' StateMachines
39
+ # are equivalent.
40
+ #--
41
+ module SSM
42
+
43
+ VERSION = '0.1.2';
44
+
45
+ class InvalidTransition < RuntimeError; end
46
+ class UndefinedState < RuntimeError; end
47
+ class DuplicateState < RuntimeError; end
48
+ class UndefinedEvent < RuntimeError; end
49
+ class DuplicateEvent < RuntimeError; end
50
+ class InitialStateRequired < RuntimeError; end
51
+
52
+ # TemplateStateMachines stores the StateMachine templates for each class
53
+ # that includes SSM. Because all setup is done before instantiation, each
54
+ # instance will then have a consistent StateMachine.
55
+ TemplateStateMachines = {} #:nodoc:
56
+
57
+ # First, extend the class with static methods as soon as the module is mixed in.
58
+ # Then, initialize the StateMachine when the module is mixed in. This allows the meta
59
+ # calls to build the StateMachine before instantiation, and once the model is actually
60
+ # instanciated, store a copy of the StateMachine.
61
+ #
62
+ # Note: We use the actual Class as the key to the TemplateStateMachines hash. If the Class is redeclared,
63
+ # the hash will see it as a new key, even though if you inspect the hash you will see two keys whose string
64
+ # representation is the same.
65
+ def self.included(klass) #:nodoc:
66
+ klass.extend SSM::ClassMethods
67
+ SSM::TemplateStateMachines[klass] = SSM::StateMachine.new
68
+
69
+ def klass.allocate(*args)
70
+ instance = super(*args)
71
+ setup(instance)
72
+ instance
73
+ end
74
+
75
+ # Intercept contructor. We can't overide initialize given that the user
76
+ # may define their own initialize method.
77
+ def klass.new(*args)
78
+ instance = super(*args)
79
+ setup(instance)
80
+ instance
81
+ end
82
+
83
+ def klass.setup(instance)
84
+ SSM::TemplateStateMachines[self].validate
85
+
86
+ sm = SSM::TemplateStateMachines[self].clone_and_freeze
87
+ instance.instance_variable_set(:@ssm_state_machine, sm)
88
+
89
+ unless sm.property_name.nil?
90
+ # This allows others to set up the object however they see fit, including mixing in setters.
91
+ instance.instance_eval("def #{sm.property_name}; @#{sm.property_name}; end") unless instance.respond_to?(sm.property_name)
92
+ instance.instance_eval("def #{sm.property_name}=(v); @#{sm.property_name} = v; end") unless instance.respond_to?("#{sm.property_name}=".to_sym)
93
+
94
+ # Set up default state
95
+ initial_state_value = sm.use_property_index == true ? sm.get_state_index_by_name(sm.initial_state.name) : sm.initial_state.name
96
+ instance.instance_variable_set("@#{sm.property_name}".to_sym, initial_state_value)
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+
103
+ # Class methods to be mixed in when the module is included.
104
+ #
105
+ #--
106
+ # Note: self returns the actual class
107
+ #--
108
+ module ClassMethods
109
+
110
+ attr_accessor :ssm_instance_property
111
+
112
+ def inherited(subclass) #:nodoc:
113
+ raise Exception.new("SSM cannot be inherited. Use include instead.")
114
+ end
115
+
116
+ # This method is used as both a setter - in the context of the class declaration -
117
+ # and a getter when called from an instance.
118
+ #
119
+ # class Door
120
+ # include SSM
121
+ #
122
+ # ssm_initial_state :closed
123
+ # end
124
+ #
125
+ # Door.new.ssm_initial_state #=> :closed
126
+ #
127
+ def ssm_initial_state(name=nil)
128
+ name.nil? ?
129
+ SSM::TemplateStateMachines[self].initial_state :
130
+ SSM::TemplateStateMachines[self].initial_state = SSM::State.new(name)
131
+ end
132
+
133
+ # Sets the instance attribute that stores a representation of the State. In
134
+ # the first form, the property will return a symbol represeting the State.
135
+ # In the second form, an index is returned, making it more convenient when
136
+ # dealing with persistence.
137
+ #
138
+ # class Door
139
+ # include SSM
140
+ #
141
+ # ssm_property :state
142
+ # ssm_initial_state :closed
143
+ # end
144
+ #
145
+ # Door.new.state #=> :closed
146
+ #
147
+ #
148
+ # class Door
149
+ # include SSM
150
+ #
151
+ # ssm_property :state, :use_index
152
+ # ssm_initial_state :closed
153
+ # end
154
+ #
155
+ # Door.new.state #=> 0
156
+ #
157
+ def ssm_property(name, use_index=nil)
158
+ SSM::TemplateStateMachines[self].property_name = name
159
+ SSM::TemplateStateMachines[self].use_property_index = use_index.nil? ? false : true
160
+ end
161
+
162
+
163
+ # Adds new States. This method takes a string or a symbol.
164
+ #
165
+ # class Door
166
+ # include SSM
167
+ #
168
+ # ssm_state :closed
169
+ # ssm_state :opened
170
+ # end
171
+ #
172
+ def ssm_state(name, options={})
173
+ SSM::TemplateStateMachines[self] << SSM::State.new(name)
174
+ end
175
+
176
+ # Adds new Events. These Events can then be called as methods.
177
+ #
178
+ # class Door
179
+ # include SSM
180
+ #
181
+ # ssm_initial_state :closed
182
+ # ssm_state :opened
183
+ #
184
+ # ssm_event :open, :from => [:closed], :to => :opened do
185
+ # puts "Just opened door"
186
+ # end
187
+ #
188
+ # ssm_event :close, :from => [:opened], :to => :closed do
189
+ # puts "Closed door"
190
+ # end
191
+ #
192
+ # end
193
+ #
194
+ # door = Door.new
195
+ # door.open
196
+ # door.is?(:opened) #=> true
197
+ # door.closed
198
+ #
199
+ def ssm_event(name, options = {}, &block)
200
+
201
+ msg = "Please specificy a final state for this transition. Use a lowly instance method if a transition is not required."
202
+ raise SSM::InvalidTransition.new(msg) unless options[:to].is_a?(Symbol)
203
+
204
+ begin
205
+ # build Array of States this transition can be called from
206
+ from = []
207
+ if options[:from].is_a?(Array) and options[:from].size > 0
208
+ options[:from].each { |state_name| from << SSM::TemplateStateMachines[self].get_state_by_name(state_name) }
209
+ end
210
+
211
+ to = SSM::TemplateStateMachines[self].get_state_by_name(options[:to])
212
+ rescue
213
+ raise
214
+ end
215
+
216
+ # Create StateMachine and create method associated with this StateTransition
217
+ SSM::TemplateStateMachines[self] << SSM::Event.new(name, SSM::StateTransition.new(from, to), &block)
218
+ define_method("#{name.to_s}") { |*args| _update_ssm_state unless _state_up_to_date?; _ssm_trigger_event(name, args) }
219
+ end
220
+
221
+ def template_state_machine #:nodoc:
222
+ SSM::TemplateStateMachines[self]
223
+ end
224
+
225
+ # Returns all the available Events
226
+ def ssm_events
227
+ template_state_machine.events
228
+ end
229
+
230
+ # Returns all the available States
231
+ def ssm_states
232
+ template_state_machine.states
233
+ end
234
+
235
+ end
236
+
237
+ #
238
+ # instance methods
239
+ #
240
+ attr_reader :ssm_state_machine
241
+
242
+ # Returns true if the Object is in the State represented by the name or symbol.
243
+ #
244
+ # class Door
245
+ # include SSM
246
+ #
247
+ # ssm_initial_state :closed
248
+ # ssm_state :opened
249
+ #
250
+ # ssm_event :open, :from => [:closed], :to => :opened do
251
+ # puts "Just opened door"
252
+ # end
253
+ #
254
+ # ssm_event :close, :from => [:opened], :to => :closed do
255
+ # puts "Closed door"
256
+ # end
257
+ #
258
+ # end
259
+ #
260
+ # door = Door.new
261
+ # door.open
262
+ # door.is?(:opened) #=> true
263
+ def is?(state_name_or_symbol)
264
+ _update_ssm_state unless _state_up_to_date?
265
+ @ssm_state_machine.current_state.name.to_sym == state_name_or_symbol.to_sym
266
+ end
267
+
268
+ # Returns true if the Object is not in the State represented by the name or symbol.
269
+ #
270
+ # class Door
271
+ # include SSM
272
+ #
273
+ # ssm_initial_state :closed
274
+ # ssm_state :opened
275
+ #
276
+ # ssm_event :open, :from => [:closed], :to => :opened do
277
+ # puts "Just opened door"
278
+ # end
279
+ #
280
+ # ssm_event :close, :from => [:opened], :to => :closed do
281
+ # puts "Closed door"
282
+ # end
283
+ #
284
+ # end
285
+ #
286
+ # door = Door.new
287
+ # door.open
288
+ # door.is?(:closed) #=> false
289
+ def is_not?(state_name_or_symbol)
290
+ _update_ssm_state unless _state_up_to_date?
291
+ @ssm_state_machine.current_state.name.to_sym != state_name_or_symbol.to_sym
292
+ end
293
+
294
+ private
295
+
296
+ def _ssm_trigger_event(event_name_or_symbol, args)
297
+ event = @ssm_state_machine.get_event_by_name(event_name_or_symbol)
298
+
299
+ @ssm_state_machine.transition(event.transition)
300
+ _update_instance_state_property unless @ssm_state_machine.property_name.nil?
301
+ instance_exec *args, &event.block
302
+ end
303
+
304
+ def _update_instance_state_property
305
+ unless @ssm_state_machine.current_state.nil?
306
+ if @ssm_state_machine.use_property_index == true
307
+ value = @ssm_state_machine.get_state_index_by_name(@ssm_state_machine.current_state.name)
308
+ else
309
+ value = @ssm_state_machine.current_state.name
310
+ end
311
+ self.send("#{@ssm_state_machine.property_name}=".to_sym, value)
312
+ end
313
+ end
314
+
315
+ # instance_exec for 1.8.x
316
+ # http://groups.google.com/group/ruby-talk-google/browse_thread/thread/34bc4c9b2cac3424
317
+ unless instance_methods.include? 'instance_exec' #:nodoc:
318
+ module InstanceExecHelper; end
319
+ include InstanceExecHelper
320
+ def instance_exec(*args, &block)
321
+ mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
322
+ InstanceExecHelper.module_eval{ define_method(mname, &block) }
323
+ begin
324
+ ret = send(mname, *args)
325
+ ensure
326
+ InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
327
+ end
328
+ ret
329
+ end
330
+ end
331
+
332
+ # Checks whether the StateMachine and the property in the instance are in sync
333
+ def _state_up_to_date?
334
+ unless @ssm_state_machine.property_name.nil?
335
+ state_value = self.send("#{@ssm_state_machine.property_name}".to_sym)
336
+ if @ssm_state_machine.use_property_index == true
337
+ state_value == @ssm_state_machine.get_state_index_by_name(@ssm_state_machine.current_state.name)
338
+ else
339
+ state_value == @ssm_state_machine.current_state.name
340
+ end
341
+ else
342
+ true
343
+ end
344
+ end
345
+
346
+ # Updates the StateMachine based on the value of the state property of the instance
347
+ def _update_ssm_state
348
+ unless @ssm_state_machine.property_name.nil?
349
+ state_value = self.send("#{@ssm_state_machine.property_name}".to_sym)
350
+ @ssm_state_machine.current_state = @ssm_state_machine.use_property_index == true ? @ssm_state_machine.get_state_by_index(state_value) : @ssm_state_machine.get_state_by_name(state_value)
351
+ end
352
+ end
353
+ end
data/lib/state.rb ADDED
@@ -0,0 +1,17 @@
1
+ module SSM
2
+
3
+ class State
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(name, options = {})
8
+ @name = name.to_sym
9
+ self.freeze
10
+ end
11
+
12
+ def equal(state)
13
+ self === state || @name === state.name
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,116 @@
1
+ require File.join(File.dirname(__FILE__), 'state')
2
+ require File.join(File.dirname(__FILE__), 'state_transition')
3
+ require File.join(File.dirname(__FILE__), 'event')
4
+
5
+ module SSM
6
+
7
+ class StateMachine
8
+
9
+ attr_reader :initial_state
10
+ attr_accessor :current_state
11
+ attr_accessor :property_name
12
+ attr_accessor :use_property_index
13
+ attr_reader :states
14
+ attr_reader :events
15
+
16
+ def initialize
17
+ @states = []
18
+ @events = []
19
+ end
20
+
21
+ def validate
22
+ raise SSM::InitialStateRequired if @initial_state.nil?
23
+ true
24
+ end
25
+
26
+ # Cloning is sufficent given that instances of SSM::State and SSM::Event are immutable.
27
+ # Furthermore, we freeze the Arrays storing those instances
28
+ def clone_and_freeze
29
+ clone = self.clone
30
+ clone.init
31
+ clone.freeze
32
+ clone
33
+ end
34
+
35
+ def init
36
+ @current_state = @initial_state
37
+ end
38
+
39
+ def freeze
40
+ @states.freeze
41
+ @events.freeze
42
+ self
43
+ end
44
+
45
+ def initial_state=(state)
46
+ self << state
47
+ @initial_state = state
48
+ end
49
+
50
+ def << state_or_event
51
+
52
+ if state_or_event.is_a?(SSM::State)
53
+ push_state(state_or_event)
54
+ elsif state_or_event.is_a?(SSM::Event)
55
+ push_event(state_or_event)
56
+ else
57
+ raise TypeError
58
+ end
59
+ end
60
+
61
+ def state_exists?(state_to_compare)
62
+ @states.find { |existing_state| existing_state.equal(state_to_compare) }
63
+ end
64
+
65
+ def event_exists?(event_to_compare)
66
+ @events.find { |existing_event| existing_event.equal(event_to_compare) }
67
+ end
68
+
69
+ def get_state_by_name(name)
70
+ state = @states.find { |state| state.name == name}
71
+ raise SSM::UndefinedState.new unless state.is_a?(SSM::State)
72
+ state
73
+ end
74
+
75
+ def get_state_index_by_name(name)
76
+ @states.index(get_state_by_name(name))
77
+ end
78
+
79
+ def get_state_by_index(index)
80
+ @states[index.to_i]
81
+ end
82
+
83
+ def get_event_by_name(name)
84
+ event = @events.find { |event| event.name == name}
85
+ raise SSM::UndefinedEvent.new unless event.is_a?(SSM::Event)
86
+ event
87
+ end
88
+
89
+ def transition(transition)
90
+ begin
91
+ transition.validate(@current_state)
92
+ @current_state = transition.to
93
+ rescue SSM::InvalidTransition => e
94
+ raise e
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def push_state(new_state)
101
+ raise DuplicateState.new if state_exists?(new_state)
102
+ @states << new_state
103
+ end
104
+
105
+ def push_event(new_event)
106
+ raise DuplicateEvent.new if event_exists?(new_event)
107
+ @events << new_event
108
+ end
109
+
110
+ end
111
+
112
+
113
+
114
+
115
+
116
+ end
@@ -0,0 +1,20 @@
1
+ module SSM
2
+
3
+ class StateTransition
4
+
5
+ attr_reader :from, :to
6
+
7
+ def initialize(from, to)
8
+ @from, @to = from, to
9
+ end
10
+
11
+ def validate(current_state)
12
+ return true if @to.nil?
13
+ return true if @from.size == 0
14
+ raise SSM::InvalidTransition unless @from.find {|state| state.equal(current_state)}
15
+ true
16
+ end
17
+
18
+ end
19
+
20
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spoonsix-ssm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Luis Correa d'Almeida
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-12 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: simple state machine is a mixin that adds finite-state machine behavior to a class.
17
+ email: luis.ca@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/event.rb
26
+ - lib/ssm.rb
27
+ - lib/state.rb
28
+ - lib/state_machine.rb
29
+ - lib/state_transition.rb
30
+ has_rdoc: false
31
+ homepage: http://github.com/spoonsix/ssm
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: simple state machine is a mixin that adds finite-state machine behavior to a class.
56
+ test_files: []
57
+