ssm 0.1.8

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.
@@ -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
+