notch8-alter-ego 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 1.0.0 2008-11-28
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,18 @@
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/alter_ego.rb
9
+ lib/hash.rb
10
+ lib/hash/keys.rb
11
+ lib/hookr.rb
12
+ script/console
13
+ script/destroy
14
+ script/generate
15
+ spec/spec.opts
16
+ spec/spec_helper.rb
17
+ spec/alter_ego_spec.rb
18
+ tasks/rspec.rake
data/PostInstall.txt ADDED
@@ -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
+
data/README.rdoc ADDED
@@ -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.
data/Rakefile ADDED
@@ -0,0 +1,33 @@
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
+ ['fail-fast', '>= 1.0.0']
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
32
+
33
+ task :default => :spec
data/TODO ADDED
@@ -0,0 +1,12 @@
1
+ # -*- mode: org -*-
2
+ * DONE Factor assertions out into separate library
3
+ CLOSED: [2008-11-29 Sat 01:29]
4
+ * TODO Remove ActiveSupport dependency
5
+ * TODO Nested state support with history
6
+ * TODO Verify full methods/respond_to? integration
7
+ * TODO Composite state support
8
+ E.g. a Traffic Light could have both green/yellow/red state and "in
9
+ service"/out of service" state at the same time
10
+ * TODO More natural/Rubyish DSL
11
+ It would be good to have plain-old "def" work as expected inside of "state"
12
+ blocks instead of requiring "handle".
data/lib/alter_ego.rb ADDED
@@ -0,0 +1,388 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'forwardable'
5
+ require 'singleton'
6
+ require 'rubygems'
7
+ require File.join(File.dirname(__FILE__), 'hash')
8
+ #require 'activesupport'
9
+ require 'fail_fast'
10
+ require 'hookr'
11
+
12
+ module AlterEgo
13
+ VERSION = '1.0.0'
14
+
15
+ include FailFast::Assertions
16
+
17
+ class StateError < RuntimeError
18
+ end
19
+ class InvalidDefinitionError < StateError
20
+ end
21
+ class InvalidTransitionError < StateError
22
+ end
23
+ class InvalidRequestError < StateError
24
+ end
25
+ class WrongStateError < StateError
26
+ end
27
+
28
+ RequestFilter = Struct.new("RequestFilter",
29
+ :state,
30
+ :request,
31
+ :new_state,
32
+ :action)
33
+ class RequestFilter
34
+ def ===(other)
35
+ result = (matches?(self.state, other.state) and
36
+ matches?(self.request, other.request) and
37
+ matches?(self.new_state, other.new_state))
38
+ result
39
+ end
40
+
41
+ def matches?(lhs, rhs)
42
+ if rhs.respond_to?(:include?)
43
+ rhs.include?(lhs)
44
+ else
45
+ rhs == lhs
46
+ end
47
+ end
48
+ end
49
+
50
+ class AnyMatcher
51
+ include Singleton
52
+
53
+ def ===(other)
54
+ self == other
55
+ end
56
+
57
+ def ==(other)
58
+ true
59
+ end
60
+ end
61
+
62
+ class NotNilMatcher
63
+ include Singleton
64
+
65
+ def ===(other)
66
+ self == other
67
+ end
68
+
69
+ def ==(other)
70
+ not other.nil?
71
+ end
72
+ end
73
+
74
+ # A customization of Hookr::Hook to deal with the fact that State internal
75
+ # callbacks need to be executed in the context of the state's context, not the
76
+ # state object itself.
77
+ class StateHook < Hookr::Hook
78
+ class StateContextCallback < Hookr::InternalCallback
79
+ def call(event)
80
+ context = event.arguments.first
81
+ context.instance_eval(&block)
82
+ end
83
+ end
84
+
85
+ # Add an internal callback that executes in the context of the state
86
+ # context, instead of the state itself
87
+ def add_internal_callback(handle=nil, &block)
88
+ add_block_callback(StateContextCallback, handle, &block)
89
+ end
90
+ end
91
+
92
+ class State
93
+ include FailFast::Assertions
94
+ extend FailFast::Assertions
95
+ include Hookr::Hooks
96
+
97
+ def self.transition(options, &trans_action)
98
+ options.assert_valid_keys(:to, :on, :if)
99
+ assert_keys(options, :to)
100
+ guard = options[:if]
101
+ to_state = options[:to]
102
+ request = options[:on]
103
+ if request
104
+ handle(request) do
105
+ transition_to(to_state, request)
106
+ end
107
+ end
108
+ valid_transitions << to_state unless valid_transitions.include?(to_state)
109
+ if guard
110
+ method = guard.kind_of?(Symbol) ? guard : nil
111
+ block = guard.kind_of?(Proc) ? guard : nil
112
+ predicate = AlterEgo.proc_from_symbol_or_block(method, &block)
113
+ guard_proc = proc do
114
+ result = instance_eval(&predicate)
115
+ throw :cancel unless result
116
+ end
117
+ add_request_filter(request, to_state, guard_proc)
118
+ end
119
+ if trans_action
120
+ add_request_filter(request, to_state, trans_action)
121
+ end
122
+ end
123
+
124
+ def self.identifier
125
+ self
126
+ end
127
+
128
+ def self.valid_transitions
129
+ (@valid_transitions ||= [])
130
+ end
131
+
132
+ def self.handled_requests
133
+ public_instance_methods(false)
134
+ end
135
+
136
+ def self.request_filters
137
+ (@request_filters ||= [])
138
+ end
139
+
140
+ def self.handle(request, method = nil, &block)
141
+ define_contextual_method_from_symbol_or_block(request, method, &block)
142
+ end
143
+
144
+ def self.make_hook(name, parent, params)
145
+ ::AlterEgo::StateHook.new(name, parent, params)
146
+ end
147
+
148
+ def valid_transitions
149
+ self.class.valid_transitions
150
+ end
151
+
152
+ def to_s
153
+ "<State:#{identifier}>"
154
+ end
155
+
156
+ def identifier
157
+ self.class.identifier
158
+ end
159
+
160
+ def ==(other)
161
+ (self.identifier == other) or super(other)
162
+ end
163
+
164
+ def can_handle_request?(request)
165
+ return true if respond_to?(request)
166
+ return false
167
+ end
168
+
169
+ def transition_to(context, request, new_state, *args)
170
+ return true if context.state == new_state
171
+ new_state_obj = context.state_for_identifier(new_state)
172
+ unless new_state_obj
173
+ raise(InvalidTransitionError,
174
+ "Context #{context.inspect} has no state '#{new_state}' defined")
175
+ end
176
+
177
+ continue = context.execute_request_filters(self.class.identifier,
178
+ request,
179
+ new_state)
180
+ return false unless continue
181
+
182
+ unless valid_transitions.empty? || valid_transitions.include?(new_state)
183
+ raise(InvalidTransitionError,
184
+ "Not allowed to transition from #{self.identifier} to #{new_state}")
185
+ end
186
+
187
+ execute_hook(:on_exit, context)
188
+ new_state_obj.execute_hook(:on_enter, context)
189
+ context.state = new_state
190
+ assert(new_state == context.state)
191
+ true
192
+ end
193
+
194
+ protected
195
+
196
+ define_hook :on_enter, :context
197
+ define_hook :on_exit, :context
198
+
199
+ private
200
+
201
+ def self.add_request_filter(request_pattern, new_state_pattern, action)
202
+ new_filter = RequestFilter.new(identifier, request_pattern, new_state_pattern, action)
203
+ self.request_filters << new_filter
204
+ end
205
+
206
+ def self.define_contextual_method_from_symbol_or_block(name, symbol, &block)
207
+ if symbol
208
+ define_method(name) do |context, *args|
209
+ context.send(symbol, *args)
210
+ end
211
+ elsif block
212
+ define_method(name) do |context, *args|
213
+ context.send(:instance_eval, &block)
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ module ClassMethods
220
+ def state(identifier, options={}, &block)
221
+ if states.has_key?(identifier)
222
+ raise InvalidDefinitionError, "State #{identifier.inspect} already defined"
223
+ end
224
+ new_state = Class.new(State)
225
+ new_state_eigenclass = class << new_state; self; end
226
+ new_state_eigenclass.send(:define_method, :identifier) { identifier }
227
+ new_state.instance_eval(&block) if block
228
+
229
+ add_state(new_state, identifier, options)
230
+ end
231
+
232
+ def request_filter(options, &block)
233
+ options.assert_valid_keys(:state, :request, :new_state, :action)
234
+ options = {
235
+ :state => not_nil,
236
+ :request => not_nil,
237
+ :new_state => nil
238
+ }.merge(options)
239
+ add_request_filter(options[:state],
240
+ options[:request],
241
+ options[:new_state],
242
+ AlterEgo.proc_from_symbol_or_block(options[:method], &block))
243
+ end
244
+
245
+ def all_handled_requests
246
+ methods = @state_proxy.public_instance_methods(false)
247
+ methods -= ["identifier", "on_enter", "on_exit"]
248
+ methods.map{|m| m.to_sym}
249
+ end
250
+
251
+ def states
252
+ (@states ||= {})
253
+ end
254
+
255
+ def states=(value)
256
+ @states = value
257
+ end
258
+
259
+ def add_state(new_state, identifier=new_state.identifier, options = {})
260
+ options.assert_valid_keys(:default)
261
+
262
+ self.states[identifier] = new_state.new
263
+
264
+ if options[:default]
265
+ if @default_state
266
+ raise InvalidDefinitionError, "Cannot have more than one default state"
267
+ end
268
+ @default_state = identifier
269
+ end
270
+
271
+ new_requests = (new_state.handled_requests - all_handled_requests)
272
+ new_requests.each do |request|
273
+ @state_proxy.send(:define_method, request) do |*args|
274
+ args.unshift(self)
275
+ begin
276
+ continue = execute_request_filters(current_state.identifier,
277
+ request,
278
+ nil)
279
+ return false unless continue
280
+ current_state.send(request, *args)
281
+ rescue NoMethodError => error
282
+ if error.name.to_s == request.to_s
283
+ raise WrongStateError,
284
+ "Request '#{request}' not supported by state #{current_state}"
285
+ else
286
+ raise
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ self.request_filters += new_state.request_filters
293
+ end
294
+
295
+ def add_request_filter(state_pattern, request_pattern, new_state_pattern, action)
296
+ @request_filters << RequestFilter.new(state_pattern,
297
+ request_pattern,
298
+ new_state_pattern,
299
+ action)
300
+ end
301
+
302
+ def default_state
303
+ @default_state
304
+ end
305
+
306
+ def request_filters
307
+ (@request_filters ||= [])
308
+ end
309
+
310
+ protected
311
+
312
+ def request_filters=(value)
313
+ @request_filters = value
314
+ end
315
+
316
+ def any
317
+ AlterEgo::AnyMatcher.instance
318
+ end
319
+
320
+ def not_nil
321
+ AlterEgo::NotNilMatcher.instance
322
+ end
323
+
324
+ end # End ClassMethods
325
+
326
+ def self.append_features(klass)
327
+ # Give the other module my instance methods at the class level
328
+ klass.extend(ClassMethods)
329
+ klass.extend(Forwardable)
330
+
331
+ state_proxy = Module.new
332
+ klass.instance_variable_set :@state_proxy, state_proxy
333
+ klass.send(:include, state_proxy)
334
+
335
+ super(klass)
336
+ end
337
+
338
+ def current_state
339
+ state_id = self.state
340
+ state_id ? self.class.states[state_id] : nil
341
+ end
342
+
343
+ def state
344
+ result = (@state || self.class.default_state)
345
+ assert(result.nil? || self.class.states.keys.include?(result))
346
+ result
347
+ end
348
+
349
+ def state=(identifier)
350
+ @state = identifier
351
+ end
352
+
353
+ def state_for_identifier(identifier)
354
+ self.class.states[identifier]
355
+ end
356
+
357
+ def transition_to(new_state, request=nil, *args)
358
+ current_state.transition_to(self, request, new_state, *args)
359
+ end
360
+
361
+ def all_handled_requests
362
+ self.class.all_handled_requests
363
+ end
364
+
365
+ def execute_request_filters(state, request, new_state)
366
+ pattern = RequestFilter.new(state, request, new_state)
367
+ self.class.request_filters.grep(pattern) do |filter|
368
+ result = catch(:cancel) do
369
+ self.instance_eval(&filter.action)
370
+ true
371
+ end
372
+ return false unless result
373
+ end
374
+ true
375
+ end
376
+
377
+ def self.proc_from_symbol_or_block(symbol = nil, &block)
378
+ if symbol then
379
+ proc do
380
+ self.send(symbol)
381
+ end
382
+ elsif block then
383
+ block
384
+ else raise "Should never get here"
385
+ end
386
+ end
387
+
388
+ end
data/lib/hash/keys.rb ADDED
@@ -0,0 +1,52 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Hash #:nodoc:
4
+ module Keys
5
+ # Return a new hash with all keys converted to strings.
6
+ def stringify_keys
7
+ inject({}) do |options, (key, value)|
8
+ options[key.to_s] = value
9
+ options
10
+ end
11
+ end
12
+
13
+ # Destructively convert all keys to strings.
14
+ def stringify_keys!
15
+ keys.each do |key|
16
+ self[key.to_s] = delete(key)
17
+ end
18
+ self
19
+ end
20
+
21
+ # Return a new hash with all keys converted to symbols.
22
+ def symbolize_keys
23
+ inject({}) do |options, (key, value)|
24
+ options[(key.to_sym rescue key) || key] = value
25
+ options
26
+ end
27
+ end
28
+
29
+ # Destructively convert all keys to symbols.
30
+ def symbolize_keys!
31
+ self.replace(self.symbolize_keys)
32
+ end
33
+
34
+ alias_method :to_options, :symbolize_keys
35
+ alias_method :to_options!, :symbolize_keys!
36
+
37
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
38
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
39
+ # as keys, this will fail.
40
+ #
41
+ # ==== Examples:
42
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
43
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
44
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
45
+ def assert_valid_keys(*valid_keys)
46
+ unknown_keys = keys - [valid_keys].flatten
47
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/hash.rb ADDED
@@ -0,0 +1,7 @@
1
+ %w(keys).each do |ext|
2
+ load "hash/#{ext}.rb"
3
+ end
4
+
5
+ class Hash #:nodoc:
6
+ include ActiveSupport::CoreExtensions::Hash::Keys
7
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/alter_ego.rb'}"
9
+ puts "Loading AlterEgo gem"
10
+ exec "#{irb} #{libs} --simple-prompt"