needle 0.5.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.
- 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
|