Skip to content

Commit

Permalink
Implement advanced middleware config
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrtj committed Oct 9, 2024
1 parent 4ef8c5f commit 71796af
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 27 deletions.
32 changes: 12 additions & 20 deletions lib/Kelp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -581,6 +568,11 @@ L<Kelp::Module::Config> for more information.
Provide a custom package name to define the ::Context object. Defaults to
L<Kelp::Context>.
=head2 middleware_obj
Provide a custom package name to define the middleware object. Defaults to
L<Kelp::Middleware>.
=head2 request_obj
Provide a custom package name to define the ::Request object. Defaults to
Expand Down Expand Up @@ -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<Kelp::Manual/Adding middleware> 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
Expand Down
31 changes: 27 additions & 4 deletions lib/Kelp/Manual.pod
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -672,9 +672,32 @@ the corresponding initializing arguments in the C<middleware_init> hash:
}

The middleware will be added in the order you specify in the C<middleware>
array.
array. Note that you can also use more verbose C<advanced> mode for middleware
config, which is also much more powerful. See L<Kelp::Middleware/Advanced> for
details.

=head3 In C<app.psgi>:
=head3 By subclassing L<Kelp::Middleware>

L<Kelp::Middleware> 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>

# app.psgi
use MyApp;
Expand All @@ -687,7 +710,7 @@ array.
$app->run;
};

=head3 By overriding the L<Kelp/run> subroutine in C<lib/MyApp.pm>:
=head3 By overriding the L<Kelp/run> subroutine in C<lib/MyApp.pm>

Make sure you call C<SUPER> first, and then wrap new middleware around the
returned app.
Expand Down
215 changes: 215 additions & 0 deletions lib/Kelp/Middleware.pm
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.
Loading

0 comments on commit 71796af

Please sign in to comment.