contextr 0.0.1 → 0.0.2
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.
- data/History.txt +8 -0
- data/LICENSE.txt +2 -2
- data/Manifest.txt +4 -2
- data/README.txt +36 -3
- data/examples/education.rb +19 -15
- data/lib/contextr.rb +4 -2
- data/lib/contextr/contextr.rb +57 -384
- data/lib/contextr/layer.rb +405 -0
- data/lib/contextr/version.rb +1 -1
- data/lib/core_ext/class.rb +6 -1
- data/lib/core_ext/module.rb +39 -3
- data/lib/core_ext/proc.rb +29 -3
- data/{ext → lib/ext}/dynamic.rb +10 -0
- data/lib/ext/method_nature.rb +80 -0
- data/spec/contextr/contextr_api_spec.rb +0 -39
- data/spec/contextr/contextr_functional_spec.rb +294 -0
- data/test/contextr/test_contextr.rb +2 -160
- data/website/index.html +16 -8
- data/website/index.txt +16 -7
- metadata +6 -5
- data/ext/method_nature.rb +0 -17
@@ -0,0 +1,405 @@
|
|
1
|
+
module ContextR
|
2
|
+
# This is the public interface of a Layer within a class definition. Use it
|
3
|
+
# to add context-dependent behaviour. It is available after calling
|
4
|
+
# +layer <em>layer_name</em>+ within a class body via
|
5
|
+
# +<em>layer_name</em>+. Each wrapper block should expect an instance of
|
6
|
+
# MethodNature to manipulate arguments or return values.
|
7
|
+
#
|
8
|
+
# class Foo
|
9
|
+
# layer :any
|
10
|
+
#
|
11
|
+
# def bar
|
12
|
+
# "bar"
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# any.pre :bar do | n |
|
16
|
+
# logger.info "Foo#bar called"
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
class LayerInClass
|
20
|
+
attr_accessor :contextualized_class, :layer # :nodoc:
|
21
|
+
|
22
|
+
def initialize( contextualized_class, layer ) # :nodoc:
|
23
|
+
self.contextualized_class = contextualized_class
|
24
|
+
self.layer = layer
|
25
|
+
end
|
26
|
+
|
27
|
+
# Adds a pre-wrapper to a single method.
|
28
|
+
#
|
29
|
+
# :call-seq:
|
30
|
+
# pre( method_name ) { | method_nature | ... }
|
31
|
+
#
|
32
|
+
def pre( method_name, &block )
|
33
|
+
layer.methods_of( self.contextualized_class )[method_name].pres <<
|
34
|
+
block.to_unbound_method( self.contextualized_class )
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adds a post-wrapper to a single method
|
39
|
+
#
|
40
|
+
# :call-seq:
|
41
|
+
# post( method_name ) { | method_nature | ... }
|
42
|
+
#
|
43
|
+
def post( method_name, &block )
|
44
|
+
layer.methods_of( self.contextualized_class )[method_name].posts <<
|
45
|
+
block.to_unbound_method( self.contextualized_class )
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds an around-wrapper to a single method
|
50
|
+
#
|
51
|
+
# :call-seq:
|
52
|
+
# around( method_name ) { | method_nature | ... }
|
53
|
+
#
|
54
|
+
def around( method_name, &block )
|
55
|
+
layer.methods_of( self.contextualized_class )[method_name].arounds <<
|
56
|
+
block.to_unbound_method( self.contextualized_class )
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
alias :wrap :around
|
61
|
+
end
|
62
|
+
|
63
|
+
class Layer # :nodoc:
|
64
|
+
# its role as base instance of all layers
|
65
|
+
class << self
|
66
|
+
attr_accessor_with_default_setter :base_layers, :combined_layers do
|
67
|
+
Hash.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def core_methods
|
71
|
+
@core_methods ||= Hash.new do | hash, extended_class |
|
72
|
+
add_redefine_callback( extended_class )
|
73
|
+
hash[extended_class] = Hash.new do | class_hash, method_name |
|
74
|
+
um = extended_class.instance_method( method_name )
|
75
|
+
replace_core_method( extended_class, method_name )
|
76
|
+
class_hash[method_name] = um
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def inherited( new_base_layer )
|
82
|
+
unless new_base_layer.name.empty?
|
83
|
+
base_layers[ContextR::symbolize( new_base_layer )] = new_base_layer
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def compose( layers )
|
88
|
+
# TODO: Add better caching
|
89
|
+
combined_layers[ layers ] ||=
|
90
|
+
layers.reverse.inject( nil ) do | akku, layer |
|
91
|
+
layer + akku
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def replace_core_method( extended_class, method_name )
|
97
|
+
num_of_args = extended_class.instance_method( method_name ).arity
|
98
|
+
arg_signature = case num_of_args <=> 0
|
99
|
+
when 0
|
100
|
+
""
|
101
|
+
when 1
|
102
|
+
"%s" % Array.new( num_of_args ) { |i| "arg%d" % i }.join( ", " )
|
103
|
+
else
|
104
|
+
"*arguments"
|
105
|
+
end
|
106
|
+
arg_call = arg_signature.empty? ? "" : ", " + arg_signature
|
107
|
+
|
108
|
+
extended_class.class_eval( %Q{
|
109
|
+
remove_method :#{method_name}
|
110
|
+
def #{method_name} #{arg_signature}
|
111
|
+
ContextR::current_layer.extended( self ).send(
|
112
|
+
:#{method_name}#{arg_call} )
|
113
|
+
end
|
114
|
+
}, __FILE__, __LINE__ )
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_redefine_callback( extended_class )
|
118
|
+
(class << extended_class; self; end).instance_eval do
|
119
|
+
define_method( :method_added ) do | method_name |
|
120
|
+
if ContextR::Layer.core_methods[extended_class].
|
121
|
+
include?( method_name )
|
122
|
+
warn( caller.first + " : ContextR - Redefining already wrapped methods is not supported yet. Your changes _may_ have no effect." )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Layer # :nodoc:
|
131
|
+
# its role as base class for all other layers
|
132
|
+
class << self
|
133
|
+
attr_accessor_with_default_setter :extended_classes do
|
134
|
+
Hash.new do | classes, extended_class |
|
135
|
+
classes[extended_class] = Hash.new do | hash, key |
|
136
|
+
hash[key.to_sym] = ContextualizedMethod.new(
|
137
|
+
ContextR::Layer.core_methods[ extended_class ][ key.to_sym ] )
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
attr_accessor :extended_objects
|
142
|
+
|
143
|
+
def methods_of( extended_class )
|
144
|
+
self.extended_classes[extended_class]
|
145
|
+
end
|
146
|
+
|
147
|
+
def extended( object )
|
148
|
+
self.extended_objects ||= Hash.new do | cache, object |
|
149
|
+
cache[ object ] =
|
150
|
+
ExtendedObject.new( object, self.methods_of( object.class ) )
|
151
|
+
end
|
152
|
+
self.extended_objects[object]
|
153
|
+
end
|
154
|
+
|
155
|
+
def + other_layer
|
156
|
+
if other_layer.nil?
|
157
|
+
self
|
158
|
+
else
|
159
|
+
combined_layer = Class.new( Layer )
|
160
|
+
combined_layer.extended_classes = self.merge_extended_classes_with(
|
161
|
+
other_layer.extended_classes )
|
162
|
+
combined_layer
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
def merge_extended_classes_with( other_ec )
|
169
|
+
extended_classes.merge( other_ec ) do | extended_c, my_ms, other_ms |
|
170
|
+
my_ms.merge( other_ms ) do | method_name, my_cm, other_cm |
|
171
|
+
my_cm + other_cm
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class ExtendedObject # :nodoc:
|
179
|
+
attr_accessor :proxied_object
|
180
|
+
attr_accessor :extended_methods
|
181
|
+
attr_accessor :behaviours
|
182
|
+
|
183
|
+
def initialize( proxied_object, extended_methods )
|
184
|
+
self.proxied_object = proxied_object
|
185
|
+
self.extended_methods = extended_methods
|
186
|
+
self.behaviours = {}
|
187
|
+
end
|
188
|
+
|
189
|
+
def send( method_name, *arguments )
|
190
|
+
( self.behaviours[method_name] ||=
|
191
|
+
self.extended_methods[method_name].behaviour( self.proxied_object )
|
192
|
+
).call( *arguments )
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class ContextualizedMethod # :nodoc:
|
197
|
+
attr_accessor :core
|
198
|
+
attr_accessor :behaviour_cache
|
199
|
+
|
200
|
+
attr_accessor_with_default_setter :pres, :posts, :arounds do
|
201
|
+
Array.new
|
202
|
+
end
|
203
|
+
|
204
|
+
def initialize( unbound_core_method )
|
205
|
+
self.core = unbound_core_method
|
206
|
+
self.behaviour_cache = {}
|
207
|
+
end
|
208
|
+
|
209
|
+
def + other_cm
|
210
|
+
new_cm = ContextualizedMethod.new( self.core )
|
211
|
+
new_cm.pres = self.pres + other_cm.pres
|
212
|
+
new_cm.posts = self.posts + other_cm.posts
|
213
|
+
new_cm.arounds = self.arounds + other_cm.arounds
|
214
|
+
new_cm
|
215
|
+
end
|
216
|
+
|
217
|
+
def behaviour( instance )
|
218
|
+
self.behaviour_cache[instance] ||=
|
219
|
+
self.send( self.behaviour_name, instance )
|
220
|
+
end
|
221
|
+
|
222
|
+
def behaviour_name
|
223
|
+
wrappers = []
|
224
|
+
wrappers << "pres" unless self.pres.empty?
|
225
|
+
wrappers << "arounds" unless self.arounds.empty?
|
226
|
+
wrappers << "posts" unless self.posts.empty?
|
227
|
+
|
228
|
+
"behaviour_" + ( wrappers.empty? ? "without_wrappers" :
|
229
|
+
"with_" + wrappers.join( "_and_" ) )
|
230
|
+
end
|
231
|
+
|
232
|
+
def behaviour_without_wrappers( instance )
|
233
|
+
self.core.bind( instance )
|
234
|
+
end
|
235
|
+
|
236
|
+
def behaviour_with_pres( instance )
|
237
|
+
combined_pres = self.combine_pres( instance )
|
238
|
+
bound_core = self.bind_core( instance )
|
239
|
+
|
240
|
+
lambda do | *arguments |
|
241
|
+
nature = MethodNature.new( arguments, nil, false )
|
242
|
+
|
243
|
+
combined_pres.call( nature )
|
244
|
+
unless nature.break
|
245
|
+
bound_core.call( *nature.arguments )
|
246
|
+
else
|
247
|
+
nature.return_value
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def behaviour_with_posts( instance )
|
253
|
+
bound_core = self.bind_core( instance )
|
254
|
+
combined_posts = self.combine_posts( instance )
|
255
|
+
|
256
|
+
lambda do | *arguments |
|
257
|
+
nature = MethodNature.new( arguments, nil, false )
|
258
|
+
|
259
|
+
nature.return_value = bound_core.call( *arguments )
|
260
|
+
combined_posts.call( nature )
|
261
|
+
|
262
|
+
nature.return_value
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def behaviour_with_pres_and_posts( instance )
|
267
|
+
combined_pres = self.combine_pres( instance )
|
268
|
+
bound_core = self.bind_core( instance )
|
269
|
+
combined_posts = self.combine_posts( instance )
|
270
|
+
|
271
|
+
lambda do | *arguments |
|
272
|
+
nature = MethodNature.new( arguments, nil, false )
|
273
|
+
|
274
|
+
combined_pres.call( nature )
|
275
|
+
unless nature.break
|
276
|
+
nature.return_value = bound_core.call( *nature.arguments )
|
277
|
+
combined_posts.call( nature )
|
278
|
+
end
|
279
|
+
nature.return_value
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def behaviour_with_arounds( instance )
|
284
|
+
bound_core = self.bind_core( instance )
|
285
|
+
bound_arounds = self.bind_arounds( instance )
|
286
|
+
|
287
|
+
lambda do | *arguments |
|
288
|
+
working_arounds = bound_arounds.clone
|
289
|
+
nature = MethodNature.new( arguments, nil, false)
|
290
|
+
nature.block = around_block( nature, working_arounds, bound_core )
|
291
|
+
|
292
|
+
catch( :break_in_around ) do
|
293
|
+
working_arounds.shift.call( nature )
|
294
|
+
end
|
295
|
+
nature.return_value
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def behaviour_with_pres_and_arounds( instance )
|
300
|
+
combined_pres = self.combine_pres( instance )
|
301
|
+
bound_core = self.bind_core( instance )
|
302
|
+
bound_arounds = self.bind_arounds( instance )
|
303
|
+
|
304
|
+
lambda do | *arguments |
|
305
|
+
nature = MethodNature.new( arguments, nil, false )
|
306
|
+
|
307
|
+
combined_pres.call( nature )
|
308
|
+
unless nature.break
|
309
|
+
working_arounds = bound_arounds.clone
|
310
|
+
nature.block = around_block( nature, working_arounds, bound_core )
|
311
|
+
catch( :break_in_around ) do
|
312
|
+
working_arounds.shift.call( nature )
|
313
|
+
end
|
314
|
+
end
|
315
|
+
nature.return_value
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def behaviour_with_arounds_and_posts( instance )
|
320
|
+
bound_core = self.bind_core( instance )
|
321
|
+
bound_arounds = self.bind_arounds( instance )
|
322
|
+
combined_posts = self.combine_posts( instance )
|
323
|
+
|
324
|
+
lambda do | *arguments |
|
325
|
+
working_arounds = bound_arounds.clone
|
326
|
+
nature = MethodNature.new( arguments, nil, false,
|
327
|
+
around_block( nature, working_arounds, bound_core ) )
|
328
|
+
|
329
|
+
catch( :break_in_around ) do
|
330
|
+
working_arounds.shift.call( nature )
|
331
|
+
end
|
332
|
+
combinded_posts.call( nature ) unless nature.break
|
333
|
+
|
334
|
+
nature.return_value
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def behaviour_with_pres_and_arounds_and_posts( instance )
|
339
|
+
combined_pres = self.combine_pres( instance )
|
340
|
+
bound_core = self.bind_core( instance )
|
341
|
+
bound_arounds = self.bind_arounds( instance )
|
342
|
+
combined_posts = self.combine_posts( instance )
|
343
|
+
|
344
|
+
lambda do | *arguments |
|
345
|
+
nature = MethodNature.new( arguments, nil, false )
|
346
|
+
|
347
|
+
combined_pres.call( nature )
|
348
|
+
unless nature.break
|
349
|
+
working_arounds = bound_arounds.clone
|
350
|
+
nature.block = around_block( nature, working_arounds, bound_core )
|
351
|
+
catch( :break_in_around ) do
|
352
|
+
working_arounds.shift.call( nature )
|
353
|
+
end
|
354
|
+
unless nature.break
|
355
|
+
combined_posts.call( nature )
|
356
|
+
end
|
357
|
+
end
|
358
|
+
nature.return_value
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
|
363
|
+
# helpers
|
364
|
+
def combine_pres( instance )
|
365
|
+
bound_pres = self.pres.collect { | p | p.bind( instance ) }
|
366
|
+
lambda do | nature |
|
367
|
+
bound_pres.each do | bound_pre |
|
368
|
+
bound_pre.call( nature )
|
369
|
+
break if nature.break
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def bind_arounds( instance )
|
375
|
+
self.arounds.collect { | a | a.bind( instance ) }
|
376
|
+
end
|
377
|
+
|
378
|
+
def around_block( nature, bound_arounds, bound_core )
|
379
|
+
lambda do
|
380
|
+
unless bound_arounds.empty?
|
381
|
+
bound_arounds.shift.call( nature )
|
382
|
+
throw( :break_in_around ) if nature.break
|
383
|
+
else
|
384
|
+
nature.return_value = bound_core.call( *nature.arguments )
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def bind_core( instance )
|
390
|
+
self.core.bind( instance )
|
391
|
+
end
|
392
|
+
|
393
|
+
def combine_posts( instance )
|
394
|
+
bound_posts = self.posts.collect { | p | p.bind( instance ) }
|
395
|
+
lambda do | nature |
|
396
|
+
bound_posts.reverse.each do | bound_post |
|
397
|
+
bound_post.call( nature )
|
398
|
+
break if nature.break
|
399
|
+
end
|
400
|
+
nature.return_value
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
data/lib/contextr/version.rb
CHANGED
data/lib/core_ext/class.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
# ContextR extends Class to allow the definition of context-dependent behaviour.
|
2
|
+
#
|
3
|
+
# After registering a layer within in class, it is accessable via its name,
|
4
|
+
# to add behaviour to methods.
|
1
5
|
class Class
|
6
|
+
# register a layer to be used within a class body
|
2
7
|
def layer( *layer_keys )
|
3
8
|
layer_keys.each do | layer_key |
|
4
9
|
layer_key = layer_key.to_s.downcase.to_sym
|
@@ -15,7 +20,7 @@ class Class
|
|
15
20
|
end
|
16
21
|
|
17
22
|
protected
|
18
|
-
def define_private_class_method( symbol, &block )
|
23
|
+
def define_private_class_method( symbol, &block ) # :nodoc:
|
19
24
|
(class << self; self; end).instance_eval do
|
20
25
|
define_method( symbol, block )
|
21
26
|
private symbol
|
data/lib/core_ext/module.rb
CHANGED
@@ -1,10 +1,46 @@
|
|
1
1
|
class Module
|
2
|
+
# returns the name without namespace prefixes
|
3
|
+
#
|
4
|
+
# module A
|
5
|
+
# module B
|
6
|
+
# module C
|
7
|
+
# end
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
# module C
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# A::B::C.name # => "A::B::C"
|
14
|
+
# C.name # => "C"
|
15
|
+
# A::B::C.namespace_free_name # => "C"
|
16
|
+
# C.namespace_free_name # => "C"
|
17
|
+
#
|
2
18
|
def namespace_free_name
|
3
19
|
self.name.match( /(\w*?)$/ )[1]
|
4
20
|
end
|
5
21
|
|
6
|
-
|
7
|
-
|
22
|
+
# allows the definition of an attr_accessor with a setter, that is used
|
23
|
+
# to set the instance variable if it is accessed before set.
|
24
|
+
#
|
25
|
+
# class A
|
26
|
+
# attr_accessor_with_default_setter :first_access { Time.now }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# A.new.first_access # => Wed May 09 18:23:36 0200 2007
|
30
|
+
#
|
31
|
+
# a1 = A.new
|
32
|
+
# a1.first_access # => Wed May 09 18:23:38 0200 2007
|
33
|
+
# a1.first_access # => Wed May 09 18:23:38 0200 2007
|
34
|
+
#
|
35
|
+
# a2 = A.new
|
36
|
+
# a2.first_access = Time.now - 10.days
|
37
|
+
# a2.first_access # => Sun Apr 29 18:23:40 0200 2007
|
38
|
+
#
|
39
|
+
# :call-seq:
|
40
|
+
# attr_accessor_with_default_setter(symbol, ...) { ... }
|
41
|
+
#
|
42
|
+
def attr_accessor_with_default_setter( *syms )
|
43
|
+
raise 'Default value in block required' unless block_given?
|
8
44
|
syms.each do | sym |
|
9
45
|
module_eval do
|
10
46
|
attr_writer( sym )
|
@@ -16,7 +52,7 @@ class Module
|
|
16
52
|
if instance_variables.include? "@#{sym}"
|
17
53
|
instance_variable_get( "@#{sym}" )
|
18
54
|
else
|
19
|
-
instance_variable_set( "@#{sym}",
|
55
|
+
instance_variable_set( "@#{sym}", yield )
|
20
56
|
end
|
21
57
|
end
|
22
58
|
end
|