ssm 0.1.8

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