Skip to content

Commit 71796af

Browse files
committed
Implement advanced middleware config
1 parent 4ef8c5f commit 71796af

File tree

4 files changed

+320
-27
lines changed

4 files changed

+320
-27
lines changed

lib/Kelp.pm

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ attr -name => sub { (ref($_[0]) =~ /(\w+)$/) ? $1 : 'Noname' };
2121
attr request_obj => 'Kelp::Request';
2222
attr response_obj => 'Kelp::Response';
2323
attr context_obj => 'Kelp::Context';
24+
attr middleware_obj => 'Kelp::Middleware';
2425

2526
# Debug
2627
attr long_error => $ENV{KELP_LONG_ERROR} // 0;
@@ -267,25 +268,11 @@ sub run
267268

268269
Kelp::Util::_DEBUG(1 => 'Running the application...');
269270

270-
# Add middleware
271-
if (defined(my $middleware = $self->config('middleware'))) {
272-
for my $class (@$middleware) {
273-
274-
# Make sure the middleware was not already loaded
275-
# This does not apply for testing, in which case we want
276-
# the middleware to wrap every single time
277-
next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING};
278-
279-
my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
280-
my $args = $self->config("middleware_init.$class") // {};
281-
282-
Kelp::Util::_DEBUG(modules => "Wrapping app in $mw middleware with args: ", $args);
283-
284-
$app = $mw->wrap($app, %$args);
285-
}
286-
}
271+
my $middleware = Kelp::Util::load_package($self->middleware_obj)->new(
272+
app => $self,
273+
);
287274

288-
return $app;
275+
return $middleware->wrap($app);
289276
}
290277

291278
sub psgi
@@ -581,6 +568,11 @@ L<Kelp::Module::Config> for more information.
581568
Provide a custom package name to define the ::Context object. Defaults to
582569
L<Kelp::Context>.
583570
571+
=head2 middleware_obj
572+
573+
Provide a custom package name to define the middleware object. Defaults to
574+
L<Kelp::Middleware>.
575+
584576
=head2 request_obj
585577
586578
Provide a custom package name to define the ::Request object. Defaults to
@@ -852,8 +844,8 @@ every route.
852844
853845
=head2 run
854846
855-
This method builds and returns the PSGI app. You can override it in order to
856-
include middleware. See L<Kelp::Manual/Adding middleware> for an example.
847+
This method builds and returns the PSGI app. You can override it to get more
848+
control over PSGI representation of the app.
857849
858850
=head2 param
859851

lib/Kelp/Manual.pod

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ information and examples.
650650

651651
=head2 Adding middleware
652652

653-
Kelp, being Plack-centric, will let you easily add middleware. There are three
653+
Kelp, being Plack-centric, will let you easily add middleware. There are four
654654
possible ways to add middleware to your application, and all three ways can be
655655
used separately or together.
656656

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

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

677-
=head3 In C<app.psgi>:
679+
=head3 By subclassing L<Kelp::Middleware>
680+
681+
L<Kelp::Middleware> is a class which handles wrapping application in middleware
682+
based on config. Subclassing it may be the most powerful way to add more
683+
middleware if configuration is not enough.
684+
685+
# lib/MyApp.pm
686+
attr middleware_obj => 'MyMiddleware';
687+
688+
# lib/MyMiddleware.pm
689+
use Kelp::Base 'Kelp::Middleware';
690+
691+
sub wrap {
692+
my $self = shift;
693+
my $app = $self->SUPER::wrap(@_);
694+
$app = Plack::Middleware::ContentLength->wrap($app);
695+
return $app;
696+
}
697+
698+
This lets you add middleware before or after config middleware (or disable config middleware completely).
699+
700+
=head3 In C<app.psgi>
678701

679702
# app.psgi
680703
use MyApp;
@@ -687,7 +710,7 @@ array.
687710
$app->run;
688711
};
689712

690-
=head3 By overriding the L<Kelp/run> subroutine in C<lib/MyApp.pm>:
713+
=head3 By overriding the L<Kelp/run> subroutine in C<lib/MyApp.pm>
691714

692715
Make sure you call C<SUPER> first, and then wrap new middleware around the
693716
returned app.

lib/Kelp/Middleware.pm

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package Kelp::Middleware;
2+
3+
use Kelp::Base;
4+
use Plack::Util;
5+
use Kelp::Util;
6+
use Carp;
7+
8+
attr -app => sub { croak 'app is required' };
9+
10+
sub _wrap_basic
11+
{
12+
my ($self, $psgi, $middleware) = @_;
13+
14+
for my $class (@$middleware) {
15+
16+
# Make sure the middleware was not already loaded
17+
# This does not apply for testing, in which case we want
18+
# the middleware to wrap every single time
19+
next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING};
20+
21+
my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
22+
my $args = $self->app->config("middleware_init.$class") // {};
23+
24+
Kelp::Util::_DEBUG(modules => "Wrapping app in $mw middleware with args: ", $args);
25+
26+
$psgi = $mw->wrap($psgi, %$args);
27+
}
28+
29+
return $psgi;
30+
}
31+
32+
sub _wrap_advanced
33+
{
34+
my ($self, $psgi, $middleware) = @_;
35+
36+
for my $name (@$middleware) {
37+
38+
# Make sure the middleware was not already loaded
39+
# This does not apply for testing, in which case we want
40+
# the middleware to wrap every single time
41+
next if $self->{_loaded_middleware}->{$name}++ && !$ENV{KELP_TESTING};
42+
43+
my $config = $self->app->config("middleware_init.$name") // {};
44+
next if $config->{disabled};
45+
my $path = $config->{path} // '';
46+
my $class = $config->{class};
47+
my $args = $config->{args} // {};
48+
49+
# forced first slash, no trailing slash
50+
$path =~ s{^/? (.*?) /?$}{/$1}x;
51+
52+
Kelp::Util::_DEBUG(modules => "Wrapping app in $name middleware (under path $path) with args: ", $args);
53+
my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
54+
my $wrapped = $mw->wrap($psgi, %$args);
55+
56+
if ($path eq '/') {
57+
58+
# no need to wrap again if path is root
59+
$psgi = $wrapped;
60+
}
61+
else {
62+
my $orig_psgi = $psgi;
63+
my $prepared_path = qr{^ \Q$path\E (/|$)}x;
64+
65+
# wrap to check PATH_INFO
66+
$psgi = sub {
67+
goto $wrapped if $_[0]->{PATH_INFO} =~ $prepared_path;
68+
goto $orig_psgi;
69+
};
70+
}
71+
}
72+
73+
return $psgi;
74+
}
75+
76+
sub wrap
77+
{
78+
my ($self, $psgi) = @_;
79+
80+
if (defined(my $middleware = $self->app->config('middleware'))) {
81+
my $type = $self->app->config('middleware_type', 'basic');
82+
my $method = "_wrap_$type";
83+
$psgi = $self->$method($psgi, $middleware);
84+
}
85+
86+
return $psgi;
87+
}
88+
89+
1;
90+
91+
__END__
92+
93+
=pod
94+
95+
=head1 NAME
96+
97+
Kelp::Middleware - Kelp app wrapper (PSGI middleware)
98+
99+
=head1 SYNOPSIS
100+
101+
middleware => [qw(TrailingSlashKiller Static)],
102+
middleware_init => {
103+
TrailingSlashKiller => {
104+
redirect => 1,
105+
},
106+
Static => {
107+
path => qr{^/static},
108+
root => '.',
109+
},
110+
}
111+
112+
=head1 DESCRIPTION
113+
114+
This is a small helper object which wraps Kelp in PSGI middleware. It is loaded
115+
and constructed by Kelp based on the value of L<Kelp/middleware_obj> (class
116+
name).
117+
118+
=head2 Middleware types
119+
120+
Kelp has a couple of middleware wrapper types available. Default type is
121+
C<basic>, which is straightforward to configure (same as Kelp modules).
122+
C<advanced> is more verbose in configuration, but adds additional capabilities.
123+
To change the type of middleware wrapper you use, specify C<middleware_type>
124+
configuration key.
125+
126+
=head3 Basic
127+
128+
Default configuration, same as shown in L</SYNOPSIS> and L<Kelp::Manual/Adding
129+
middleware>.
130+
131+
=head3 Advanced
132+
133+
More advanced configuration, adds some helpful capabilities:
134+
135+
=over
136+
137+
=item * multiple middlewares of the same type
138+
139+
You must specify your own names for the middlewares, and each name have its own
140+
C<class> attached to it. This way you can easily introduce more than one
141+
middleware of the same type. C<class> can be specified the same way as normally
142+
- either a submodule of C<Plack::Middleware::> namespace, or full namespace when
143+
prefixed by C<+>.
144+
145+
=item * disabling middlewares
146+
147+
It's hard to disable middleware in environment-specific config (like
148+
C<deployment>), and it's impossible to choose the order of new middleware added
149+
in these configs (they will always come last).
150+
151+
To solve this, you can set C<< disabled => 1 >> in your main config, and
152+
override with C<< disabled => 0 >> in environmental configs.
153+
154+
=item * choosing a path for middlewares
155+
156+
Middlewares let you skip a ton of coding, but with basic config you can only
157+
use them at the root level. This may be a pain if you only want a middleware
158+
used under a specific path.
159+
160+
With advanced middleware, you can specify a C<path>. Your app will only run the
161+
middleware if the path matches (as a prefix).
162+
163+
=back
164+
165+
The arguments passed to the actual middleware must be wrapped inside the
166+
C<args> structure. Outside of that structure, you must specify C<class> and can
167+
specify C<path> and C<disabled>, as discussed above.
168+
169+
middleware_type => 'advanced',
170+
middleware => [qw(kill_slash public_files static_files api_auth)],
171+
middleware_init => {
172+
kill_slash => {
173+
class => 'TrailingSlashKiller,
174+
args => {
175+
redirect => 1,
176+
},
177+
},
178+
public_files => {
179+
class => 'Static',
180+
path => '/public',
181+
args => {
182+
root => '.',
183+
},
184+
},
185+
static_files => {
186+
class => 'Static',
187+
path => '/static',
188+
disabled => 1,
189+
args => {
190+
root => '.',
191+
},
192+
},
193+
api_auth => {
194+
class => 'Auth::Basic',
195+
path => '/api',
196+
args => {
197+
authenticator => app,
198+
},
199+
},
200+
},
201+
202+
=head1 ATTRIBUTES
203+
204+
=head2 app
205+
206+
Main application object. Required.
207+
208+
=head1 METHODS
209+
210+
=head2 wrap
211+
212+
$wrapped_psgi = $object->wrap($psgi)
213+
214+
Wraps the object in all middlewares according to L</app> configuration.
215+

0 commit comments

Comments
 (0)