davidlee-state-fu 0.3.1 → 0.10.0

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.
Files changed (90) hide show
  1. data/README.textile +124 -34
  2. data/Rakefile +36 -30
  3. data/lib/no_stdout.rb +1 -1
  4. data/lib/state-fu.rb +9 -8
  5. data/lib/state_fu/active_support_lite/array/access.rb +12 -5
  6. data/lib/state_fu/active_support_lite/array/conversions.rb +10 -4
  7. data/lib/state_fu/active_support_lite/array/extract_options.rb +5 -4
  8. data/lib/state_fu/active_support_lite/array/grouping.rb +7 -4
  9. data/lib/state_fu/active_support_lite/array/random_access.rb +4 -3
  10. data/lib/state_fu/active_support_lite/array/wrapper.rb +4 -3
  11. data/lib/state_fu/active_support_lite/array.rb +3 -1
  12. data/lib/state_fu/active_support_lite/blank.rb +18 -9
  13. data/lib/state_fu/active_support_lite/cattr_reader.rb +4 -1
  14. data/lib/state_fu/active_support_lite/keys.rb +8 -3
  15. data/lib/state_fu/active_support_lite/misc.rb +6 -4
  16. data/lib/state_fu/active_support_lite/module/delegation.rb +130 -0
  17. data/lib/state_fu/active_support_lite/module.rb +1 -0
  18. data/lib/state_fu/active_support_lite/object.rb +5 -2
  19. data/lib/state_fu/active_support_lite/string.rb +6 -1
  20. data/lib/state_fu/active_support_lite/symbol.rb +2 -1
  21. data/lib/state_fu/applicable.rb +41 -0
  22. data/lib/state_fu/{helper.rb → arrays.rb} +45 -121
  23. data/lib/state_fu/binding.rb +136 -159
  24. data/lib/state_fu/core_ext.rb +78 -10
  25. data/lib/state_fu/event.rb +112 -48
  26. data/lib/state_fu/exceptions.rb +80 -34
  27. data/lib/state_fu/executioner.rb +149 -0
  28. data/lib/state_fu/has_options.rb +16 -0
  29. data/lib/state_fu/hooks.rb +21 -16
  30. data/lib/state_fu/interface.rb +80 -83
  31. data/lib/state_fu/lathe.rb +361 -148
  32. data/lib/state_fu/logger.rb +122 -45
  33. data/lib/state_fu/machine.rb +60 -32
  34. data/lib/state_fu/method_factory.rb +180 -72
  35. data/lib/state_fu/methodical.rb +17 -0
  36. data/lib/state_fu/persistence/active_record.rb +6 -1
  37. data/lib/state_fu/persistence/attribute.rb +1 -0
  38. data/lib/state_fu/persistence/base.rb +8 -6
  39. data/lib/state_fu/persistence.rb +94 -23
  40. data/lib/state_fu/sprocket.rb +26 -11
  41. data/lib/state_fu/state.rb +8 -27
  42. data/lib/state_fu/transition.rb +207 -98
  43. data/lib/state_fu/transition_query.rb +214 -0
  44. data/lib/state_fu.rb +1 -0
  45. data/lib/tasks/spec_last.rake +46 -0
  46. data/lib/tasks/state_fu.rake +57 -0
  47. data/lib/vizier.rb +61 -61
  48. data/spec/custom_formatter.rb +49 -0
  49. data/spec/features/binding_and_transition_helper_mixin_spec.rb +2 -2
  50. data/spec/features/method_missing_only_once_spec.rb +28 -0
  51. data/spec/features/not_requirements_spec.rb +83 -46
  52. data/spec/features/plotter_spec.rb +97 -0
  53. data/spec/features/shared_log_spec.rb +7 -0
  54. data/spec/features/singleton_machine_spec.rb +39 -0
  55. data/spec/features/state_and_array_options_accessor_spec.rb +1 -1
  56. data/spec/features/{transition_boolean_comparison.rb → transition_boolean_comparison_spec.rb} +29 -18
  57. data/spec/helper.rb +6 -117
  58. data/spec/integration/active_record_persistence_spec.rb +18 -4
  59. data/spec/integration/binding_extension_spec.rb +1 -1
  60. data/spec/integration/class_accessor_spec.rb +49 -59
  61. data/spec/integration/event_definition_spec.rb +20 -20
  62. data/spec/integration/example_01_document_spec.rb +13 -8
  63. data/spec/integration/example_02_string_spec.rb +3 -2
  64. data/spec/integration/instance_accessor_spec.rb +16 -19
  65. data/spec/integration/lathe_extension_spec.rb +2 -2
  66. data/spec/integration/machine_duplication_spec.rb +59 -37
  67. data/spec/integration/relaxdb_persistence_spec.rb +6 -3
  68. data/spec/integration/requirement_reflection_spec.rb +66 -57
  69. data/spec/integration/state_definition_spec.rb +72 -66
  70. data/spec/integration/transition_spec.rb +169 -173
  71. data/spec/spec.opts +5 -3
  72. data/spec/spec_helper.rb +132 -0
  73. data/spec/state_fu_spec.rb +870 -0
  74. data/spec/units/binding_spec.rb +33 -22
  75. data/spec/units/event_spec.rb +3 -22
  76. data/spec/units/exceptions_spec.rb +7 -0
  77. data/spec/units/lathe_spec.rb +7 -7
  78. data/spec/units/machine_spec.rb +67 -75
  79. data/spec/units/method_factory_spec.rb +55 -48
  80. data/spec/units/sprocket_spec.rb +5 -7
  81. data/spec/units/state_spec.rb +33 -24
  82. metadata +31 -19
  83. data/lib/state_fu/active_support_lite/inheritable_attributes.rb +0 -1
  84. data/lib/state_fu/fu_space.rb +0 -51
  85. data/lib/state_fu/mock_transition.rb +0 -38
  86. data/spec/BDD/plotter_spec.rb +0 -115
  87. data/spec/integration/dynamic_requirement_spec.rb +0 -160
  88. data/spec/integration/ex_machine_for_accounts_spec.rb +0 -79
  89. data/spec/integration/sanity_spec.rb +0 -31
  90. data/spec/units/fu_space_spec.rb +0 -95
@@ -1,8 +1,8 @@
1
1
  module StateFu
2
2
  class Binding
3
- include ContextualEval
4
3
 
5
- attr_reader :object, :machine, :method_name, :persister, :transitions, :options
4
+ attr_reader :object, :machine, :method_name, :field_name, :persister, :transitions, :options, :target
5
+
6
6
 
7
7
  # the constructor should not be called manually; a binding is
8
8
  # returned when an instance of a class with a StateFu::Machine
@@ -18,39 +18,31 @@ module StateFu
18
18
  @method_name = method_name
19
19
  @transitions = []
20
20
  @options = options.symbolize_keys!
21
- field_name = StateFu::FuSpace.field_names[object.class][@method_name]
22
- raise( ArgumentError, "No field_name" ) unless field_name
23
- # ensure state field is set up (in case we created this binding
24
- # manually, instead of via Machine.bind!)
25
- StateFu::Persistence.prepare_field( object.class, field_name )
26
- # add a persister
27
- @persister = StateFu::Persistence.for( self, field_name )
28
- Logger.debug( "Persister (#{@persister.class}) added: #{method_name} as field #{field_name}" )
29
-
30
- # define event methods on self( binding ) and @object
31
- StateFu::MethodFactory.new( self ).install!
32
- machine.helpers.inject_into( self )
33
- # StateFu::Persistence.prepare_field( @object.class, field_name )
21
+ @target = singleton? ? object : object.class
22
+ @field_name = options[:field_name] || @target.state_fu_field_names[method_name]
23
+ @persister = Persistence.for( self )
24
+
25
+ # define event methods on this binding and its @object
26
+ MethodFactory.new( self ).install!
27
+ @machine.helpers.inject_into( self )
34
28
  end
29
+
35
30
  alias_method :o, :object
36
31
  alias_method :obj, :object
37
32
  alias_method :model, :object
38
33
  alias_method :instance, :object
39
34
 
40
- alias_method :machine, :machine
41
35
  alias_method :workflow, :machine
42
36
  alias_method :state_machine, :machine
43
37
 
44
- # the perister's field_name (a symbol)
45
- def field_name
46
- persister.field_name
47
- end
38
+ #
39
+ # current state
40
+ #
48
41
 
49
- # the current_state, as maintained by the persister.
42
+ # the current State
50
43
  def current_state
51
44
  persister.current_state
52
45
  end
53
- alias_method :at, :current_state
54
46
  alias_method :now, :current_state
55
47
  alias_method :state, :current_state
56
48
 
@@ -66,54 +58,48 @@ module StateFu
66
58
  alias_method :state_name, :current_state_name
67
59
  alias_method :to_sym, :current_state_name
68
60
 
69
- # returns a list of StateFu::Events which can fire from the current_state
61
+ #
62
+ # events
63
+ #
64
+
65
+ # returns a list of Events which can fire from the current_state
70
66
  def events
71
- machine.events.select {|e| e.complete? && e.from?( current_state ) }.extend EventArray
67
+ machine.events.select do |e|
68
+ e.can_transition_from? current_state
69
+ end.extend EventArray
72
70
  end
73
71
  alias_method :events_from_current_state, :events
74
72
 
75
- # the subset of events() whose requirements for firing are met
76
- # (with the arguments supplied, if any)
77
- def valid_events( *args )
78
- return nil unless current_state
79
- return [] unless current_state.exitable_by?( self, *args )
80
- events.select {|e| e.fireable_by?( self, *args ) }.extend EventArray
81
- end
82
-
83
73
  # the subset of events() whose requirements for firing are NOT met
84
74
  # (with the arguments supplied, if any)
85
75
  def invalid_events( *args )
86
76
  ( events - valid_events( *args ) ).extend StateArray
87
77
  end
88
78
 
89
- def unmet_requirements_for( event, target )
90
- raise NotImplementedError
79
+ # all states which can be reached from the current_state.
80
+ # Does not check transition requirements, etc.
81
+ def next_states
82
+ events.map(&:targets).compact.flatten.uniq.extend StateArray
83
+ end
84
+
85
+ #
86
+ # transition validation
87
+ #
88
+
89
+ def transitions(opts={}) # .with(*args)
90
+ TransitionQuery.new(self, opts)
91
91
  end
92
92
 
93
- # the counterpart to valid_events - the states we can arrive at in
94
- # the firing of one event, taking into account event and state
95
- # transition requirements
96
- def valid_next_states( *args )
97
- vt = valid_transitions( *args )
98
- vt && vt.values.flatten.uniq.extend( StateArray )
93
+ def valid_transitions(*args)
94
+ transitions.valid.with(*args)
99
95
  end
100
96
 
101
- #
102
- def next_states
103
- events.map(&:targets).compact.flatten.uniq.extend StateArray
97
+ def valid_next_states(*args)
98
+ transitions.with(*args).targets
104
99
  end
105
100
 
106
- # returns a hash of { event => [states] } whose transition
107
- # requirements are met
108
- def valid_transitions( *args )
109
- h = {}
110
- return nil unless ve = valid_events( *args )
111
- ve.each do |e|
112
- h[e] = e.targets.select do |s|
113
- s.enterable_by?( self, *args )
114
- end
115
- end
116
- h
101
+ def valid_events(*args)
102
+ transitions.with(*args).events
117
103
  end
118
104
 
119
105
  # initializes a new Transition to the given destination, with the
@@ -122,8 +108,7 @@ module StateFu
122
108
  # If a block is given, it yields the Transition or is executed in
123
109
  # its evaluation context, depending on the arity of the block.
124
110
  def transition( event_or_array, *args, &block )
125
- event, target = parse_destination( event_or_array )
126
- StateFu::Transition.new( self, event, target, *args, &block )
111
+ return transitions.with(*args, &block).find(event_or_array)
127
112
  end
128
113
  alias_method :fire, :transition
129
114
  alias_method :fire_event, :transition
@@ -131,130 +116,66 @@ module StateFu
131
116
  alias_method :trigger_event, :transition
132
117
  alias_method :begin_transition, :transition
133
118
 
134
- # return a MockTransition to nowhere and passes it the given
135
- # *args. Useful for evaluating requirements in spec / test code.
136
- def blank_mock_transition( *args, &block )
137
- StateFu::MockTransition.new( self, nil, nil, *args, &block )
138
- end
139
-
140
- # return a MockTransition; otherwise the same as #transition
141
- def mock_transition( event_or_array, *args, &block )
142
- event, target = nil
143
- event, target = parse_destination( event_or_array )
144
- StateFu::MockTransition.new( self, event, target, *args, &block )
145
- end
146
-
147
- # sanitizes / extracts destination from *args for other methods.
148
- #
149
- # takes a single, simple (one target only) event,
150
- # or an array of [event, target],
151
- # or one of the above with symbols in place of the objects themselves.
152
- def parse_destination( event_or_array )
153
- case event_or_array
154
- when StateFu::Event, Symbol
155
- event = event_or_array
156
- target = nil
157
- when Array
158
- event, target = *event_or_array
159
- end
160
- x = event_or_array.is_a?( Array ) ? event_or_array.map(&:class) : event_or_array
161
- raise ArgumentError.new( x.inspect ) unless
162
- [StateFu::Event, Symbol ].include?( event.class ) &&
163
- [StateFu::State, Symbol, NilClass].include?( target.class )
164
- [event, target]
165
- end
166
-
167
119
  # check that the event and target are valid (all requirements are
168
120
  # met) with the given (optional) arguments
169
121
  def fireable?( event_or_array, *args )
170
- event, target = parse_destination( event_or_array )
171
122
  begin
172
- t = transition( [event, target] )
123
+ return nil unless t = transition( event_or_array, *args )
173
124
  !! t.requirements_met?
174
125
  rescue InvalidTransition => e
175
126
  nil
176
127
  end
177
128
  end
178
- alias_method :event?, :fireable?
179
- alias_method :event_fireable?, :fireable?
180
- alias_method :can_fire?, :fireable?
181
- alias_method :can_fire_event?, :fireable?
182
- alias_method :trigger?, :fireable?
183
- alias_method :triggerable?, :fireable?
184
- alias_method :can_trigger?, :fireable?
185
- alias_method :can_trigger_event?, :fireable?
186
- alias_method :event_triggerable?, :fireable?
187
- alias_method :transition?, :fireable?
188
- alias_method :can_transition?, :fireable?
189
- alias_method :transitionable?, :fireable?
190
-
191
- # construct an event transition and fire it
129
+
130
+ # construct an event transition and fire it, returning the transition.
131
+ # (which is == true if the transition completed successfully.)
192
132
  def fire!( event_or_array, *args, &block)
193
- event, target = parse_destination( event_or_array )
194
- t = transition( [event, target], *args, &block )
133
+ # TODO rather than die, try to find the next valid transition and fire that
134
+ t = transition(event_or_array, *args, &block )
195
135
  t.fire!
196
- t
197
136
  end
198
- alias_method :event!, :fire!
199
137
  alias_method :trigger!, :fire!
200
138
  alias_method :transition!, :fire!
201
139
 
202
- # evaluate a requirement depending whether it's a method or proc,
203
- # and its arity - see helper.rb (ContextualEval) for the smarts
204
-
205
- # TODO - enable requirement block / method to know the target
206
-
207
- def evaluate_requirement_with_args( name, *args )
208
- t = blank_mock_transition( *args )
209
- evaluate_named_proc_or_method( name, t )
210
- end
211
- # alias_method :evaluate_requirement, :evaluate_requirement_with_args
212
-
213
- alias_method :evaluate_requirement_with_transition, :evaluate_named_proc_or_method
214
-
215
- # if there is exactly one legal transition which can be fired with
140
+ #
141
+ # next_transition and friends: when there's exactly one valid move
142
+ #
143
+
144
+ # if there is exactly one legal & valid transition which can be fired with
216
145
  # the given (optional) arguments, return it.
217
146
  def next_transition( *args, &block )
218
- vts = valid_transitions( *args )
219
- return nil if vts.nil?
220
- next_transition_candidates = vts.select {|e, s| s.length == 1 }
221
- if next_transition_candidates.length == 1
222
- nt = next_transition_candidates.first
223
- evt = nt[0]
224
- targ = nt[1][0]
225
- return transition( [ evt, targ], *args, &block )
226
- end
147
+ transitions.with(*args, &block).next
148
+ end
149
+
150
+ # as above but ignoring any transitions whose origin and target are the same
151
+ def next_transition_excluding_cycles( *args, &block )
152
+ transitions.not_cyclic.with(*args, &block).next
227
153
  end
228
154
 
229
155
  # if there is exactly one state reachable via a transition which
230
156
  # is valid with the given optional arguments, return it.
231
- def next_state( *args )
232
- nt = next_transition( *args )
233
- nt && nt.target
157
+ def next_state(*args, &block)
158
+ transitions.with(*args, &block).next_state
234
159
  end
235
160
 
236
161
  # if there is exactly one event which is valid with the given
237
162
  # optional arguments, return it
238
163
  def next_event( *args )
239
- nt = next_transition( *args )
240
- nt && nt.event
164
+ transitions.with(*args, &block).next_event
241
165
  end
242
-
166
+
243
167
  # if there is a next_transition, create, fire & return it
244
168
  # otherwise raise an InvalidTransition
245
169
  def next!( *args, &block )
246
170
  if t = next_transition( *args, &block )
247
171
  t.fire!
248
- t
249
172
  else
250
- vts = valid_transitions( *args )
251
- n = vts && vts.length
252
- raise InvalidTransition.
253
- new( self, current_state, vts, "there are #{n} candidate transitions, need exactly 1")
173
+ raise TransitionNotFound.new( self, valid_transitions(*args), "Exactly 1 valid transition required.")
254
174
  end
255
175
  end
256
- alias_method :next_state!, :next!
176
+ alias_method :next_transition!, :next!
257
177
  alias_method :next_event!, :next!
178
+ alias_method :next_state!, :next!
258
179
 
259
180
  # if there is a next_transition, return true / false depending on
260
181
  # whether its requirements are met
@@ -264,37 +185,45 @@ module StateFu
264
185
  t.requirements_met?
265
186
  end
266
187
  end
267
- alias_method :next_state?, :next?
268
- alias_method :next_event?, :next?
188
+ # alias_method :next_state?, :next?
189
+ # alias_method :next_event?, :next?
190
+
191
+ # Cyclic transitions (origin == target)
269
192
 
270
193
  # if there is one possible cyclical event, return a transition there
271
- def cycle( *args, &block)
272
- cycle_events = events.select {|e| e.target == current_state }
273
- if cycle_events.length == 1
274
- transition( cycle_events[0], *args, &block )
194
+ # otherwise, maybe we got an event name as an argument?
195
+ def cycle(event_or_array=nil, *args, &block)
196
+ if event_or_array.nil?
197
+ transitions.cyclic.with(*args, &block).singular ||
198
+ transitions.cyclic.with(*args, &block).valid.singular
199
+ else
200
+ transitions.cyclic.with(*args, &block).find(event_or_array)
275
201
  end
276
202
  end
277
203
 
278
- # if there is a cycle() transition, fire and return it
204
+ # if there is a single possible cycle() transition, fire and return it
279
205
  # otherwise raise an InvalidTransition
280
- def cycle!( *args, &block )
281
- if t = cycle( *args, &block )
206
+ def cycle!(event_or_array=nil, *args, &block )
207
+ if t = cycle(event_or_array, *args, &block )
282
208
  t.fire!
283
209
  t
284
210
  else
285
- err_msg = "Cannot cycle! unless there is exactly one event leading from the current state to itself"
286
- raise InvalidTransition.new( self, current_state, current_state, err_msg )
211
+ raise TransitionNotFound.new( self, transitions.cyclic.with(*args,&block), "Cannot cycle! unless there is exactly one cyclic event")
287
212
  end
288
213
  end
289
214
 
290
215
  # if there is one possible cyclical event, evaluate its
291
216
  # requirements (true/false), else nil
292
- def cycle?( *args )
293
- if t = cycle( *args )
217
+ def cycle?(event_or_array=nil, *args )
218
+ if t = cycle(event_or_array, *args )
294
219
  t.requirements_met?
295
220
  end
296
221
  end
297
222
 
223
+ #
224
+ # misc
225
+ #
226
+
298
227
  # change the current state of the binding without any
299
228
  # requirements or other sanity checks, or any hooks firing.
300
229
  # Useful for test / spec scenarios, and abusing the framework.
@@ -308,12 +237,12 @@ module StateFu
308
237
  attrs = [[:current_state, state_name.inspect],
309
238
  [:object_type , @object.class],
310
239
  [:method_name , method_name.inspect],
311
- [:field_name , persister.field_name.inspect],
240
+ [:field_name , field_name.inspect],
312
241
  [:machine , machine.inspect]].
313
242
  map {|x| x.join('=') }.join( " " ) + ' =>|'
314
243
  end
315
244
 
316
- # let's be == the current_state_name as a symbol.
245
+ # let's be == (and hence ===) the current_state_name as a symbol.
317
246
  # a nice little convenience.
318
247
  def == other
319
248
  if other.respond_to?( :to_sym ) && current_state
@@ -323,5 +252,53 @@ module StateFu
323
252
  end
324
253
  end
325
254
 
255
+ # TODO better name
256
+ # is this a binding unique to a specific instance (not bound to a class)?
257
+ def singleton?
258
+ options[:singleton]
259
+ end
260
+
261
+ # SPECME DOCME OR KILLME
262
+ def reload()
263
+ if persister.is_a?( Persistence::ActiveRecord )
264
+ object.reload
265
+ end
266
+ persister.reload
267
+ self
268
+ end
269
+
270
+ # This method is called from methods defined by MethodFactory.
271
+ # You don't want to call it directly.
272
+ def _event_method(action, event, *args)
273
+ target_or_options = args.shift
274
+ options = {}
275
+ case target_or_options
276
+ when Hash
277
+ options = target_or_options.symbolize_keys!
278
+ target = target_or_options.delete[:to]
279
+ when Symbol, String
280
+ target = target_or_options.to_sym
281
+ when nil
282
+ target = nil
283
+ end
284
+
285
+ case action
286
+ when :get_transition
287
+ transition [event, target], *args, &lambda {|t| t.options = options}
288
+ when :query_transition
289
+ fireable? [event, target], *args, &lambda {|t| t.options = options}
290
+ when :fire_transition
291
+ fire! [event, target], *args, &lambda {|t| t.options = options}
292
+ else
293
+ raise ArgumentError.new(action)
294
+ end
295
+ end
296
+
297
+ #
298
+ # transition constructor
299
+ #
300
+ def new_transition(event, target, *args, &block)
301
+ Transition.new( self, event, target, *args, &block )
302
+ end
326
303
  end
327
304
  end
@@ -1,23 +1,91 @@
1
1
  require 'rubygems'
2
- # ruby1.9 style symbol comparability for ruby1.8
3
- class Symbol # :nodoc:
4
- unless instance_methods.include?(:'<=>')
5
- def <=> other
6
- self.to_s <=> other.to_s
7
- end
8
- end
9
- end
10
2
 
11
3
  # if ActiveSupport is absent, install a very small subset of it for
12
4
  # some convenience methods
13
- unless Object.const_defined?('ActiveSupport') # :nodoc:
5
+ unless Object.const_defined?('ActiveSupport') #:nodoc
14
6
  Dir[File.join(File.dirname( __FILE__), 'active_support_lite','**' )].sort.each do |lib|
15
7
  next unless File.file?( lib )
16
8
  require lib
17
9
  end
18
10
 
19
- class Hash #:nodoc:
11
+ class Hash #:nodoc
20
12
  include ActiveSupport::CoreExtensions::Hash::Keys
21
13
  end
14
+ end
15
+
16
+ # ruby1.9 style symbol comparability for ruby1.8
17
+ if RUBY_VERSION < "1.9"
18
+ class Symbol #:nodoc
19
+ unless instance_methods.include?(:'<=>')
20
+ def <=> other
21
+ self.to_s <=> other.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ module ArrayToHash
28
+ def to_h
29
+ if RUBY_VERSION >= '1.8.7'
30
+ Hash[self]
31
+ else
32
+ inject({}) { |h, a| h[a.first] = a.last; h }
33
+ end
34
+ end
35
+ end
36
+
37
+ class Array
38
+ include ArrayToHash unless instance_methods.include?(:to_h)
39
+ end
40
+
41
+ class Object
42
+
43
+ def self.__define_method( method_name, &block )
44
+ self.class.class_eval do
45
+ define_method method_name, &block
46
+ end
47
+ end
48
+
49
+ def __define_singleton_method( method_name, &block )
50
+ (class << self; self; end).class_eval do
51
+ define_method method_name, &block
52
+ end
53
+ end
22
54
 
55
+
56
+ def with_methods_on(other)
57
+ (class << self; self; end).class_eval do
58
+ # we need some accounting to ensure that everything behaves itself when
59
+ # .with_methods_on is called more than once.
60
+ @_with_methods_on ||= []
61
+ if !@_with_methods_on.include?("method_missing_before_#{other.__id__}")
62
+ alias_method "method_missing_before_#{other.__id__}", :method_missing
63
+ end
64
+ @_with_methods_on << "method_missing_before_#{other.__id__}"
65
+
66
+ define_method :method_missing do |method_name, *args|
67
+ if _other.respond_to?(method_name, true)
68
+ _other.__send__( method_name, *args )
69
+ else
70
+ send "method_missing_before_#{other.__id__}", method_name, *args
71
+ end
72
+ end
73
+ end
74
+
75
+ result = yield
76
+
77
+ (class << self; self; end).class_eval do
78
+ # heal the damage
79
+ if @_with_methods_on.pop != "method_missing_before_#{other.__id__}"
80
+ raise "there is no god"
81
+ end
82
+ if !@_with_methods_on.include?("method_missing_before_#{other.__id__}")
83
+ alias_method :method_missing, "method_missing_before_#{other.__id__}"
84
+ undef_method "method_missing_before_#{other.__id__}"
85
+ end
86
+ end
87
+
88
+ result
89
+ end # with_methods_on
23
90
  end
91
+