Thinking about how middleware could be implemented in DBD::Gofer - see below ...

Tim Bunce wrote:
On Thu, Sep 08, 2011 at 01:19:17PM +0100, Andrew Ford wrote:
   and I would quite like to get this module onto CPAN and would like some 
feedback as to
   whether "the expert" thinks it is a bad idea.  I think it would be a useful 
addition to the testing
   armoury, but then I am obviously biased.

The general idea is certainly useful. I'd like to generalise it though.

There's a conceptual similarity with PSGI/Plack middlewares that I'd
like to build on.  Specifically, rather than creating a new transport,
I'd like to see DBD/Gofer/Transport/Base.pm extended to call one or more
'Gofer client middlewares' just before it calls transmit_request_by_transport().

The calling code could be something like:

+   my $mw = $self->middleware;
    my $transmit_sub = sub {
        my $response = eval {
            ...
-          $self->transmit_request_by_transport($request)
+          $mw->($self, $request);
        }
        ...
    }

The middleware attribute would default to:

    sub { shift->transmit_request_by_transport(@_) }

new middleware layer would be added by doing:

    my $mw = $self->middleware;
    $self->middleware( sub {
        my ($self, $request) = @_;
        ...do something with $request...
        my $response = $mw->($self, $request);
        ...do something with $response...
        return $response;
    } );

[This is all off-the-top-of-my-head, and I'm not very familar with Plack
internals so I may well be missing important issues.]

Then we just need a way to add middlewares via the environment.
I'd be delighted if you would work up a patch to add that to the DBI.
Currently DBD::Gofer::dr:connect() contains code to set up the transport:

           my $transport_class = delete $go_attr{go_transport}
               or return $drh->set_err($DBI::stderr, "No transport=
   argument in '$orig_dsn'");
           $transport_class = "DBD::Gofer::Transport::$transport_class"
               unless $transport_class =~ /::/;
           _load_class($transport_class)
               or return $drh->set_err($DBI::stderr, "Can't load
   $transport_class: $@");
           my $go_transport = eval { $transport_class->new(\%go_attr) }
               or return $drh->set_err($DBI::stderr, "Can't instanciate
   $transport_class: $@");


I would imagine that we could have a "middleware" keyword that was a comma-separated list of names that were assumed to be in the DBDx::GoferMiddleware namespace - these would be installed in sequence with a similar block of code that follows on from the snippet above, looking something like:

           my $middleware_classes = delete $go_attr{go_middleware};
           foreach my $middleware_class (split /,/,
$middleware_classes) { $middleware_class =
   "DBDx::GoferMiddleware::$middleware_class"
                   unless $middleware_class =~ /::/;
               _load_class($middleware_class)
                   or return $drh->set_err($DBI::stderr, "Can't load
   $middleware_class: $@");
               $go_transport->middleware( sub { return
   $middleware_class->HANDLER(shift, $go_transport->middleware) } );
           }

Each middleware handler would take a request object and a reference to the next handler in the chain.

We would need to have a default method name for the middleware handler (I've used the placeholder HANDLER in the pseudo-code above).

The DSNs would then look like:

    
dbi:Gofer:transport=null;middleware=adaptor;transform=MyMapper;dsn=dbi:XXX...

There are a few holes in this that I can think of:

   * We might want to instantiate a middleware object for each
     component and use that in installing the middleware handler
   * I am not sure how we could specify a handler name other than the
     default for individual middleware components
   * Also not sure how attributes for each middleware component could
     be specified such that the component for which the attribute is
     intended could be specified - the example DSN above assumes that
     the attribute (transform in the example DSN) is just dumped into
     the general "go_" attribute hash)
   * The go_middleware attribute could be set in the DBI connect
     attribute hash to a list of subroutine references (code would need
     to differentiate between a comma-separated list value and a list
     reference value)
   * Should the middleware components be listed innermost first (as
     implemented in the pseudo-code above) or outermost first?
   * We might want to add some convenience methods to the request class
     to build response objects (as suggested in my
     DBD::Gofer::Transport::adaptor code)


I'll have a go at getting this coded over the next few days, but it looks as if the changes required to implement middleware are relatively trivial.

Your module could then use this mechanism. If you wanted to release it
to CPAN then a name like DBDx::GoferMiddleware::FOO would be good.
(Note the DBDx not DBIx since this is client-side. We may well end up
with server-side gofer middlewares as well.)
I think I would take the name DBDx::GoferMiddleware::adaptor for my transformation middleware, unless anyone has a better idea (are we keeping the final class name component for the middleware class in lower case, as it is with the transport classes?).

Andrew

Reply via email to