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.

Custom namespace implementations [ruby]
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:

Configuring an interceptor [ruby]
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.

Advanced configuration of an interceptor [ruby]
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.

Custom DefinitionContext example [ruby]
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"