File Coverage

blib/lib/Kelp/Routes.pm
Criterion Covered Total %
statement 101 105 96.1
branch 46 52 88.4
condition 34 36 94.4
subroutine 12 12 100.0
pod 5 5 100.0
total 198 210 94.2


line stmt bran cond sub pod time code
1              
2             use Carp;
3 35     35   361416  
  35         100  
  35         1940  
4             use Kelp::Base;
5 35     35   1484 use Kelp::Routes::Pattern;
  35         73  
  35         161  
6 35     35   15287 use Plack::Util;
  35         81  
  35         197  
7 35     35   1839 use Class::Inspector;
  35         42614  
  35         798  
8 35     35   1829  
  35         12081  
  35         51065  
9             attr base => '';
10             attr routes => sub { [] };
11             attr names => sub { {} };
12              
13             # Cache
14             attr _CACHE => sub { {} };
15             attr cache => sub {
16             my $self = shift;
17             Plack::Util::inline_object(
18             get => sub { $self->_CACHE->{ $_[0] } },
19             set => sub { $self->_CACHE->{ $_[0] } = $_[1] },
20             clear => sub { $self->_CACHE( {} ) }
21             );
22             };
23              
24             my ( $self, $pattern, $descr ) = @_;
25             $self->_parse_route( {}, $pattern, $descr );
26 178     178 1 597 }
27 178         401  
28             $_[0]->routes( [] );
29             $_[0]->cache->clear;
30             $_[0]->names( {} );
31 22     22 1 11976 }
32 22         49  
33 22         56 my ( $string, $base ) = @_;
34             return $string unless $string;
35             my @parts = split( /\#/, $string );
36             my $sub = pop @parts;
37 97     97   7780 @parts = map {
38 97 100       185 join '', map { ucfirst lc } split /\_/
39 95         269 } @parts;
40 95         150 unshift @parts, $base if $base;
41             return join( '::', @parts, $sub );
42 95         163 }
  45         83  
  61         239  
43              
44 95 100       194 my ( $self, $parent, $key, $val ) = @_;
45 95         314  
46             # Scalar, e.g. path => 'bar#foo'
47             # CODE, e.g. path => sub { ... }
48             if ( !ref($val) || ref($val) eq 'CODE' ) {
49 190     190   336 $val = { to => $val };
50             }
51              
52             # Sanity check
53 190 100 100     735 if ( ref($val) ne 'HASH' ) {
54 148         316 carp "Route description must be a SCALAR, CODE or HASH. Skipping.";
55             return;
56             }
57              
58 190 100       431 # 'to' is required
59 1         76 if ( !exists $val->{to} ) {
60 1         52 carp "Route is missing destination. Skipping.";
61             return;
62             }
63              
64 189 100       375 # Format destination
65 2         201 if ( !ref $val->{to} ) {
66 2         109 my $sigil = defined $val->{to} && $val->{to} =~ s/^(\+)// ? $1 : undef;
67             $val->{to} = _camelize( $val->{to}, $sigil ? undef : $self->base );
68              
69             # Load the class, if there is one and it is not 'main'
70 187 100       375 if ( defined $val->{to}
71 72 100 100     344 && $val->{to} =~ /^(.+)::(\w+)$/
72 72 100       233 && $1 ne 'main'
73             && !Class::Inspector->loaded($1) ) {
74             Plack::Util::load_class($1);
75 72 100 100     710 }
      100        
      100        
76             }
77              
78             # Handle the value part
79 9         680 if ( ref($key) eq 'ARRAY' ) {
80             my ( $method, $pattern ) = @$key;
81             if ( !grep { $method eq $_ } qw/GET POST PUT DELETE/ ) {
82             carp "Using an odd method: $method";
83             }
84 186 100       3809 $val->{method} = $method;
85 16         31 $key = $pattern;
86 16 100       27 }
  64         113  
87 1         162  
88             # Only SCALAR and Regexp allowed
89 16         114 if ( ref($key) && ref($key) ne 'Regexp' ) {
90 16         26 carp "Pattern $key can not be computed.";
91             return;
92             }
93              
94 186 100 100     442 $val->{pattern} = $key;
95 2         145  
96 2         98 my $tree;
97             if ( $tree = delete $val->{tree} ) {
98             if ( ref($tree) ne 'ARRAY' ) {
99 184         313 carp "Tree must be an ARRAY. Skipping.";
100             $tree = undef;
101 184         224 }
102 184 100       386 else {
103 7 50       21 $val->{bridge} = 1;
104 0         0 }
105 0         0 }
106             $tree //= [];
107              
108 7         15 # Parrent defined?
109             if (%$parent) {
110             if ( $val->{name} && $parent->{name} ) {
111 184   100     657 $val->{name} = $parent->{name} . '_' . $val->{name};
112             }
113             $val->{pattern} = $parent->{pattern} . $val->{pattern};
114 184 100       345 }
115 12 100 100     78  
116 7         18 # Create pattern object
117             push @{ $self->routes }, Kelp::Routes::Pattern->new(%$val);
118 12         27  
119             # Add route index to names
120             if ( my $name = $val->{name} ) {
121             if ( exists $self->names->{$name} ) {
122 184         252 carp "Redefining route name $name";
  184         411  
123             }
124             $self->names->{$name} = scalar( @{ $self->routes } ) - 1;
125 184 100       560 }
126 19 50       47  
127 0         0 while (@$tree) {
128             my ( $k, $v ) = splice( @$tree, 0, 2 );
129 19         25 $self->_parse_route( $val, $k, $v );
  19         33  
130             }
131             }
132 184         843  
133 12         31 my $self = shift;
134 12         70 my $name = shift // die "Route name is missing";
135             my %args = @_ == 1 ? %{ $_[0] } : @_;
136              
137             return $name unless exists $self->names->{$name};
138             my $route = $self->routes->[ $self->names->{$name} ];
139 14     14 1 25 return $route->build(%args);
140 14   50     31 }
141 14 50       38  
  0         0  
142             my ( $self, $path, $method ) = @_;
143 14 100       38  
144 9         20 # Look for this path and method in the cache. If found,
145 9         51 # return the array of routes that matched the previous time.
146             # If not found, then return all routes.
147             my $key = $path . ':' . ( $method // '' );
148             my $routes = $self->cache->get($key) // $self->routes;
149 238     238 1 20156  
150             # Look through all routes, grep the ones that match
151             # and sort them by 'bridge' and 'pattern'
152             my @processed =
153             sort { $b->bridge <=> $a->bridge || $a->pattern cmp $b->pattern }
154 238   100     729 grep { $_->match( $path, $method ) } @$routes;
155 238   66     524  
156             my $value = \@processed;
157             $self->cache->set( $key, $value );
158             return $value;
159             }
160 15 50       31  
161 238         537 my ( $self, $app, $route ) = @_;
  1108         2087  
162             $app || die "Application instance required";
163 238         384 $route || die "No route pattern instance supplied";
164 238         523  
165 238         657 # Shortcuts
166             my $req = $app->req;
167             my $to = $route->to;
168              
169 193     193 1 334 # Destination must be either a scalar, or a code reference
170 193 50       379 if ( !$to || ref $to && ref $to ne 'CODE' ) {
171 193 50       342 die 'Invalid destination for ' . $req->path;
172             }
173              
174 193         332 # If the destination is not a code reference, then we assume it's
175 193         388 # a fully qualified function name, so we find its reference
176             unless ( ref $to ) {
177              
178 193 100 100     915 # Check if the destination function exists
      100        
179 3         7 unless ( exists &$to ) {
180             die sprintf( 'Route not found %s for %s', $to, $req->path );
181             }
182              
183             # Move to reference
184 190 100       410 $to = \&{$to};
185             }
186              
187 22 100       93 return $to->( $app, @{ $route->param } );
188 3         8 }
189              
190             1;
191              
192 19         29  
  19         47  
193             =pod
194              
195 187         230 =head1 NAME
  187         311  
196              
197             Kelp::Routes - Routing for a Kelp app
198              
199             =head1 SYNOPSIS
200              
201             use Kelp::Routes;
202             my $r = Kelp::Routes->new( base => 'MyApp' );
203             $r->add( '/home', 'home' );
204              
205             =head1 DESCRIPTION
206              
207             The router provides the connection between the HTTP requests and the web
208             application code. It tells the application I<"If you see a request coming to
209             *this* URI, send it to *that* subroutine for processing">. For example, if a
210             request comes to C</home>, then send it to C<sub home> in the current
211             namespace. The process of capturing URIs and sending them to their corresponding
212             code is called routing.
213              
214             This router was specifically crafted as part of the C<Kelp> web framework. It
215             is, however, possible to use it on its own, if needed.
216              
217             It provides a simple, yet sophisticated routing utilizing Perl 5.10's
218             regular expressions, which makes it fast, robust and reliable.
219              
220             The routing process can roughly be broken down into three steps:
221              
222             =over
223              
224             =item B<Adding routes>
225              
226             First you create a router object:
227              
228             my $r = Kelp::Routes->new();
229              
230             Then you add your application's routes and their descriptions:
231              
232             $r->add( '/path' => 'Module::function' );
233             ...
234              
235             =item B<Matching>
236              
237             Once you have your routes added, you can match with the L</match> subroutine.
238              
239             $r->match( $path, $method );
240              
241             The Kelp framework already does matching for you, so you may never
242             have to do your own matching. The above example is provided only for
243             reference.
244              
245             =item B<Building URLs from routes>
246              
247             You can name each of your routes, and use that name later to build a URL:
248              
249             $r->add( '/begin' => { to => 'function', name => 'home' } );
250             my $url = $r->url('home'); # /begin
251              
252             This can be used in views and other places where you need the full URL of
253             a route.
254              
255             =back
256              
257             =head1 PLACEHOLDERS
258              
259             Often routes may get more complicated. They may contain variable parts. For
260             example this one C</user/1000> is expected to do something with user ID 1000.
261             So, in this case we need to capture a route that begins with C</user/> and then
262             has something else after it.
263              
264             Naturally, when it comes to capturing routes, the first instinct of the Perl
265             programmer is to use regular expressions, like this:
266              
267             qr{/user/(\d+)} -> "sub home"
268              
269             This module will let you do that, however regular expressions can get very
270             complicated, and it won't be long before you lose track of what does what.
271              
272             This is why a good router (this one included) allows for I<named placeholders>.
273             These are words prefixed with special symbols, which denote a variable piece in
274             the URI. To use the above example:
275              
276             "/user/:id" -> "sub home"
277              
278             It looks a little cleaner.
279              
280             Placeholders are variables you place in the route path. They are identified by
281             a prefix character and their names must abide to the rules of a regular Perl
282             variable. If necessary, curly braces can be used to separate placeholders from
283             the rest of the path.
284              
285             There are three types of place holders:
286              
287             =head2 Explicit
288              
289             These placeholders begin with a column (C<:>) and must have a value in order for the
290             route to match. All characters are matched, except for the forward slash.
291              
292             $r->add( '/user/:id' => 'Module::sub' );
293             # /user/a -> match (id = 'a')
294             # /user/123 -> match (id = 123)
295             # /user/ -> no match
296             # /user -> no match
297             # /user/10/foo -> no match
298              
299             $r->add( '/page/:page/line/:line' => 'Module::sub' );
300             # /page/1/line/2 -> match (page = 1, line = 2)
301             # /page/bar/line/foo -> match (page = 'bar', line = 'foo')
302             # /page/line/4 -> no match
303             # /page/5 -> no match
304              
305             $r->add( '/{:a}ing/{:b}ing' => 'Module::sub' );
306             # /walking/singing -> match (a = 'walk', b = 'sing')
307             # /cooking/ing -> no match
308             # /ing/ing -> no match
309              
310             =head2 Optional
311              
312             Optional placeholders begin with a question mark C<?> and denote an optional
313             value. You may also specify a default value for the optional placeholder via
314             the L</defaults> option. Again, like the explicit placeholders, the optional
315             ones capture all characters, except the forward slash.
316              
317             $r->add( '/data/?id' => 'Module::sub' );
318             # /bar/foo -> match ( id = 'foo' )
319             # /bar/ -> match ( id = undef )
320             # /bar -> match ( id = undef )
321              
322             $r->add( '/:a/?b/:c' => 'Module::sub' );
323             # /bar/foo/baz -> match ( a = 'bar', b = 'foo', c = 'baz' )
324             # /bar/foo -> match ( a = 'bar', b = undef, c = 'foo' )
325             # /bar -> no match
326             # /bar/foo/baz/moo -> no match
327              
328             Optional default values may be specified via the C<defaults> option.
329              
330             $r->add(
331             '/user/?name' => {
332             to => 'Module::sub',
333             defaults => { name => 'hank' }
334             }
335             );
336              
337             # /user -> match ( name = 'hank' )
338             # /user/ -> match ( name = 'hank' )
339             # /user/jane -> match ( name = 'jane' )
340             # /user/jane/cho -> no match
341              
342             =head2 Wildcards
343              
344             The wildcard placeholders expect a value and capture all characters, including
345             the forward slash.
346              
347             $r->add( '/:a/*b/:c' => 'Module::sub' );
348             # /bar/foo/baz/bat -> match ( a = 'bar', b = 'foo/baz', c = 'bat' )
349             # /bar/bat -> no match
350              
351             =head2 Using curly braces
352              
353             Curly braces may be used to separate the placeholders from the rest of the
354             path:
355              
356             $r->add( '/{:a}ing/{:b}ing' => 'Module::sub' );
357             # /looking/seeing -> match ( a = 'look', b = 'see' )
358             # /ing/ing -> no match
359              
360             $r->add( '/:a/{?b}ing' => 'Module::sub' );
361             # /bar/hopping -> match ( a = 'bar', b = 'hopp' )
362             # /bar/ing -> match ( a = 'bar' )
363             # /bar -> no match
364              
365             $r->add( '/:a/{*b}ing/:c' => 'Module::sub' );
366             # /bar/hop/ping/foo -> match ( a = 'bar', b = 'hop/p', c = 'foo' )
367             # /bar/ing/foo -> no match
368              
369             =head1 BRIDGES
370              
371             The L</match> subroutine will stop and return the route that best matches the
372             specified path. If that route is marked as a bridge, then L</match> will
373             continue looking for another match, and will eventually return an array of one or
374             more routes. Bridges can be used for authentication or other route preprocessing.
375              
376             $r->add( '/users/*', { to => 'Users::auth', bridge => 1 } );
377             $r->add( '/users/:action' => 'Users::dispatch' );
378              
379             The above example will require F</users/profile> to go through two
380             subroutines: C<Users::auth> and C<Users::dispatch>:
381              
382             my $arr = $r->match('/users/view');
383             # $arr is an array of two routes now, the bridge and the last one matched
384              
385             =head1 TREES
386              
387             A quick way to add bridges is to use the L</tree> option. It allows you to
388             define all routes under a bridge. Example:
389              
390             $r->add(
391             '/users/*' => {
392             to => 'users#auth',
393             name => 'users',
394             tree => [
395             '/profile' => {
396             name => 'profile',
397             to => 'users#profile'
398             },
399             '/settings' => {
400             name => 'settings',
401             to => 'users#settings',
402             tree => [
403             '/email' => { name => 'email', to => 'users#email' },
404             '/login' => { name => 'login', to => 'users#login' }
405             ]
406             }
407             ]
408             }
409             );
410              
411             The above call to C<add> causes the following to occur under the hood:
412              
413             =over
414              
415             =item
416              
417             The paths of all routes inside the tree are joined to the path of their
418             parent, so the following five new routes are created:
419              
420             /users -> MyApp::Users::auth
421             /users/profile -> MyApp::Users::profile
422             /users/settings -> MyApp::Users::settings
423             /users/settings/email -> MyApp::Users::email
424             /users/settings/login -> MyApp::Users::login
425              
426             =item
427              
428             The names of the routes are joined via C<_> with the name of their parent:
429              
430             /users -> 'users'
431             /users/profile -> 'users_profile'
432             /users/settings -> 'users_settings'
433             /users/settings/email -> 'users_settings_email'
434             /users/settings/login -> 'users_settings_login'
435              
436             =item
437              
438             The C</users> and C</users/settings> routes are automatically marked as
439             bridges, because they contain a tree.
440              
441             =back
442              
443             =head1 ATTRIBUTES
444              
445             =head2 base
446              
447             Sets the base class for the routes destinations.
448              
449             my $r = Kelp::Routes->new( base => 'MyApp' );
450              
451             This will prepend C<MyApp::> to all route destinations.
452              
453             $r->add( '/home' => 'home' ); # /home -> MyApp::home
454             $r->add( '/user' => 'user#home' ); # /user -> MyApp::User::home
455             $r->add( '/view' => 'User::view' ); # /view -> MyApp::User::view
456              
457             A Kelp application will automatically set this value to the name of the main
458             class. If you need to use a route located in another package, you must prefix
459             it with a plus sign:
460              
461             # Problem:
462              
463             $r->add( '/outside' => 'Outside::Module::route' );
464             # /outside -> MyApp::Outside::Module::route
465             # (most likely not what you want)
466              
467             # Solution:
468              
469             $r->add( '/outside' => '+Outside::Module::route' );
470             # /outside -> Outside::Module::route
471              
472             =head2 cache
473              
474             Routes will be cached in memory, so repeating requests will be dispatched much
475             faster. The C<cache> attribute can optionally be initialized with an instance of
476             a caching module with interface similar to L<CHI> and L<Cache>.
477             The module interface should at the very least provide the following methods:
478              
479             =head3 get($key)
480              
481             retrieve a key from the cache
482              
483             =head3 set($key, $value, $expiration)
484              
485             set a key in the cache
486              
487             =head3 clear()
488              
489             clear all cache
490              
491             The caching module should be initialized in the config file:
492              
493             # config.pl
494             {
495             modules_init => {
496             Routes => {
497             cache => Cache::Memory->new(
498             namespace => 'MyApp',
499             default_expires => '3600 sec'
500             );
501             }
502             }
503             }
504              
505             =head1 SUBROUTINES
506              
507             =head2 add
508              
509             Adds a new route definition to the routes array.
510              
511             $r->add( $path, $destination );
512              
513             C<$path> can be a path string, e.g. C<'/user/view'> or an ARRAY containing a
514             method and a path, e.g. C<[ PUT =E<gt> '/item' ]>.
515              
516             The route destination is very flexible. It can be one of these three things:
517              
518             =over
519              
520             =item
521              
522             A string name of a subroutine, for example C<"Users::item">. Using a C<#> sign
523             to replace C<::> is also allowed, in which case the name will get converted.
524             C<"users#item"> becomes C<"Users::item">.
525              
526             $r->add( '/home' => 'user#home' );
527              
528             =item
529              
530             A code reference.
531              
532             $r->add( '/system' => sub { return \%ENV } );
533              
534             =item
535              
536             A hashref with options.
537              
538             # GET /item/100 -> MyApp::Items::view
539             $r->add(
540             '/item/:id', {
541             to => 'items#view',
542             method => 'GET'
543             }
544             );
545              
546             See L</Destination Options> for details.
547              
548             =back
549              
550             =head3 Destination Options
551              
552             There are a number of options you can add to modify the behavior of the route,
553             if you specify a hashref for a destination:
554              
555             =head4 to
556              
557             Sets the destination for the route. It should be a subroutine name or CODE
558             reference.
559              
560             $r->add( '/home' => { to => 'users#home' } ); # /home -> MyApp::Users::home
561             $r->add( '/sys' => { to => sub { ... } }); # /sys -> execute code
562             $r->add( '/item' => { to => 'Items::handle' } ) ; # /item -> MyApp::Items::handle
563             $r->add( '/item' => { to => 'items#handle' } ); # Same as above
564              
565             =head4 method
566              
567             Specifies an HTTP method to be considered by L</match> when matching a route.
568              
569             # POST /item -> MyApp::Items::add
570             $r->add(
571             '/item' => {
572             method => 'POST',
573             to => 'items#add'
574             }
575             );
576              
577             A shortcut for the above is this:
578              
579             $r->add( [ POST => '/item' ] => 'items#add' );
580              
581             =head4 name
582              
583             Give the route a name, and you can always use it to build a URL later via the L</url>
584             subroutine.
585              
586             $r->add(
587             '/item/:id/:name' => {
588             to => 'items#view',
589             name => 'item'
590             }
591             );
592              
593             # Later
594             $r->url( 'item', id => 8, name => 'foo' ); # /item/8/foo
595              
596             =head4 check
597              
598             A hashref of checks to perform on the captures. It should contain capture
599             names and stringified regular expressions. Do not use C<^> and C<$> to denote
600             beginning and ending of the matched expression, because it will get embedded
601             in a bigger Regexp.
602              
603             $r->add(
604             '/item/:id/:name' => {
605             to => 'items#view',
606             check => {
607             id => '\d+', # id must be a digit
608             name => 'open|close' # name can be 'open' or 'close'
609             }
610             }
611             );
612              
613             =head4 defaults
614              
615             Set default values for optional placeholders.
616              
617             $r->add(
618             '/pages/?id' => {
619             to => 'pages#view',
620             defaults => { id => 2 }
621             }
622             );
623              
624             # /pages -> match ( id = 2 )
625             # /pages/ -> match ( id = 2 )
626             # /pages/4 -> match ( id = 4 )
627              
628             =head4 bridge
629              
630             If set to 1 this route will be treated as a bridge. Please see L</BRIDGES>
631             for more information.
632              
633             =head4 tree
634              
635             Creates a tree of sub-routes. See L</TREES> for more information and examples.
636              
637             =head2 url
638              
639             my $url = $r->url($path, @arguments);
640              
641             Builds an url from path and arguments. If the request is named a name can be specified instead.
642              
643             =head2 match
644              
645             Returns an array of L<Kelp::Routes::Pattern> objects that match the path
646             and HTTP method provided. Each object will contain a hash with the named
647             placeholders in L<Kelp::Routes::Pattern/named>, and an array with their
648             values in the order they were specified in the pattern in
649             L<Kelp::Routes::Pattern/param>.
650              
651             $r->add( '/:id/:name', "route" );
652             for my $pattern ( @{ $r->match('/15/alex') } ) {
653             $pattern->named; # { id => 15, name => 'alex' }
654             $pattern->param; # [ 15, 'alex' ]
655             }
656              
657             Routes that used regular expressions instead of patterns will only initialize
658             the C<param> array with the regex captures, unless those patterns are using
659             named captures in which case the C<named> hash will also be initialized.
660              
661             =head2 dispatch
662              
663             my $result = $r->dispatch($kelp, $route_pattern);
664              
665             Dispatches an instance of L<Kelp::Routes::Pattern> by running the route destination specified in L<Kelp::Routes::Pattern/to>
666              
667             =head1 EXTENDING
668              
669             This is the default router class for each new Kelp application, but it doesn't
670             have to be. You can create your own subclass that better suits your needs. It's
671             generally enough to override the L</dispatch> method.
672              
673             Kelp comes with L<Kelp::Routes::Controller>, a router extension which reblesses
674             the application instance into a controller class.
675              
676             =head1 ACKNOWLEDGEMENTS
677              
678             This module was inspired by L<Routes::Tiny>.
679              
680             =cut