9. Customizing Needle
9.1. Namespaces
By default, when you create a namespace in Needle, the namespace is registered as a service. The type of the service is determined by the :namespace_impl_factory
service, which (by default) returns the Needle::Container
class.
You can specify your own custom implementation for namespaces by registering your own :namespace_impl_factory
service. In fact, each namespace can have its own implementation of subnamespaces—just register a :namespace_impl_factory
in each one that you want to be specialized.
Here’s a contrived example. Suppose you want each namespace to keep track of the precise time that it was created.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class TimeTrackerNamespace < Needle::Container attr_reader :birth_date def initialize( *args ) super @birth_date = Time.now end end reg = Needle::Registry.new reg.register( :namespace_impl_factory ) { TimeTrackerNamespace } reg.namespace :hello p reg.hello.birth_date |
In general, you’ll be better off having your custom implementation extend Needle::Container
, although the only real requirement is that your implementation publish the same interface as the default namespace implementation.
9.2. Interceptors
When you attach an interceptor to a service, that new interceptor is wrapped in a definition object that includes various metadata about the interceptor, including its implementation, its priority, its name, and so forth. The implementation of this interceptor definition is determined by the value of the :interceptor_impl_factory
service, which by default returns Needle::Interceptor
.
It is this wrapper object that allows interceptor definitions to be done using method chaining:
reg.intercept( :foo ).with { ... }.with_options(...)
If you wish to add custom, domain-specific functionality to the interceptor wrapper, you can register your own implementation of the :interceptor_impl_factory
. Consider the following contrived example, where an “only_if” clause is given to determine when the interceptor should be invoked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class OnlyIfInterceptor < Needle::Interceptor def only_if( &block ) @only_if = block self end def action action_proc = super lambda do |chain,ctx| if @only_if.call( chain, ctx ) action_proc.call( chain, ctx ) else chain.process_next( ctx ) end end end end reg = Needle::Registry.new reg.register( :interceptor_impl_factory ) { OnlyIfInterceptor } reg.register( :foo ) { Bar.new } reg.intercept( :foo ). with { |c| c.logging_interceptor }. only_if { |ch,ctx| something_is_true( ch, ctx ) }. with_options(...) |
9.3. Contexts
A definition context is used when registering services using any of the #define
interfaces. For example, Container#define
yields an instance of a definition context to the given block, and Container#define!
uses the block in an instance_eval
on a definition context.
The default implementation used for definition contexts is defined by the :definition_context_factory
service. By default, this service returns Needle::DefinitionContext
, but you can specify your own definition context implementations by overriding this service. In fact, each namespace could have its own definition context implementation, if needed.
Consider the following contrived example, where you want to provide a convenient way to register services of type Hash.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class MyDefinitionContext < Needle::DefinitionContext def register_hash( name, opts={} ) this_container.register( name, opts ) { Hash.new } end end reg = Needle::Registry.new reg.register( :definition_context_factory ) { MyDefinitionContext } reg.define do |b| b.register_hash( :test1 ) b.register_hash( :test2 ) end reg.test1[:key] = "value" reg.test2[:foo] = "bar" |