needle 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/LICENSE-BSD +27 -0
- data/doc/LICENSE-GPL +280 -0
- data/doc/LICENSE-RUBY +56 -0
- data/doc/README +70 -0
- data/doc/manual/chapter.erb +18 -0
- data/doc/manual/index.erb +29 -0
- data/doc/manual/manual.css +192 -0
- data/doc/manual/manual.rb +240 -0
- data/doc/manual/manual.yml +48 -0
- data/doc/manual/page.erb +86 -0
- data/doc/manual/parts/01_license.txt +5 -0
- data/doc/manual/parts/01_support.txt +1 -0
- data/doc/manual/parts/01_use_cases.txt +141 -0
- data/doc/manual/parts/01_what_is_needle.txt +1 -0
- data/doc/manual/parts/02_creating.txt +9 -0
- data/doc/manual/parts/02_namespaces.txt +47 -0
- data/doc/manual/parts/02_overview.txt +3 -0
- data/doc/manual/parts/02_services.txt +44 -0
- data/doc/manual/tutorial.erb +30 -0
- data/doc/manual-html/chapter-1.html +354 -0
- data/doc/manual-html/chapter-2.html +310 -0
- data/doc/manual-html/chapter-3.html +154 -0
- data/doc/manual-html/chapter-4.html +154 -0
- data/doc/manual-html/chapter-5.html +154 -0
- data/doc/manual-html/chapter-6.html +154 -0
- data/doc/manual-html/chapter-7.html +154 -0
- data/doc/manual-html/index.html +177 -0
- data/doc/manual-html/manual.css +192 -0
- data/lib/needle/container.rb +318 -0
- data/lib/needle/errors.rb +32 -0
- data/lib/needle/include-exclude.rb +116 -0
- data/lib/needle/interceptor-chain.rb +162 -0
- data/lib/needle/interceptor.rb +189 -0
- data/lib/needle/log-factory.rb +207 -0
- data/lib/needle/logger.rb +161 -0
- data/lib/needle/logging-interceptor.rb +62 -0
- data/lib/needle/models/prototype-deferred.rb +41 -0
- data/lib/needle/models/prototype.rb +39 -0
- data/lib/needle/models/proxy.rb +84 -0
- data/lib/needle/models/singleton-deferred.rb +57 -0
- data/lib/needle/models/singleton.rb +56 -0
- data/lib/needle/models.rb +44 -0
- data/lib/needle/registry.rb +110 -0
- data/lib/needle/service-point.rb +109 -0
- data/lib/needle/version.rb +28 -0
- data/lib/needle.rb +54 -0
- data/test/ALL-TESTS.rb +21 -0
- data/test/models/tc_prototype.rb +53 -0
- data/test/models/tc_prototype_deferred.rb +54 -0
- data/test/models/tc_proxy.rb +51 -0
- data/test/models/tc_singleton.rb +53 -0
- data/test/models/tc_singleton_deferred.rb +54 -0
- data/test/tc_container.rb +246 -0
- data/test/tc_interceptor.rb +92 -0
- data/test/tc_interceptor_chain.rb +181 -0
- data/test/tc_logger.rb +181 -0
- data/test/tc_models.rb +44 -0
- data/test/tc_registry.rb +34 -0
- data/test/tc_service_point.rb +100 -0
- metadata +107 -0
@@ -0,0 +1,318 @@
|
|
1
|
+
#--
|
2
|
+
# =============================================================================
|
3
|
+
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# This source file is distributed as part of the Needle dependency injection
|
7
|
+
# library for Ruby. This file (and the library as a whole) may be used only as
|
8
|
+
# allowed by either the BSD license, or the Ruby license (or, by association
|
9
|
+
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
|
10
|
+
# distribution for the texts of these licenses.
|
11
|
+
# -----------------------------------------------------------------------------
|
12
|
+
# needle website : http://needle.rubyforge.org
|
13
|
+
# project website: http://rubyforge.org/projects/needle
|
14
|
+
# =============================================================================
|
15
|
+
#++
|
16
|
+
|
17
|
+
require 'needle/errors'
|
18
|
+
require 'needle/interceptor'
|
19
|
+
require 'needle/service-point'
|
20
|
+
|
21
|
+
module Needle
|
22
|
+
|
23
|
+
# The container is the heart of Needle's model. Every Container instance is
|
24
|
+
# a miniature registry, and is really a namespace separate from every other
|
25
|
+
# Container instance. Service lookups inside of a container always look in
|
26
|
+
# +self+ first, and if not found, they then look in their parent container,
|
27
|
+
# recursively.
|
28
|
+
#
|
29
|
+
# You will rarely need to instantiate a Container directly. Instead, use the
|
30
|
+
# Container#namespace method to create new containers.
|
31
|
+
class Container
|
32
|
+
|
33
|
+
# This class is used by the #register! and #namespace! methods to allow
|
34
|
+
# an +instance_eval+'d block to create new service points simply by
|
35
|
+
# invoking imaginary methods. It is basically an empty shell, with almost
|
36
|
+
# all of the builtin methods removed from it. (This allows services like
|
37
|
+
# "hash" and "print" to be defined, where they would normally conflict
|
38
|
+
# with the Kernel methods of the same name.)
|
39
|
+
class RegistrationContext
|
40
|
+
( private_instance_methods +
|
41
|
+
protected_instance_methods +
|
42
|
+
public_instance_methods -
|
43
|
+
[ "instance_eval", "__id__", "__send__", "initialize", "remove_const",
|
44
|
+
"method_missing", "inspect" ]
|
45
|
+
).
|
46
|
+
each { |m| undef_method m }
|
47
|
+
|
48
|
+
# Create a new RegistrationContext that wraps the given container. All
|
49
|
+
# operations performed on this context will be delegated to the
|
50
|
+
# container.
|
51
|
+
def initialize( container )
|
52
|
+
@container = container
|
53
|
+
end
|
54
|
+
|
55
|
+
# Delegate to Container#intercept.
|
56
|
+
def intercept( name )
|
57
|
+
@container.intercept( name )
|
58
|
+
end
|
59
|
+
|
60
|
+
# Delegate to Container#namespace!. Note that this is an exception to the
|
61
|
+
# general rule regarding bang methods, since this (non-bang) method
|
62
|
+
# delegates to a bang-method. However, because this is typically called
|
63
|
+
# within the context of a bang method (like Container#register!), it felt
|
64
|
+
# redundant to have the bang here as well. Disagree? Let me know.
|
65
|
+
def namespace( *parms, &block )
|
66
|
+
@container.namespace!( *parms, &block )
|
67
|
+
end
|
68
|
+
|
69
|
+
# Any method invocation with no block and no parameters is interpreted to
|
70
|
+
# be a service reference on the wrapped container, and delegates to
|
71
|
+
# Container#[]. If the block is not given but the args are not empty, a
|
72
|
+
# NoMethodError will be raised.
|
73
|
+
#
|
74
|
+
# If a block is given, this delegates to Container#register, leaving
|
75
|
+
# all parameters in place.
|
76
|
+
def method_missing( sym, *args, &block )
|
77
|
+
if block.nil?
|
78
|
+
if args.empty?
|
79
|
+
@container[sym]
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
else
|
84
|
+
@container.register( sym, *args, &block )
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# The container that contains this container. This will be +nil+ for
|
90
|
+
# the root of a hierarchy (see Registry).
|
91
|
+
attr_reader :parent
|
92
|
+
|
93
|
+
# The name of this container. May be +nil+.
|
94
|
+
attr_reader :name
|
95
|
+
|
96
|
+
# Create a new empty container with the given parent and name.
|
97
|
+
def initialize( parent=nil, name=nil )
|
98
|
+
@root = nil
|
99
|
+
|
100
|
+
@name = name
|
101
|
+
@parent = parent
|
102
|
+
@service_points = Hash.new
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the root of the current hierarchy. If the container is the
|
106
|
+
# root, returns self, otherwise calls Container#root on its parent.
|
107
|
+
# The value is cached for future reference.
|
108
|
+
def root
|
109
|
+
return @root if @root
|
110
|
+
return self if parent.nil?
|
111
|
+
@root = parent.root
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return the fully qualified name of this container, which is the
|
115
|
+
# container's name and all parent's names up to the root container,
|
116
|
+
# catenated together with dot characters, i.e., "one.two.three".
|
117
|
+
def fullname
|
118
|
+
return @name.to_s unless @parent
|
119
|
+
"#{@parent.fullname}.#{@name}"
|
120
|
+
end
|
121
|
+
|
122
|
+
# Register the named service with the container. When the service is
|
123
|
+
# requested (with Container#[]), the associated callback will be used
|
124
|
+
# to construct it.
|
125
|
+
#
|
126
|
+
# Usage:
|
127
|
+
#
|
128
|
+
# container.register( :calc, :model=>:prototype ) do |c|
|
129
|
+
# Calc.new( c.operations )
|
130
|
+
# end
|
131
|
+
def register( name, opts={}, &callback )
|
132
|
+
raise ArgumentError, "expect block" unless callback
|
133
|
+
name = name.to_s.intern unless name.is_a?( Symbol )
|
134
|
+
@service_points[ name ] =
|
135
|
+
ServicePoint.new( self, name, opts, &callback )
|
136
|
+
end
|
137
|
+
|
138
|
+
# Create a new RegistrationContext around the container, and then evaluate
|
139
|
+
# the block within the new context instance (via +instance_eval+).
|
140
|
+
#
|
141
|
+
# Usage:
|
142
|
+
#
|
143
|
+
# container.register! do
|
144
|
+
# calc( :model => :prototype ) { Calc.new( operations ) }
|
145
|
+
# end
|
146
|
+
def register!( &block )
|
147
|
+
raise ArgumentError, "block expected" unless block
|
148
|
+
ctx = RegistrationContext.new( self )
|
149
|
+
ctx.instance_eval( &block )
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create a new namespace within the container. Each parameter ought to
|
154
|
+
# be the name of a namespace. If more than one parameter is given,
|
155
|
+
# each one represents the name of a new namespace to create inside of
|
156
|
+
# the last-created namespace, unless that namespace already exists.
|
157
|
+
# This makes it work analogously to FileUtils#mkdir_p (creating new
|
158
|
+
# namespaces along a path of namespaces, as needed).
|
159
|
+
#
|
160
|
+
# If a block is given, the latest namespace created is yielded to the
|
161
|
+
# block.
|
162
|
+
#
|
163
|
+
# The last parameter may be a Hash, in which case it is used to specify
|
164
|
+
# options describing how the namespace should be created.
|
165
|
+
#
|
166
|
+
# For the curious, namespaces are simply services that are implemented
|
167
|
+
# by Container. The two statements are really identical:
|
168
|
+
#
|
169
|
+
# container.namespace( :calc )
|
170
|
+
# container.register( :calc ) { |c| Needle::Container.new( c, :calc ) }
|
171
|
+
#
|
172
|
+
# Usage:
|
173
|
+
#
|
174
|
+
# container.namespace( :calc, :operations ) do |op|
|
175
|
+
# op.register( :add ) { Adder.new }
|
176
|
+
# ...
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# adder = container.calc.operations.add
|
180
|
+
def namespace( *parms )
|
181
|
+
opts = {}
|
182
|
+
opts = parms.pop if parms.last.is_a?( Hash )
|
183
|
+
|
184
|
+
if parms.length < 1
|
185
|
+
raise ArgumentError, "you must specify at least one name"
|
186
|
+
end
|
187
|
+
|
188
|
+
container = self
|
189
|
+
parms.each do |parm|
|
190
|
+
unless container.has_key?( parm )
|
191
|
+
container.register( parm, opts ) { |c| Container.new( c, parm ) }
|
192
|
+
end
|
193
|
+
|
194
|
+
container = container[parm]
|
195
|
+
end
|
196
|
+
|
197
|
+
yield container if block_given?
|
198
|
+
end
|
199
|
+
|
200
|
+
# Create a new namespace within the container. Each parameter ought to
|
201
|
+
# be the name of a namespace. If more than one parameter is given,
|
202
|
+
# each one represents the name of a new namespace to create inside of
|
203
|
+
# the last-created namespace, unless that namespace already exists.
|
204
|
+
# This makes it work analogously to FileUtils#mkdir_p (creating new
|
205
|
+
# namespaces along a path of namespaces, as needed).
|
206
|
+
#
|
207
|
+
# The last parameter may be a Hash, in which case it is used to specify
|
208
|
+
# options describing how the namespace should be created.
|
209
|
+
#
|
210
|
+
# The block is passed to the Container#register! method of the last
|
211
|
+
# namespace created.
|
212
|
+
#
|
213
|
+
# For the curious, namespaces are simply services that are implemented
|
214
|
+
# by Container. The two statements are really identical:
|
215
|
+
#
|
216
|
+
# container.namespace( :calc )
|
217
|
+
# container.register( :calc ) { |c| Needle::Container.new( c, :calc ) }
|
218
|
+
#
|
219
|
+
# Usage:
|
220
|
+
#
|
221
|
+
# container.namespace!( :calc, :operations ) do
|
222
|
+
# add { Adder.new }
|
223
|
+
# ...
|
224
|
+
# end
|
225
|
+
#
|
226
|
+
# adder = container.calc.operations.add
|
227
|
+
def namespace!( *parms, &block )
|
228
|
+
raise ArgumentError, "block expected" unless block
|
229
|
+
namespace( *parms ) { |ns| ns.register!( &block ) }
|
230
|
+
end
|
231
|
+
|
232
|
+
# Describe a new interceptor to use that will intercept method calls
|
233
|
+
# on the named service. This method returns a new Interceptor instance,
|
234
|
+
# which can be used directly to configure the behavior of the interceptor.
|
235
|
+
#
|
236
|
+
# Usage:
|
237
|
+
#
|
238
|
+
# container.intercept( :calc ).with { |c| c.logging_interceptor }
|
239
|
+
def intercept( name )
|
240
|
+
point = find_definition( name )
|
241
|
+
raise ServiceNotFound, "#{fullname}.#{name}" unless point
|
242
|
+
|
243
|
+
interceptor = Interceptor.new
|
244
|
+
point.interceptor interceptor
|
245
|
+
|
246
|
+
interceptor
|
247
|
+
end
|
248
|
+
|
249
|
+
# Searches the current container and its ancestors for the named service.
|
250
|
+
# If found, the service point (the definition of that service) is returned,
|
251
|
+
# otherwise +nil+ is returned.
|
252
|
+
def find_definition( name )
|
253
|
+
point = @service_points[ name ]
|
254
|
+
point = @parent.find_definition( name ) if @parent unless point
|
255
|
+
point
|
256
|
+
end
|
257
|
+
|
258
|
+
# Retrieves the named service, if it exists. Ancestors are searched if the
|
259
|
+
# service is not defined by the current container (see #find_definition).
|
260
|
+
# If the named service does not exist, ServiceNotFound is raised.
|
261
|
+
#
|
262
|
+
# Note that this returns the instantiated service, not the service point.
|
263
|
+
def []( name )
|
264
|
+
point = find_definition( name )
|
265
|
+
raise ServiceNotFound, "#{fullname}.#{name}" unless point
|
266
|
+
|
267
|
+
point.instance
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns +true+ if this container includes a service point with the given
|
271
|
+
# name. Returns +false+ otherwise.
|
272
|
+
def has_key?( name )
|
273
|
+
@service_points.has_key?( name )
|
274
|
+
end
|
275
|
+
|
276
|
+
# Returns +true+ if this container <em>or any ancestor</em> includes a
|
277
|
+
# service point with the given name. Returns +false+ otherwise.
|
278
|
+
def knows_key?( name )
|
279
|
+
return true if has_key?( name )
|
280
|
+
return @parent.knows_key?( name ) if @parent
|
281
|
+
false
|
282
|
+
end
|
283
|
+
|
284
|
+
# Return an array of the names of all service points in this container.
|
285
|
+
def keys
|
286
|
+
@service_points.keys
|
287
|
+
end
|
288
|
+
|
289
|
+
# As a convenience for accessing services, this delegates any message
|
290
|
+
# sent to the container (which has no parameters and no block) to
|
291
|
+
# Container#[]. Note that this incurs slightly more overhead than simply
|
292
|
+
# calling Container#[] directly, so if performance is an issue, you should
|
293
|
+
# avoid this approach.
|
294
|
+
#
|
295
|
+
# Usage:
|
296
|
+
#
|
297
|
+
# container.register( :add ) { Adder.new }
|
298
|
+
# p container.add == container[:add] # => true
|
299
|
+
#
|
300
|
+
def method_missing( sym, *args, &block )
|
301
|
+
if block.nil? && args.empty? && knows_key?( sym )
|
302
|
+
self[sym]
|
303
|
+
else
|
304
|
+
super
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns true if this container responds to the given message, or if it
|
309
|
+
# explicitly contains a service with the given name (see #has_key?). In
|
310
|
+
# this case, #has_key? is used instead of #knows_key? so that subcontainers
|
311
|
+
# may be used as proper hashes by their parents.
|
312
|
+
def respond_to?( sym )
|
313
|
+
@service_points.has_key?( sym ) || super
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#--
|
2
|
+
# =============================================================================
|
3
|
+
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# This source file is distributed as part of the Needle dependency injection
|
7
|
+
# library for Ruby. This file (and the library as a whole) may be used only as
|
8
|
+
# allowed by either the BSD license, or the Ruby license (or, by association
|
9
|
+
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
|
10
|
+
# distribution for the texts of these licenses.
|
11
|
+
# -----------------------------------------------------------------------------
|
12
|
+
# needle website : http://needle.rubyforge.org
|
13
|
+
# project website: http://rubyforge.org/projects/needle
|
14
|
+
# =============================================================================
|
15
|
+
#++
|
16
|
+
|
17
|
+
module Needle
|
18
|
+
|
19
|
+
# The base class for all Needle-specific errors.
|
20
|
+
class NeedleError < StandardError; end
|
21
|
+
|
22
|
+
# Raised when a requested service could not be located.
|
23
|
+
class ServiceNotFound < NeedleError; end
|
24
|
+
|
25
|
+
# Raised when there was an error configuring an interceptor.
|
26
|
+
class InterceptorConfigurationError < NeedleError; end
|
27
|
+
|
28
|
+
# Raised to denote a condition that should never occur. If this gets
|
29
|
+
# raised, it is Needle's fault, not the consumer's.
|
30
|
+
class Bug < NeedleError; end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
#--
|
2
|
+
# =============================================================================
|
3
|
+
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# This source file is distributed as part of the Needle dependency injection
|
7
|
+
# library for Ruby. This file (and the library as a whole) may be used only as
|
8
|
+
# allowed by either the BSD license, or the Ruby license (or, by association
|
9
|
+
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
|
10
|
+
# distribution for the texts of these licenses.
|
11
|
+
# -----------------------------------------------------------------------------
|
12
|
+
# needle website : http://needle.rubyforge.org
|
13
|
+
# project website: http://rubyforge.org/projects/needle
|
14
|
+
# =============================================================================
|
15
|
+
#++
|
16
|
+
|
17
|
+
require 'needle/errors'
|
18
|
+
|
19
|
+
module Needle
|
20
|
+
|
21
|
+
# A simple structure for representing a single include/exclude pattern.
|
22
|
+
IncludeExcludePattern = Struct.new( :name, :comparitor, :arity )
|
23
|
+
|
24
|
+
# A module encapsulating the functionality of a service with include/exclude
|
25
|
+
# functionality. Such functionality involves a the ability to specify a
|
26
|
+
# pair of include and exclude arrays, each of which must be an array of
|
27
|
+
# method names that should be included or excluded from some kind of
|
28
|
+
# processing.
|
29
|
+
module IncludeExclude
|
30
|
+
|
31
|
+
# This is the regular expression for parsing elements in an include or
|
32
|
+
# exclude array.
|
33
|
+
PATTERN = /^
|
34
|
+
(.*?) (?# this matches the method name pattern)
|
35
|
+
(?: (?# begin optional arity section)
|
36
|
+
\( (?# begin parenthesized section)
|
37
|
+
([<=>])? (?# optional comparator character)
|
38
|
+
(\d+) (?# arity specification)
|
39
|
+
\) (?# end parenthesized section)
|
40
|
+
)? (?# end optional arity section)
|
41
|
+
$/x
|
42
|
+
|
43
|
+
# This is a utility function for converting an array of strings
|
44
|
+
# representing method name patterns, into an array of
|
45
|
+
# IncludeExcludePattern instances.
|
46
|
+
def build_map( array )
|
47
|
+
( array || [] ).map do |pattern|
|
48
|
+
unless pattern =~ PATTERN
|
49
|
+
raise InterceptorConfigurationError,
|
50
|
+
"invalid logging interceptor method pattern: #{pattern.inspect}"
|
51
|
+
end
|
52
|
+
|
53
|
+
name = $1
|
54
|
+
comparitor = $2
|
55
|
+
arity = ( $3 || -1 ).to_i
|
56
|
+
|
57
|
+
comparitor ||= ">" if arity < 0
|
58
|
+
comparitor ||= "="
|
59
|
+
|
60
|
+
IncludeExcludePattern.new( Regexp.new( "^" + name + "$" ),
|
61
|
+
comparitor,
|
62
|
+
arity )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
private :build_map
|
66
|
+
|
67
|
+
# Returns +false+ if the given context object "matches" any of the
|
68
|
+
# exclude patterns without matching any of the include patterns.
|
69
|
+
# The context object must respond to the <tt>:sym</tt> and
|
70
|
+
# <tt>:args</tt> messages, where <tt>:sym</tt> is a symbol identifying
|
71
|
+
# the method being matched, and <tt>:args</tt> is an array of
|
72
|
+
# arguments that will be sent to that method.
|
73
|
+
def match( context )
|
74
|
+
match = true
|
75
|
+
|
76
|
+
@excludes.each do |pattern|
|
77
|
+
if match_pattern( context, pattern )
|
78
|
+
match = false
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
unless match
|
84
|
+
@includes.each do |pattern|
|
85
|
+
if match_pattern( context, pattern )
|
86
|
+
match = true
|
87
|
+
break
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
return match
|
93
|
+
end
|
94
|
+
private :match
|
95
|
+
|
96
|
+
# Returns +true+ if the given context matches the given pattern, and
|
97
|
+
# +false+ otherwise.
|
98
|
+
def match_pattern( context, pattern )
|
99
|
+
if context.sym.to_s =~ pattern.name
|
100
|
+
case pattern.comparitor
|
101
|
+
when "<"
|
102
|
+
return context.args.length < pattern.arity
|
103
|
+
when ">"
|
104
|
+
return context.args.length > pattern.arity
|
105
|
+
when "="
|
106
|
+
return context.args.length == pattern.arity
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
private :match_pattern
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
#--
|
2
|
+
# =============================================================================
|
3
|
+
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# This source file is distributed as part of the Needle dependency injection
|
7
|
+
# library for Ruby. This file (and the library as a whole) may be used only as
|
8
|
+
# allowed by either the BSD license, or the Ruby license (or, by association
|
9
|
+
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
|
10
|
+
# distribution for the texts of these licenses.
|
11
|
+
# -----------------------------------------------------------------------------
|
12
|
+
# needle website : http://needle.rubyforge.org
|
13
|
+
# project website: http://rubyforge.org/projects/needle
|
14
|
+
# =============================================================================
|
15
|
+
#++
|
16
|
+
|
17
|
+
require 'needle/errors'
|
18
|
+
|
19
|
+
module Needle
|
20
|
+
|
21
|
+
# This module encapsulates the functionality for building interceptor chains.
|
22
|
+
module InterceptorChainBuilder
|
23
|
+
|
24
|
+
# The context of a method invocation. This is used in an interceptor chain
|
25
|
+
# to encapsulate the elements of the current invocation.
|
26
|
+
# sym: the name of the method being invoked
|
27
|
+
# args: the argument list being passed to the method
|
28
|
+
# block: the reference to the block attached to the method invocation
|
29
|
+
# data: a hash that may be used by clients for storing arbitrary data in
|
30
|
+
# the context.
|
31
|
+
InvocationContext = Struct.new( :sym, :args, :block, :data )
|
32
|
+
|
33
|
+
# A single element in an interceptor chain. Each interceptor object is
|
34
|
+
# wrapped in an instance of one of these. Calling #process_next on a given
|
35
|
+
# chain element, invokes the #process method on the corresponding
|
36
|
+
# interceptor, with the next element in the chain being passed in.
|
37
|
+
class InterceptorChainElement
|
38
|
+
|
39
|
+
# Create a new InterceptorChainElement that wraps the given interceptor.
|
40
|
+
def initialize( interceptor )
|
41
|
+
@interceptor = interceptor
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set the next element in the interceptor chain to the given object. This
|
45
|
+
# must be either an InterceptorChainElement instance of a
|
46
|
+
# ProxyObjectChainElement instance.
|
47
|
+
def next=( next_obj )
|
48
|
+
@next_obj = next_obj
|
49
|
+
end
|
50
|
+
|
51
|
+
# Invokes the #process method of the interceptor encapsulated by this
|
52
|
+
# object, with the _next_ element in the chain being passed to it.
|
53
|
+
def process_next( context )
|
54
|
+
if @next_obj.nil?
|
55
|
+
raise Bug,
|
56
|
+
"[BUG] interceptor chain should always terminate with proxy"
|
57
|
+
end
|
58
|
+
@interceptor.process( @next_obj, context )
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Encapsulates the end of an interceptor chain, which is the actual object
|
64
|
+
# being affected.
|
65
|
+
class ProxyObjectChainElement
|
66
|
+
|
67
|
+
# Create a new ProxyObjectChainElement that wraps the given object.
|
68
|
+
def initialize( obj )
|
69
|
+
@obj = obj
|
70
|
+
end
|
71
|
+
|
72
|
+
# Invoke the method represented by the context on the wrapped object.
|
73
|
+
def process_next( context )
|
74
|
+
@obj.__send__( context.sym, *context.args, &context.block )
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# This is just a trivial proxy class that is used to wrap a service
|
80
|
+
# before the interceptors are applied to it. This additional level of
|
81
|
+
# abstraction prevents the need for mangling the names of the service's
|
82
|
+
# methods, and also offers those applications that need it the ability
|
83
|
+
# to invoke methods of the service without going through the interceptors.
|
84
|
+
#
|
85
|
+
# The proxy will be decorated with dynamically appended methods by the
|
86
|
+
# InterceptorChainBuilder#build method.
|
87
|
+
class InterceptedServiceProxy
|
88
|
+
|
89
|
+
# Create a new InterceptedServiceProxy that wraps the given interceptor
|
90
|
+
# chain.
|
91
|
+
def initialize( chain )
|
92
|
+
@chain = chain
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# This will apply the given interceptors to the given service by first
|
98
|
+
# ordering the interceptors based on their relative priorities,
|
99
|
+
# and then dynamically modifying the service's methods so that the chain
|
100
|
+
# of interceptors sits in front of each of them.
|
101
|
+
#
|
102
|
+
# The modified service is returned.
|
103
|
+
def build( point, service, interceptors )
|
104
|
+
return service if interceptors.nil? || interceptors.empty?
|
105
|
+
|
106
|
+
ordered_list =
|
107
|
+
interceptors.sort { |a,b|
|
108
|
+
a.options[:priority] <=> b.options[:priority] }
|
109
|
+
|
110
|
+
chain = ProxyObjectChainElement.new( service )
|
111
|
+
|
112
|
+
ordered_list.reverse.each do |interceptor|
|
113
|
+
factory = interceptor.action.call( point.container )
|
114
|
+
instance = factory.new( point, interceptor.options )
|
115
|
+
element = InterceptorChainElement.new( instance )
|
116
|
+
element.next = chain
|
117
|
+
chain = element
|
118
|
+
end
|
119
|
+
|
120
|
+
# FIXME: should inherited methods of "Object" be interceptable?
|
121
|
+
methods_to_intercept = ( service.class.instance_methods( true ) -
|
122
|
+
Object.instance_methods +
|
123
|
+
service.class.instance_methods( false ) ).uniq
|
124
|
+
|
125
|
+
service = InterceptedServiceProxy.new( chain )
|
126
|
+
singleton = class << service; self; end
|
127
|
+
|
128
|
+
methods_to_intercept.each do |method|
|
129
|
+
next if method =~ /^__/
|
130
|
+
|
131
|
+
if singleton.instance_methods(false).include? method
|
132
|
+
singleton.send( :remove_method, method )
|
133
|
+
end
|
134
|
+
|
135
|
+
singleton.class_eval <<-EOF
|
136
|
+
def #{method}( *args, &block )
|
137
|
+
context = InvocationContext.new( :#{method}, args, block, Hash.new )
|
138
|
+
@chain.process_next( context )
|
139
|
+
end
|
140
|
+
EOF
|
141
|
+
end
|
142
|
+
|
143
|
+
# allow the interceptor to intercept methods not explicitly
|
144
|
+
# declared on the reciever.
|
145
|
+
if singleton.instance_methods(false).include? "method_missing"
|
146
|
+
singleton.send( :remove_method, :method_missing )
|
147
|
+
end
|
148
|
+
|
149
|
+
singleton.class_eval <<-EOF
|
150
|
+
def method_missing( sym, *args, &block )
|
151
|
+
context = InvocationContext.new( sym, args, block, Hash.new )
|
152
|
+
@chain.process_next( context )
|
153
|
+
end
|
154
|
+
EOF
|
155
|
+
|
156
|
+
return service
|
157
|
+
end
|
158
|
+
module_function :build
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|