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