diff --git a/lib/Kelp.pm b/lib/Kelp.pm index 508f5c6..8fd0933 100644 --- a/lib/Kelp.pm +++ b/lib/Kelp.pm @@ -21,6 +21,7 @@ attr -name => sub { (ref($_[0]) =~ /(\w+)$/) ? $1 : 'Noname' }; attr request_obj => 'Kelp::Request'; attr response_obj => 'Kelp::Response'; attr context_obj => 'Kelp::Context'; +attr middleware_obj => 'Kelp::Middleware'; # Debug attr long_error => $ENV{KELP_LONG_ERROR} // 0; @@ -267,25 +268,11 @@ sub run Kelp::Util::_DEBUG(1 => 'Running the application...'); - # Add middleware - if (defined(my $middleware = $self->config('middleware'))) { - for my $class (@$middleware) { - - # Make sure the middleware was not already loaded - # This does not apply for testing, in which case we want - # the middleware to wrap every single time - next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING}; - - my $mw = Plack::Util::load_class($class, 'Plack::Middleware'); - my $args = $self->config("middleware_init.$class") // {}; - - Kelp::Util::_DEBUG(modules => "Wrapping app in $mw middleware with args: ", $args); - - $app = $mw->wrap($app, %$args); - } - } + my $middleware = Kelp::Util::load_package($self->middleware_obj)->new( + app => $self, + ); - return $app; + return $middleware->wrap($app); } sub psgi @@ -581,6 +568,11 @@ L for more information. Provide a custom package name to define the ::Context object. Defaults to L. +=head2 middleware_obj + +Provide a custom package name to define the middleware object. Defaults to +L. + =head2 request_obj Provide a custom package name to define the ::Request object. Defaults to @@ -852,8 +844,8 @@ every route. =head2 run -This method builds and returns the PSGI app. You can override it in order to -include middleware. See L for an example. +This method builds and returns the PSGI app. You can override it to get more +control over PSGI representation of the app. =head2 param diff --git a/lib/Kelp/Manual.pod b/lib/Kelp/Manual.pod index 6d6e52e..7459631 100644 --- a/lib/Kelp/Manual.pod +++ b/lib/Kelp/Manual.pod @@ -650,7 +650,7 @@ information and examples. =head2 Adding middleware -Kelp, being Plack-centric, will let you easily add middleware. There are three +Kelp, being Plack-centric, will let you easily add middleware. There are four possible ways to add middleware to your application, and all three ways can be used separately or together. @@ -672,9 +672,32 @@ the corresponding initializing arguments in the C hash: } The middleware will be added in the order you specify in the C -array. +array. Note that you can also use more verbose C mode for middleware +config, which is also much more powerful. See L for +details. -=head3 In C: +=head3 By subclassing L + +L is a class which handles wrapping application in middleware +based on config. Subclassing it may be the most powerful way to add more +middleware if configuration is not enough. + + # lib/MyApp.pm + attr middleware_obj => 'MyMiddleware'; + + # lib/MyMiddleware.pm + use Kelp::Base 'Kelp::Middleware'; + + sub wrap { + my $self = shift; + my $app = $self->SUPER::wrap(@_); + $app = Plack::Middleware::ContentLength->wrap($app); + return $app; + } + +This lets you add middleware before or after config middleware (or disable config middleware completely). + +=head3 In C # app.psgi use MyApp; @@ -687,7 +710,7 @@ array. $app->run; }; -=head3 By overriding the L subroutine in C: +=head3 By overriding the L subroutine in C Make sure you call C first, and then wrap new middleware around the returned app. diff --git a/lib/Kelp/Middleware.pm b/lib/Kelp/Middleware.pm new file mode 100644 index 0000000..161684f --- /dev/null +++ b/lib/Kelp/Middleware.pm @@ -0,0 +1,215 @@ +package Kelp::Middleware; + +use Kelp::Base; +use Plack::Util; +use Kelp::Util; +use Carp; + +attr -app => sub { croak 'app is required' }; + +sub _wrap_basic +{ + my ($self, $psgi, $middleware) = @_; + + for my $class (@$middleware) { + + # Make sure the middleware was not already loaded + # This does not apply for testing, in which case we want + # the middleware to wrap every single time + next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING}; + + my $mw = Plack::Util::load_class($class, 'Plack::Middleware'); + my $args = $self->app->config("middleware_init.$class") // {}; + + Kelp::Util::_DEBUG(modules => "Wrapping app in $mw middleware with args: ", $args); + + $psgi = $mw->wrap($psgi, %$args); + } + + return $psgi; +} + +sub _wrap_advanced +{ + my ($self, $psgi, $middleware) = @_; + + for my $name (@$middleware) { + + # Make sure the middleware was not already loaded + # This does not apply for testing, in which case we want + # the middleware to wrap every single time + next if $self->{_loaded_middleware}->{$name}++ && !$ENV{KELP_TESTING}; + + my $config = $self->app->config("middleware_init.$name") // {}; + next if $config->{disabled}; + my $path = $config->{path} // ''; + my $class = $config->{class}; + my $args = $config->{args} // {}; + + # forced first slash, no trailing slash + $path =~ s{^/? (.*?) /?$}{/$1}x; + + Kelp::Util::_DEBUG(modules => "Wrapping app in $name middleware (under path $path) with args: ", $args); + my $mw = Plack::Util::load_class($class, 'Plack::Middleware'); + my $wrapped = $mw->wrap($psgi, %$args); + + if ($path eq '/') { + + # no need to wrap again if path is root + $psgi = $wrapped; + } + else { + my $orig_psgi = $psgi; + my $prepared_path = qr{^ \Q$path\E (/|$)}x; + + # wrap to check PATH_INFO + $psgi = sub { + goto $wrapped if $_[0]->{PATH_INFO} =~ $prepared_path; + goto $orig_psgi; + }; + } + } + + return $psgi; +} + +sub wrap +{ + my ($self, $psgi) = @_; + + if (defined(my $middleware = $self->app->config('middleware'))) { + my $type = $self->app->config('middleware_type', 'basic'); + my $method = "_wrap_$type"; + $psgi = $self->$method($psgi, $middleware); + } + + return $psgi; +} + +1; + +__END__ + +=pod + +=head1 NAME + +Kelp::Middleware - Kelp app wrapper (PSGI middleware) + +=head1 SYNOPSIS + + middleware => [qw(TrailingSlashKiller Static)], + middleware_init => { + TrailingSlashKiller => { + redirect => 1, + }, + Static => { + path => qr{^/static}, + root => '.', + }, + } + +=head1 DESCRIPTION + +This is a small helper object which wraps Kelp in PSGI middleware. It is loaded +and constructed by Kelp based on the value of L (class +name). + +=head2 Middleware types + +Kelp has a couple of middleware wrapper types available. Default type is +C, which is straightforward to configure (same as Kelp modules). +C is more verbose in configuration, but adds additional capabilities. +To change the type of middleware wrapper you use, specify C +configuration key. + +=head3 Basic + +Default configuration, same as shown in L and L. + +=head3 Advanced + +More advanced configuration, adds some helpful capabilities: + +=over + +=item * multiple middlewares of the same type + +You must specify your own names for the middlewares, and each name have its own +C attached to it. This way you can easily introduce more than one +middleware of the same type. C can be specified the same way as normally +- either a submodule of C namespace, or full namespace when +prefixed by C<+>. + +=item * disabling middlewares + +It's hard to disable middleware in environment-specific config (like +C), and it's impossible to choose the order of new middleware added +in these configs (they will always come last). + +To solve this, you can set C<< disabled => 1 >> in your main config, and +override with C<< disabled => 0 >> in environmental configs. + +=item * choosing a path for middlewares + +Middlewares let you skip a ton of coding, but with basic config you can only +use them at the root level. This may be a pain if you only want a middleware +used under a specific path. + +With advanced middleware, you can specify a C. Your app will only run the +middleware if the path matches (as a prefix). + +=back + +The arguments passed to the actual middleware must be wrapped inside the +C structure. Outside of that structure, you must specify C and can +specify C and C, as discussed above. + + middleware_type => 'advanced', + middleware => [qw(kill_slash public_files static_files api_auth)], + middleware_init => { + kill_slash => { + class => 'TrailingSlashKiller, + args => { + redirect => 1, + }, + }, + public_files => { + class => 'Static', + path => '/public', + args => { + root => '.', + }, + }, + static_files => { + class => 'Static', + path => '/static', + disabled => 1, + args => { + root => '.', + }, + }, + api_auth => { + class => 'Auth::Basic', + path => '/api', + args => { + authenticator => app, + }, + }, + }, + +=head1 ATTRIBUTES + +=head2 app + +Main application object. Required. + +=head1 METHODS + +=head2 wrap + + $wrapped_psgi = $object->wrap($psgi) + +Wraps the object in all middlewares according to L configuration. + diff --git a/t/middleware.t b/t/middleware.t index ab63fb4..cd9d978 100644 --- a/t/middleware.t +++ b/t/middleware.t @@ -8,8 +8,10 @@ use Test::More; my $app = Kelp->new(mode => 'test', __config => 1); $app->routes->base("main"); -# Need only one route -$app->add_route('/mw', sub { "OK" }); +$app->add_route('/', sub { "/" }); +$app->add_route('/mw', sub { "/mw" }); +$app->add_route('/mw/2', sub { "/mw/2" }); +$app->add_route('/mw2', sub { "/mw2" }); my $t = Kelp::Test->new(app => $app); @@ -31,6 +33,67 @@ $app->_cfg->merge( $t->request(GET '/mw') ->header_is("X-Framework", "Changed") - ->header_is("Content-Length", 2); + ->header_is("Content-Length", 3); + +$t->request(GET '/') + ->header_is("X-Framework", "Changed") + ->header_is("Content-Length", 1); + +# Add advanced middleware +$app->_cfg->merge( + { + middleware_type => 'advanced', + middleware => [qw(set_framework content_length set_framework_disabled)], + middleware_init => { + content_length => { + class => 'ContentLength', + }, + set_framework => { + class => 'XFramework', + path => 'mw/', + args => { + framework => 'Changed' + }, + }, + set_framework_disabled => { + class => 'XFramework', + path => '/mw2', + disabled => 1, + args => { + framework => 'ERROR' + }, + } + } + } +); + +$t->request(GET '') + ->header_is("X-Framework", "Perl Kelp") + ->header_is("Content-Length", 1); + +$t->request(GET '/') + ->header_is("X-Framework", "Perl Kelp") + ->header_is("Content-Length", 1); + +# NOTE: 404 +$t->request(GET '/m') + ->header_is("X-Framework", "Perl Kelp"); + +$t->request(GET '/mw') + ->header_is("X-Framework", "Changed") + ->header_is("Content-Length", 3); + +$t->request(GET '/mw/') + ->header_is("X-Framework", "Changed") + ->header_is("Content-Length", 3); + +$t->request(GET '/mw2') + ->header_is("X-Framework", "Perl Kelp") + ->header_is("Content-Length", 4); + +$t->request(GET '/mw/2') + ->header_is("X-Framework", "Changed") + ->header_is("Content-Length", 5); done_testing; +