File Coverage

blib/lib/PlackX/Framework/Router.pm
Criterion Covered Total %
statement 101 111 90.9
branch 25 40 62.5
condition 18 30 60.0
subroutine 18 19 94.7
pod 2 14 14.2
total 164 214 76.6


line stmt bran cond sub pod time code
1 7     7   96 use v5.36;
  7         28  
2             package PlackX::Framework::Router {
3 7     7   45 use Carp ();
  7         15  
  7         2101  
4              
5             my $local_filters = {};
6             my $bases = {};
7             my $engines = {};
8             my $subnames = eval { require Sub::Util; 1 } ? {} : undef;
9              
10             # Override in subclass to change the export names
11 7     7 0 80 sub global_filter_request_keyword { 'global_filter' }
12 7     7 0 74 sub filter_request_keyword { 'filter' }
13 7     7 0 64 sub route_request_keyword { 'route' }
14 7     7 0 63 sub uri_base_keyword { 'base' }
15              
16             # This module exports the routing DSL and parses routes and filters
17             # The engine is what stores the routes and performs matching
18 61     61 0 19097 sub engine ($class) { ($class.'::Engine')->instance; }
  61         139  
  61         135  
  61         346  
19              
20             # Export the DSL
21 8     8   87 sub import ($class, @extra) {
  8         18  
  8         16  
  8         14  
22 8         27 my $export_to = caller(0);
23              
24 8 50       33 die "You must import from your app's subclass, not directly from ".__PACKAGE__
25             if $class eq __PACKAGE__;
26              
27             # Remember which controller is using which router engine object
28             # This is needed if PXF is being used for multiple different apps
29 8         50 $engines->{$export_to} = $class->engine;
30              
31             # Export
32 8         354 foreach my $export_sub (qw/global_filter_request filter_request route_request uri_base/) {
33 32 50       2388 my $export_name = eval "$class->$export_sub\_keyword" or die $@;
34 7     7   51 no strict 'refs';
  7         15  
  7         10053  
35 32         133 *{$export_to . '::' . $export_name} = \&{'DSL_' . $export_sub};
  32         122974  
  32         108  
36             }
37             }
38              
39             # DSL functions
40 1     1 0 201 sub DSL_uri_base ($base) {
  1         3  
  1         3  
41 1         4 my ($package) = caller;
42 1         5 $bases->{$package} = without_trailing_slash($base);
43             }
44              
45             # We handle local filters in this module, but the engine handles global ones
46             sub DSL_global_filter_request {
47 3     3 0 1821 my ($package) = caller;
48 3         8 my $when = shift;
49 3         9 my $action = pop;
50 3 50       11 my $pattern = @_ ? shift : undef;
51              
52 3 50 66     20 die "usage: global_filter ('before' || 'after') => sub {}"
53             unless $when eq 'before' or $when eq 'after';
54              
55 3         14 $engines->{$package}->add_global_filter(
56             when => $when,
57             pattern => $pattern,
58             action => coerce_action_to_subref($action, $package),
59             );
60             }
61              
62 6     6 0 326 sub DSL_filter_request ($when, $action) {
  6         37  
  6         12  
  6         10  
63 6         21 my ($package) = caller;
64              
65 6 50 66     32 die "usage: filter ('before' || 'after') => sub {}"
66             unless $when eq 'before' or $when eq 'after';
67              
68 6   100     48 $local_filters->{$package}{$when} ||= [];
69 6         26 push $local_filters->{$package}{$when}->@*, {
70             controller => $package,
71             when => $when,
72             action => coerce_action_to_subref($action, $package),
73             };
74             }
75              
76 28     28 0 199326 sub DSL_route_request (@args) {
  28         89  
  28         52  
77 28         97 my ($package) = caller;
78 28         98 my $spec = shift @args;
79 28         93 my $action = pop @args;
80              
81 28 50 33     171 die 'expected coderef or hash as last argument'
      33        
82             unless ref $action and (ref $action eq 'CODE' or ref $action eq 'HASH');
83              
84 28 100       87 if (@args) {
85 3         22 my $verb = $spec;
86 3         7 my $path = shift @args;
87 3         12 $spec = { $verb => $path };
88             }
89              
90             # Note: When adding filters, we have to use a copy
91             # Otherwise, a filter that is add later will be added to earlier routes
92 28         79 my $prefilters = $local_filters->{$package}{'before'}; #->@*;
93 28         80 my $postfilters = $local_filters->{$package}{'after'}; #->@*;
94              
95             $engines->{$package}->add_route(
96             spec => $spec,
97 28 100       213 base => $bases->{$package},
    100          
98             prefilters => $prefilters ? [@$prefilters ] : undef,
99             postfilters => $postfilters ? [@$postfilters] : undef,
100             action => coerce_action_to_subref($action, $package),
101             );
102             }
103              
104             # Class method-style
105             # (does not support base or local filters unless they are explicitly specified each time)
106 12     12 1 5277 sub add_route ($class, $spec, $action, %options) {
  12         44  
  12         43  
  12         37  
  12         23  
  12         20  
107 12         44 my ($package) = caller;
108 12   33     113 $options{'filter'} //= $options{'filters'};
109 12   66     45 my $engine = ($engines->{$class} ||= $class->engine);
110             $engine->add_route(
111             spec => $spec,
112             base => $options{'base'} ? without_trailing_slash($options{'base'}) : undef,
113             prefilters => coerce_to_arrayref_or_undef($options{'filter'}{'before'}),
114 12 50       106 postfilters => coerce_to_arrayref_or_undef($options{'filter'}{'after' }),
115             action => coerce_action_to_subref($action, $package),
116             );
117             }
118              
119 5     5 1 253 sub add_global_filter ($class, @args) {
  5         10  
  5         10  
  5         9  
120 5         10 my $when = shift @args;
121 5         8 my $action = pop @args;
122 5   100     40 my $pattern = shift @args // undef;
123 5 100       16 $class->engine->add_global_filter(
124             when => $when,
125             $pattern ? (pattern => $pattern) : (),
126             action => $action
127             );
128             }
129              
130             # Helpers and private methods
131 1     1 0 2 sub without_trailing_slash ($uri) {
  1         2  
  1         2  
132 1 50       8 substr($uri, -1, 1) eq '/' ? substr($uri, 0, -1) : $uri
133             }
134              
135 49     49 0 100 sub coerce_action_to_subref ($action, $package) {
  49         90  
  49         80  
  49         86  
136 49 100       264 if (not ref $action) {
    50          
    50          
137             # String with subroutine name--currently dead code path, maybe bring back
138 1 50       5 $action = ($action =~ m/::/) ? \&{$action} : \&{$package.'::'.$action};
  0         0  
  1         9  
139             } elsif (ref $action eq 'HASH') {
140 0 0       0 Carp::confess 'Invalid router action, expected subref, or hashref with 1 key'
141             unless scalar keys %$action == 1;
142             # Shortcut to response->render_X
143 0         0 my %render_params = %$action;
144 0     0   0 $action = sub ($request, $response) { $response->render(%render_params) };
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
145             } elsif (ref $action ne 'CODE') {
146 0         0 Carp::confess 'Invalid router action, expected subref, or hashref with 1 key'
147             }
148              
149             # Helpful for Debugging
150 49 100 66     593 if (defined $subnames and Sub::Util::subname($action) =~ m/__ANON__/) {
151 48   100     199 $subnames->{$package} //= 0;
152 48         95 my $id = $subnames->{$package}++;
153 48         431 Sub::Util::set_subname($package.'::PXF-anon-route-'.$id, $action);
154             }
155              
156 49         322 return $action;
157             }
158              
159 24     24 0 38 sub coerce_to_arrayref_or_undef ($val) {
  24         53  
  24         44  
160 24 50 33     169 return $val if ref $val eq 'ARRAY' and @$val > 0;
161 24 50       53 return [$val] if defined $val;
162 24         77 return undef;
163             }
164             }
165              
166             1;
167              
168             =pod
169              
170             =head1 NAME
171              
172             PlackX::Framework::Router - Parse routes and export DSL for PXF apps
173              
174              
175             =head1 SYNOPSIS
176              
177             package My::App::Controller {
178             use My::App::Router;
179              
180             request_base '/myapp';
181              
182             filter before => sub ($request, $resp) { ... }
183              
184             filter after => sub ($request, $resp) { ... }
185              
186             route '/' => sub ($request, $resp) { ... }
187             }
188              
189              
190             =head1 EXPORTS
191              
192             This module exports the subroutines "filter", "global_filter", "route", and
193             "base" to the calling package. These can then be used like DSL keywords to lay
194             out your web app.
195              
196             You can choose your own keyword names by overridding them in your subclass this
197             way:
198              
199             package MyApp::Router {
200             use parent 'PlackX::Framework::Router';
201             sub filter_request_keyword { 'my_filter'; }
202             sub global_filter_request_keyword { 'my_global_filter' }
203             sub route_request_keyword { 'my_route'; }
204             sub uri_base_keyword { 'my_base'; }
205             }
206              
207             For more detail, see the "DSL style" section.
208              
209              
210             =head1 CLASS-METHOD STYLE
211              
212             You may add filters routes using class method calls, but this is not the
213             preferred way to use this module.
214              
215             Mixing class method style and DSL style routing in the same app is not
216             recommended.
217              
218             =over 4
219              
220             =item add_route($SPEC, \&ACTION, %OPTIONS)
221              
222             Adds a route matching $SPEC to execute \&ACTION. In the future, %OPTIONS
223             can contain keys 'base', 'prefilters', and/or 'postfilters'.
224              
225             $ACTION should be a coderef, string containing a package and subroutine, e.g.
226             "MyApp::Controller::index", or a hashref containing one of the keys 'template',
227             'text', or 'html' with the value being a string containing a template filename,
228             plain text content to render, or html to render, respectively.
229              
230             =item add_global_filter($WHEN, \&ACTION);
231              
232             =item add_global_filter($WHEN, $PATTERN, \&ACTION);
233              
234             Add a filter which will be applied to any route defined anywhere in the
235             application. If $PATTERN is defined, the filter will only be executed if the
236             request uri matches it. $PATTERN may be a string, scalar reference to a string,
237             or regex; $PATTERN is the same as in DSL style described below.
238              
239             \&ACTION is a reference to a subroutine. The subroutine should return a false
240             value to continue with the next filter or route; if it returns a response
241             object, processing will stop and the response will be rendered.
242              
243             =back
244              
245              
246             =head1 DSL-STYLE
247              
248             =over 4
249              
250             =item request_base $STRING;
251              
252             Set the base URI path for all subsequent routes defined in the current package.
253              
254             =item filter before|after => sub { ... };
255              
256             Filter all subsequent routes. Your filter subroutine should return a false
257             value to continue request processing. $response->next is available for semantic
258             convenience. To render a response early, return the response object.
259              
260             =item global_filter before|after => sub { ... };
261              
262             =item global_filter before|after => $PATTERN => sub { ... };
263              
264             Adds a filter that can match any route anywhere in your application,
265             regardless of where it is defined. Optionally, you may supply a $pattern which
266             may be:
267              
268             =over 4
269              
270             =item - a string, in which case the filter will be executed if the request uri
271             BEGINS WITH the string.
272              
273             =item - a scalar reference to a string, in which case the filter will be executed
274             only if it matches the request path exactly
275              
276             =item - a reference to a regular expression, e.g. qr|^/restricted| which will be
277             used to match the request uri path.
278              
279             =back
280              
281             =item route $URI_SPEC => $ACTION;
282              
283             =item route $METHOD => $PATH => $ACTION;
284              
285             =item route $ARRAYREF => $ACTION;
286              
287             =item route $HASHREF => $ACTION;
288              
289             Execute action \&ACTION for the matching uri described by $URI_SPEC. The
290             $URI_SPEC may contain patterns described by PXF's routing engine,
291             Router::Boom.
292              
293             The $ACTION is a coderef, subroutine name, or hashref, as described in the
294             class method add_route, described above.
295              
296             The $METHOD may contain more than one method, separated by a pipe, for
297             example, the string "get|post".
298              
299             You may specify a list of $URI_SPECs in an $ARRAYREF.
300              
301             You may specify a hashref of key-value pairs, where the key is the HTTP
302             request method, and the value is the desired URI path.
303              
304             See the section below for examples of various combinations.
305              
306             =back
307              
308              
309             =head1 EXAMPLES
310              
311             # Base example
312             base '/myapp';
313              
314             # Filter example
315             # Fat arrow operator allows us to use "before" or "after" without quotes.
316             filter before => sub ($request, $response) {
317             unless ($request->{cookies}{logged_in}) {
318             $response->status(403);
319             return $response;
320             }
321             $request->{logged_in} = 1;
322             return;
323             };
324              
325             # Simple route
326             # Because of the request_base, this will actually match /myapp/index
327             route '/index' => sub { ... };
328              
329             # Route with method
330             route get => '/default' => sub { ... }
331             route post => '/form' => sub { ... }
332              
333             # Route with method, alternate formats
334             route { get => '/login' } => sub { ... }
335              
336             route { post => '/login' } => sub {
337             # do some processing to log in a user..
338             ...
339              
340             # successful login
341             $request->redirect('/user/home');
342              
343             # reroute the request
344             $request->reroute('/try_again');
345             };
346              
347             # Route with arrayref
348             route ['/list/user', '/user/list', '/users/list'] => sub { ... };
349              
350             # Routes with hashref
351             route { post => '/path1', put => '/path1' } => sub { ... };
352              
353             # Route with pattern matching
354             # See Router::Boom for pattern options
355             route { delete => '/user/:id' } => sub {
356             my $request = shift;
357             my $id = $request->route_param('id');
358             };
359              
360             # Combination hashref arrayref
361             route { get => ['/path1', '/path2'] } => sub {
362             ...
363             };
364              
365             # Routes with alternate HTTP verbs
366             route 'get|post' => '/somewhere' => sub { ... };
367              
368             # Action hashref instead of coderef
369             # Key can be one of "template", "text", or "html"
370             route '/' => {
371             template => 'index.tmpl'
372             };
373              
374             route '/hello-world.txt' => {
375             text => 'Hello World'
376             };
377              
378             route '/hello-world.html' => {
379             html => 'Hello World'
380             };
381              
382              
383             =head1 META
384              
385             For author, copyright, and license, see PlackX::Framework.