spoonsix-ssm 0.1.4

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