File: interceptor-chain.rb

package info (click to toggle)
libneedle-ruby 1.2.0-2
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 1,436 kB
  • ctags: 886
  • sloc: ruby: 4,464; makefile: 52
file content (162 lines) | stat: -rw-r--r-- 5,972 bytes parent folder | download
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#--
# =============================================================================
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
# All rights reserved.
#
# This source file is distributed as part of the Needle dependency injection
# library for Ruby. This file (and the library as a whole) may be used only as
# allowed by either the BSD license, or the Ruby license (or, by association
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
# distribution for the texts of these licenses.
# -----------------------------------------------------------------------------
# needle website : http://needle.rubyforge.org
# project website: http://rubyforge.org/projects/needle
# =============================================================================
#++

require 'needle/errors'

module Needle

  # This module encapsulates the functionality for building interceptor chains.
  module InterceptorChainBuilder

    # The context of a method invocation.  This is used in an interceptor chain
    # to encapsulate the elements of the current invocation.
    # sym:   the name of the method being invoked
    # args:  the argument list being passed to the method
    # block: the reference to the block attached to the method invocation
    # data:  a hash that may be used by clients for storing arbitrary data in
    #        the context.
    InvocationContext = Struct.new( :sym, :args, :block, :data )

    # A single element in an interceptor chain.  Each interceptor object is
    # wrapped in an instance of one of these.  Calling #process_next on a given
    # chain element, invokes the #process method on the corresponding
    # interceptor, with the next element in the chain being passed in.
    class InterceptorChainElement

      # Create a new InterceptorChainElement that wraps the given interceptor.
      def initialize( interceptor )
        @interceptor = interceptor
      end

      # Set the next element in the interceptor chain to the given object.  This
      # must be either an InterceptorChainElement instance of a
      # ProxyObjectChainElement instance.
      def next=( next_obj )
        @next_obj = next_obj
      end

      # Invokes the #process method of the interceptor encapsulated by this
      # object, with the _next_ element in the chain being passed to it.
      def process_next( context )
        if @next_obj.nil?
          raise Bug,
            "[BUG] interceptor chain should always terminate with proxy"
        end
        @interceptor.process( @next_obj, context )
      end

    end

    # Encapsulates the end of an interceptor chain, which is the actual object
    # being affected.
    class ProxyObjectChainElement

      # Create a new ProxyObjectChainElement that wraps the given object.
      def initialize( obj )
        @obj = obj
      end

      # Invoke the method represented by the context on the wrapped object.
      def process_next( context )
        @obj.__send__( context.sym, *context.args, &context.block )
      end

    end

    # This is just a trivial proxy class that is used to wrap a service
    # before the interceptors are applied to it. This additional level of
    # abstraction prevents the need for mangling the names of the service's
    # methods, and also offers those applications that need it the ability
    # to invoke methods of the service without going through the interceptors.
    #
    # The proxy will be decorated with dynamically appended methods by the
    # InterceptorChainBuilder#build method.
    class InterceptedServiceProxy

      # Create a new InterceptedServiceProxy that wraps the given interceptor
      # chain.
      def initialize( chain )
        @chain = chain
      end

    end

    # This will apply the given interceptors to the given service by first
    # ordering the interceptors based on their relative priorities,
    # and then dynamically modifying the service's methods so that the chain
    # of interceptors sits in front of each of them.
    #
    # The modified service is returned.
    def build( point, service, interceptors )
      return service if interceptors.nil? || interceptors.empty?

      ordered_list =
        interceptors.sort { |a,b|
          a.options[:priority] <=> b.options[:priority] }

      chain = ProxyObjectChainElement.new( service )

      ordered_list.reverse.each do |interceptor|
        factory = interceptor.action.call( point.container )
        instance = factory.new( point, interceptor.options )
        element = InterceptorChainElement.new( instance )
        element.next = chain
        chain = element
      end

      # FIXME: should inherited methods of "Object" be interceptable?
      methods_to_intercept = ( service.class.instance_methods( true ) -
                               Object.instance_methods +
                               service.class.instance_methods( false ) ).uniq

      service = InterceptedServiceProxy.new( chain )
      singleton = class << service; self; end

      methods_to_intercept.each do |method|
        next if method =~ /^__/

        if singleton.instance_methods(false).include? method
          singleton.send( :remove_method, method )
        end

        singleton.class_eval <<-EOF
          def #{method}( *args, &block )
            context = InvocationContext.new( :#{method}, args, block, Hash.new )
            @chain.process_next( context )
          end
        EOF
      end

      # allow the interceptor to intercept methods not explicitly
      # declared on the reciever.
      if singleton.instance_methods(false).include? "method_missing"
        singleton.send( :remove_method, :method_missing )
      end

      singleton.class_eval <<-EOF
        def method_missing( sym, *args, &block )
          context = InvocationContext.new( sym, args, block, Hash.new )
          @chain.process_next( context )
        end
      EOF

      return service
    end
    module_function :build

  end

end