-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement advanced middleware config
- Loading branch information
Showing
4 changed files
with
320 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Kelp/middleware_obj> (class | ||
name). | ||
=head2 Middleware types | ||
Kelp has a couple of middleware wrapper types available. Default type is | ||
C<basic>, which is straightforward to configure (same as Kelp modules). | ||
C<advanced> is more verbose in configuration, but adds additional capabilities. | ||
To change the type of middleware wrapper you use, specify C<middleware_type> | ||
configuration key. | ||
=head3 Basic | ||
Default configuration, same as shown in L</SYNOPSIS> and L<Kelp::Manual/Adding | ||
middleware>. | ||
=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<class> attached to it. This way you can easily introduce more than one | ||
middleware of the same type. C<class> can be specified the same way as normally | ||
- either a submodule of C<Plack::Middleware::> namespace, or full namespace when | ||
prefixed by C<+>. | ||
=item * disabling middlewares | ||
It's hard to disable middleware in environment-specific config (like | ||
C<deployment>), 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<path>. 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<args> structure. Outside of that structure, you must specify C<class> and can | ||
specify C<path> and C<disabled>, 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</app> configuration. | ||
Oops, something went wrong.