alter-ego 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,4 @@
1
+ == 1.0.0 2008-11-28
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ State_Design_Pattern_UML_Class_Diagram.png
6
+ Rakefile
7
+ TODO
8
+ lib/assertions.rb
9
+ spec/assertions_spec.rb
10
+ lib/alter_ego.rb
11
+ script/console
12
+ script/destroy
13
+ script/generate
14
+ spec/spec.opts
15
+ spec/spec_helper.rb
16
+ spec/alter_ego_spec.rb
17
+ lib/alter_ego.rb
18
+ spec/alter_ego_spec.rb
19
+ tasks/rspec.rake
@@ -0,0 +1,7 @@
1
+
2
+ For more information on states, see http://alter-ego.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
@@ -0,0 +1,149 @@
1
+ = AlterEgo
2
+
3
+ * http://alter-ego.rubyforge.org
4
+
5
+ == DESCRIPTION:
6
+
7
+ AlterEgo is a Ruby implementation of the State pattern as described by the Gang
8
+ of Four. It differs from other Ruby state machine libraries in that it focuses
9
+ on providing polymorphic behavior based on object state. In effect, it makes it
10
+ easy to give an object different personalities depending on the state it is in.
11
+
12
+ == SYNOPSIS:
13
+
14
+ class TrafficLight
15
+ include AlterEgo
16
+
17
+ state :proceed, :default => true do
18
+ handle :color do
19
+ "green"
20
+ end
21
+ transition :to => :caution, :on => :cycle!
22
+ end
23
+
24
+ state :caution do
25
+ handle :color do
26
+ "yellow"
27
+ end
28
+ transition :to => :stop, :on => :cycle!
29
+ end
30
+
31
+ state :stop do
32
+ handle :color do
33
+ "red"
34
+ end
35
+ transition :to => :proceed, :on => :cycle!
36
+ end
37
+ end
38
+
39
+ light = TrafficLight.new
40
+ light.color # => "green"
41
+ light.cycle!
42
+ light.color # => "yellow"
43
+ light.cycle!
44
+ light.color # => "red"
45
+ light.cycle!
46
+ light.color # => "green"
47
+
48
+
49
+ == FEATURES:
50
+
51
+ * Implemented as a module which can be included in any Ruby class.
52
+ * Fully tested with literate RSpec
53
+ * Guard clauses may be defined for each transition.
54
+ * Enter/exit actions may be defined for each state.
55
+ * For more advanced scenarios, arbitrary "request filters" may be
56
+ defined with full control over which requests are filtered.
57
+ * Uses dynamic module generation and delegation instead of method
58
+ rewriting.
59
+ * Pervasive contract-checking catches mistakes in library usage
60
+ early.
61
+ * Storing and reading current state is completely customizable,
62
+ making it easier to add AlterEgo to legacy classes.
63
+
64
+ == DETAILS:
65
+
66
+ AlterEgo differs from other Finite State Machine implementations in
67
+ Ruby in that where other libraries focus on describing a set of
68
+ valid state transitions, AlterEgo focuses on varying _behavior_ based
69
+ on state. In other words, it provides state-based polymorphism.
70
+
71
+ AlterEgo draws heavily on the State Pattern as published in the book
72
+ Design Patterns[1]. A summary of the pattern can be
73
+ found on Wikipedia[http://en.wikipedia.org/wiki/State_pattern].
74
+ Because AlterEgo uses the terminology set forth in the State Pattern,
75
+ it is useful to have some familiarity with the pattern in order to
76
+ understand the library.
77
+
78
+ link://State_Design_Pattern_UML_Class_Diagram.png
79
+
80
+ In the State Pattern, states of an object are represented as
81
+ discrete objects. At any given time an object with state-based
82
+ behavior has-a state object. The object with state-based behavior
83
+ delegates certain method calls to its current state. In this way,
84
+ the implementation of those methods can vary with the state of the
85
+ object. Or in certain states some methods may not be supported at
86
+ all.
87
+
88
+ The AlterEgo library provides both an object model for manually
89
+ setting up explicit state classes, and a concise DSL built on top
90
+ of that model which simplifies building classes with state-based
91
+ behavior.
92
+
93
+ This file only scratches the surface of AlterEgo
94
+ functionality. For complete tutorial documentation, see the file
95
+ spec/alter_ego_spec.rb. It contains the annotated specification,
96
+ written in the style of a step-by-step tutorial.
97
+
98
+ === Terminology:
99
+
100
+ [Context] The *context* is the class which should have state-based
101
+ behavior. In the example above, the +TrafficLight+ class
102
+ is the context.
103
+ [State] Each *state* the *context* might exist in is represented by a
104
+ class, In the example given above, the available states
105
+ are +caution+, +stop+, and +proceed+.
106
+ [Request] A *request* refers to a message (method) sent to the
107
+ *context*. In the example given above, the supported
108
+ requests are +color+ and +cycle+.
109
+ [Handler] A *handler* is a method on a *state* which implements a
110
+ *request* for that state. For instance, in the example
111
+ above, when the +TrafficLight+ is in the +caution+ state,
112
+ the handler for the request +color+ returns "yellow".
113
+
114
+ === Footnotes:
115
+
116
+ 1. Gamma, Erich; Richard Helm, Ralph Johnson, John M. Vlissides (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 395. ISBN 0201633612.
117
+
118
+ == REQUIREMENTS:
119
+
120
+ * ActiveSupport
121
+
122
+ == INSTALL:
123
+
124
+ * sudo gem install alter-ego
125
+
126
+ == LICENSE:
127
+
128
+ (The MIT License)
129
+
130
+ Copyright (c) 2008 Avdi Grimm
131
+
132
+ Permission is hereby granted, free of charge, to any person obtaining
133
+ a copy of this software and associated documentation files (the
134
+ 'Software'), to deal in the Software without restriction, including
135
+ without limitation the rights to use, copy, modify, merge, publish,
136
+ distribute, sublicense, and/or sell copies of the Software, and to
137
+ permit persons to whom the Software is furnished to do so, subject to
138
+ the following conditions:
139
+
140
+ The above copyright notice and this permission notice shall be
141
+ included in all copies or substantial portions of the Software.
142
+
143
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
144
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
145
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
146
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
147
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
148
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
149
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/alter_ego'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('alter-ego', AlterEgo::VERSION) do |p|
7
+ p.developer('Avdi Grimm', 'avdi@avdi.org')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.rubyforge_name = p.name # TODO this is default value
10
+ p.extra_deps = [
11
+ ['activesupport','>= 2.0.2'],
12
+ ]
13
+ p.extra_dev_deps = [
14
+ ['newgem', ">= #{::Newgem::VERSION}"]
15
+ ]
16
+
17
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
18
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
19
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
20
+ p.rsync_args = '-av --delete --ignore-errors'
21
+ end
22
+
23
+ require 'newgem/tasks' # load /tasks/*.rake
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
25
+
26
+ # TODO - want other tests/tasks run by default? Add them to the list
27
+ # task :default => [:spec, :features]
28
+
29
+ task :docs do |task|
30
+ cp "State_Design_Pattern_UML_Class_Diagram.png", "doc"
31
+ end
data/TODO ADDED
@@ -0,0 +1,10 @@
1
+ * TODO Factor assertions out into separate library
2
+ * TODO Remove ActiveSupport dependency
3
+ * TODO Nested state support with history
4
+ * TODO Verify full methods/respond_to? integration
5
+ * TODO Composite state support
6
+ E.g. a Traffic Light could have both green/yellow/red state and "in
7
+ service"/out of service" state at the same time
8
+ * TODO More natural/Rubyish DSL
9
+ It would be good to have plain-old "def" work as expected inside of "state"
10
+ blocks instead of requiring "handle".
@@ -0,0 +1,381 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require File.join(File.dirname(__FILE__), 'assertions')
5
+ require 'forwardable'
6
+ require 'singleton'
7
+ require 'rubygems'
8
+ require 'activesupport'
9
+
10
+ module AlterEgo
11
+ VERSION = '1.0.0'
12
+
13
+ include Assertions
14
+
15
+ class StateError < RuntimeError
16
+ end
17
+ class InvalidDefinitionError < StateError
18
+ end
19
+ class InvalidTransitionError < StateError
20
+ end
21
+ class InvalidRequestError < StateError
22
+ end
23
+ class WrongStateError < StateError
24
+ end
25
+
26
+ RequestFilter = Struct.new("RequestFilter",
27
+ :state,
28
+ :request,
29
+ :new_state,
30
+ :action)
31
+ class RequestFilter
32
+ def ===(other)
33
+ result = (matches?(self.state, other.state) and
34
+ matches?(self.request, other.request) and
35
+ matches?(self.new_state, other.new_state))
36
+ result
37
+ end
38
+
39
+ def matches?(lhs, rhs)
40
+ if rhs.respond_to?(:include?)
41
+ rhs.include?(lhs)
42
+ else
43
+ rhs == lhs
44
+ end
45
+ end
46
+ end
47
+
48
+ class AnyMatcher
49
+ include Singleton
50
+
51
+ def ===(other)
52
+ self == other
53
+ end
54
+
55
+ def ==(other)
56
+ true
57
+ end
58
+ end
59
+
60
+ class NotNilMatcher
61
+ include Singleton
62
+
63
+ def ===(other)
64
+ self == other
65
+ end
66
+
67
+ def ==(other)
68
+ not other.nil?
69
+ end
70
+ end
71
+
72
+ class State
73
+ include Assertions
74
+ extend Assertions
75
+
76
+ def self.transition(options, &trans_action)
77
+ options.assert_valid_keys(:to, :on, :if)
78
+ assert_keys(options, :to)
79
+ guard = options[:if]
80
+ to_state = options[:to]
81
+ request = options[:on]
82
+ if request
83
+ handle(request) do
84
+ transition_to(to_state, request)
85
+ end
86
+ end
87
+ valid_transitions << to_state unless valid_transitions.include?(to_state)
88
+ if guard
89
+ method = guard.kind_of?(Symbol) ? guard : nil
90
+ block = guard.kind_of?(Proc) ? guard : nil
91
+ predicate = AlterEgo.proc_from_symbol_or_block(method, &block)
92
+ guard_proc = proc do
93
+ result = instance_eval(&predicate)
94
+ throw :cancel unless result
95
+ end
96
+ add_request_filter(request, to_state, guard_proc)
97
+ end
98
+ if trans_action
99
+ add_request_filter(request, to_state, trans_action)
100
+ end
101
+ end
102
+
103
+ def self.identifier
104
+ self
105
+ end
106
+
107
+ def self.valid_transitions
108
+ (@valid_transitions ||= [])
109
+ end
110
+
111
+ def self.handled_requests
112
+ public_instance_methods(false)
113
+ end
114
+
115
+ def self.request_filters
116
+ (@request_filters ||= [])
117
+ end
118
+
119
+ def self.handle(request, method = nil, &block)
120
+ define_contextual_method_from_symbol_or_block(request, method, &block)
121
+ end
122
+
123
+ def self.on_enter(method = nil, &block)
124
+ assert(method.nil? ^ block.nil?)
125
+ define_contextual_method_from_symbol_or_block(:on_enter, method, &block)
126
+ end
127
+
128
+ def self.on_exit(method = nil, &block)
129
+ assert(method.nil? ^ block.nil?)
130
+ define_contextual_method_from_symbol_or_block(:on_exit, method, &block)
131
+ end
132
+
133
+ def valid_transitions
134
+ self.class.valid_transitions
135
+ end
136
+
137
+ def to_s
138
+ "<State:#{identifier}>"
139
+ end
140
+
141
+ def identifier
142
+ self.class.identifier
143
+ end
144
+
145
+ def ==(other)
146
+ (self.identifier == other) or super(other)
147
+ end
148
+
149
+ def can_handle_request?(request)
150
+ return true if respond_to?(request)
151
+ return false
152
+ end
153
+
154
+ def transition_to(context, request, new_state, *args)
155
+ return true if context.state == new_state
156
+ new_state_obj = context.state_for_identifier(new_state)
157
+ unless new_state_obj
158
+ raise(InvalidTransitionError,
159
+ "Context #{context.inspect} has no state '#{new_state}' defined")
160
+ end
161
+
162
+ continue = context.execute_request_filters(self.class.identifier,
163
+ request,
164
+ new_state)
165
+ return false unless continue
166
+
167
+ if (not valid_transitions.empty?) and (not valid_transitions.include?(new_state))
168
+ raise(InvalidTransitionError,
169
+ "Not allowed to transition from #{self.identifier} to #{new_state}")
170
+ end
171
+
172
+ on_exit(context)
173
+ new_state_obj.on_enter(context)
174
+ context.state=(new_state)
175
+ assert(new_state == context.state)
176
+ true
177
+ end
178
+
179
+ protected
180
+
181
+ def on_exit(context)
182
+ end
183
+
184
+ def on_enter(context)
185
+ end
186
+
187
+ private
188
+
189
+ def self.add_request_filter(request_pattern, new_state_pattern, action)
190
+ new_filter = RequestFilter.new(identifier,
191
+ request_pattern,
192
+ new_state_pattern,
193
+ action)
194
+ self.request_filters << new_filter
195
+ end
196
+
197
+ def self.define_contextual_method_from_symbol_or_block(name,
198
+ symbol,
199
+ &block)
200
+ if symbol
201
+ define_method(name) do |context, *args|
202
+ context.send(symbol, *args)
203
+ end
204
+ elsif block
205
+ define_method(name) do |context, *args|
206
+ context.send(:instance_eval, &block)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ module ClassMethods
213
+ def state(identifier, options={}, &block)
214
+ if states.has_key?(identifier)
215
+ raise InvalidDefinitionError, "State #{identifier.inspect} already defined"
216
+ end
217
+ new_state = Class.new(State)
218
+ new_state_eigenclass = class << new_state; self; end
219
+ new_state_eigenclass.send(:define_method, :identifier) { identifier }
220
+ new_state.instance_eval(&block) if block
221
+
222
+ add_state(new_state, identifier, options)
223
+ end
224
+
225
+ def request_filter(options, &block)
226
+ options.assert_valid_keys(:state, :request, :new_state, :action)
227
+ options = {
228
+ :state => not_nil,
229
+ :request => not_nil,
230
+ :new_state => nil
231
+ }.merge(options)
232
+ add_request_filter(options[:state],
233
+ options[:request],
234
+ options[:new_state],
235
+ AlterEgo.proc_from_symbol_or_block(options[:method], &block))
236
+ end
237
+
238
+ def all_handled_requests
239
+ methods = @state_proxy.public_instance_methods(false)
240
+ methods -= ["identifier", "on_enter", "on_exit"]
241
+ methods.map{|m| m.to_sym}
242
+ end
243
+
244
+ def states
245
+ (@states ||= {})
246
+ end
247
+
248
+ def states=(value)
249
+ @states = value
250
+ end
251
+
252
+ def add_state(new_state, identifier=new_state.identifier, options = {})
253
+ options.assert_valid_keys(:default)
254
+
255
+ self.states[identifier] = new_state.new
256
+
257
+ if options[:default]
258
+ if @default_state
259
+ raise InvalidDefinitionError, "Cannot have more than one default state"
260
+ end
261
+ @default_state = identifier
262
+ end
263
+
264
+ new_requests = (new_state.handled_requests - all_handled_requests)
265
+ new_requests.each do |request|
266
+ @state_proxy.send(:define_method, request) do |*args|
267
+ args.unshift(self)
268
+ begin
269
+ continue = execute_request_filters(current_state.identifier,
270
+ request,
271
+ nil)
272
+ return false unless continue
273
+ current_state.send(request, *args)
274
+ rescue NoMethodError => error
275
+ if error.name.to_s == request.to_s
276
+ raise WrongStateError,
277
+ "Request '#{request}' not supported by state #{current_state}"
278
+ else
279
+ raise
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ self.request_filters += new_state.request_filters
286
+ end
287
+
288
+ def add_request_filter(state_pattern, request_pattern, new_state_pattern, action)
289
+ @request_filters << RequestFilter.new(state_pattern,
290
+ request_pattern,
291
+ new_state_pattern,
292
+ action)
293
+ end
294
+
295
+ def default_state
296
+ @default_state
297
+ end
298
+
299
+ def request_filters
300
+ (@request_filters ||= [])
301
+ end
302
+
303
+ protected
304
+
305
+ def request_filters=(value)
306
+ @request_filters = value
307
+ end
308
+
309
+ def any
310
+ AlterEgo::AnyMatcher.instance
311
+ end
312
+
313
+ def not_nil
314
+ AlterEgo::NotNilMatcher.instance
315
+ end
316
+
317
+ end
318
+
319
+ def self.append_features(klass)
320
+ # Give the other module my instance methods at the class level
321
+ klass.extend(ClassMethods)
322
+ klass.extend(Forwardable)
323
+
324
+ state_proxy = Module.new
325
+ klass.instance_variable_set :@state_proxy, state_proxy
326
+ klass.send(:include, state_proxy)
327
+
328
+ super(klass)
329
+ end
330
+
331
+ def current_state
332
+ state_id = self.state
333
+ state_id ? self.class.states[state_id] : nil
334
+ end
335
+
336
+ def state
337
+ result = (@state || self.class.default_state)
338
+ assert(result.nil? || self.class.states.keys.include?(result))
339
+ result
340
+ end
341
+
342
+ def state=(identifier)
343
+ @state = identifier
344
+ end
345
+
346
+ def state_for_identifier(identifier)
347
+ self.class.states[identifier]
348
+ end
349
+
350
+ def transition_to(new_state, request=nil, *args)
351
+ current_state.transition_to(self, request, new_state, *args)
352
+ end
353
+
354
+ def all_handled_requests
355
+ self.class.all_handled_requests
356
+ end
357
+
358
+ def execute_request_filters(state, request, new_state)
359
+ pattern = RequestFilter.new(state, request, new_state)
360
+ self.class.request_filters.grep(pattern) do |filter|
361
+ result = catch(:cancel) do
362
+ self.instance_eval(&filter.action)
363
+ true
364
+ end
365
+ return false unless result
366
+ end
367
+ true
368
+ end
369
+
370
+ def self.proc_from_symbol_or_block(symbol = nil, &block)
371
+ if symbol then
372
+ proc do
373
+ self.send(symbol)
374
+ end
375
+ elsif block then
376
+ block
377
+ else raise "Should never get here"
378
+ end
379
+ end
380
+
381
+ end