File Coverage

blib/lib/Router/Generic.pm
Criterion Covered Total %
statement 158 175 90.2
branch 73 100 73.0
condition 18 29 62.0
subroutine 13 15 86.6
pod 4 8 50.0
total 266 327 81.3


line stmt bran cond sub pod time code
1              
2             package Router::Generic;
3              
4 5     5   5187 use strict;
  5         12  
  5         285  
5 5     5   31 use warnings 'all';
  5         9  
  5         266  
6 5     5   35 use Carp 'confess';
  5         10  
  5         8808  
7              
8             our $VERSION = '0.021';
9              
10             sub new
11             {
12 11     11 0 5427 my ($class, %args) = @_;
13            
14 11         80 my $s = bless {
15             cache => { },
16             routes => [ ],
17             patterns => { },
18             names => { },
19             paths_methods => { },
20             %args
21             }, $class;
22            
23 11         32 $s->init();
24            
25 11         45 return $s;
26             }# end new()
27              
28 11     11 0 19 sub init { }
29              
30              
31             sub add_route
32             {
33 1046     1046 1 9740 my ($s, %args) = @_;
34            
35             # Set the method:
36 1046   100     4344 $args{method} ||= '*';
37 1046         1863 $args{method} = uc($args{method});
38            
39 1046         2135 my $uid = "$args{method} $args{path}";
40 1046         1369 my $starUID = "* $args{path}";
41            
42             # Validate the args:
43 1046 50 33     4529 confess "Required param 'path' was not provided."
44             unless defined($args{path}) && length($args{path});
45            
46 1046 50 33     4881 confess "Required param 'target' was not provided."
47             unless defined($args{target}) && length($args{target});
48            
49 1046 50 33     4276 confess "Required param 'name' was not provided."
50             unless defined($args{name}) && length($args{name});
51            
52 1046 100       2957 confess "name '$args{name}' is already in use by '$s->{names}->{$args{name}}->{path}'."
53             if exists($s->{names}->{$args{name}});
54            
55 1045 100       3302 confess "path '$args{method} $args{path}' conflicts with pre-existing path '$s->{paths_methods}->{$uid}->{method} $s->{paths_methods}->{$uid}->{path}'."
56             if exists($s->{paths_methods}->{$uid});
57            
58 1041 50       2031 confess "name '* $args{name}' is already in use by '$s->{paths_methods}->{$starUID}->{method} $s->{paths_methods}->{$starUID}->{path}'."
59             if exists($s->{paths_methods}->{$starUID});
60            
61            
62 1041   100     4224 $args{defaults} ||= { };
63            
64             # Fixup our pattern:
65 1041         2848 ($args{regexp}, $args{captures}, $args{uri_template}) = $s->_patternize( $args{path} );
66            
67 1041         2851 my $regUID = "$args{method} " . $args{regexp};
68            
69 1041 100       3151 if( my $exists = $s->{patterns}->{$regUID} )
70             {
71 1         102 confess "path '$args{path}' conflicts with pre-existing path '$exists'.";
72             }# end if()
73            
74 1040         1156 push @{$s->{routes}}, \%args;
  1040         1949  
75 1040         3566 $s->{patterns}->{$regUID} = $args{path};
76 1040         2858 $s->{names}->{$args{name}} = $s->{routes}->[-1];
77 1040         2551 $s->{paths_methods}->{$uid} = $s->{routes}->[-1];
78              
79 1040         2503 return 1;
80             }# end add_route()
81              
82              
83             sub replace_route
84             {
85 0     0 0 0 my ($s, %args) = @_;
86            
87             $s->add_route( %args )
88 0 0       0 unless eval { $s->uri_for($args{name}) };
  0         0  
89 0         0 1;
90             }# end replace_route()
91              
92              
93             sub _patternize
94             {
95 1041     1041   1296 my ($s, $path) = @_;
96            
97             # For lack of real *actual* named captures:
98 1041         13383 my @captures = ( );
99            
100             # Construct a regexp that can be used to select the matching route for any
101             # given uri:
102 1041         931 my $regexp = do {
103 1041         6306 (my $copy = $path) =~ s!
104             \{(\w+\:(?:\{[0-9,]+\}|[^{}]+)+)\} | # /foo/{Page:\d+}
105             :([^/\{\}\:\-]+) | # /foo/:title
106             \{([^\}]+)\} | # /foo/{Bar} and /foo/{*WhateverElse}
107             ([^/\{\}\:\-]+) # /foo/literal/
108             !
109 2108 100       8271 if( $1 )
    100          
    100          
    50          
110             {
111 1010         3215 my ($name, $pattern) = split /:/, $1;
112 1010         1704 push @captures, $name;
113 1010 50       3208 $pattern ? "($pattern)" : "([^/]*?)";
114             }
115             elsif( $2 )
116             {
117 24         50 push @captures, $2;
118 24         84 "([^/]*?)";
119             }
120             elsif( $3 )
121             {
122 21         31 my $part = $3;
123 21 100       53 if( $part =~ m/^\*/ )
124             {
125 10         21 $part =~ s/^\*//;
126 10         17 push @captures, $part;
127 10         23 "(.*?)";
128             }
129             else
130             {
131 11         16 push @captures, $part;
132 11         77 "([^/]*?)";
133             }# end if()
134             }
135             elsif( $4 )
136             {
137 1053         6962 quotemeta($4);
138             }# end if()
139             !sgxe;
140            
141             # Make the trailing '/' optional:
142 1041 100       2804 unless( $copy =~ m{\/[^/]+\.[^/]+$} )
143             {
144 1031 100       2339 $copy .= '/' unless $copy =~ m/\/$/;
145 1031         3611 $copy =~ s{\/$}{\/?};
146             }# end unless()
147 1041         18666 qr{^$copy$};
148             };
149            
150             # This tokenized string becomes a template for the 'uri_for(...)' method:
151 1041         1656 my $uri_template = do {
152 1041         4748 (my $copy = $path) =~ s!
153             \{(\w+\:(?:\{[0-9,]+\}|[^{}]+)+)\} | # /foo/{Page:\d+}
154             :([^/\{\}\:\-]+) | # /foo/:title
155             \{([^\}]+)\} | # /foo/{Bar} and /foo/{*WhateverElse}
156             ([^/\{\}\:\-]+) # /foo/literal/
157             !
158 2108 100       7380 if( $1 )
    100          
    100          
    50          
159             {
160 1010         4193 my ($name, $pattern) = split /:/, $1;
161 1010         3463 "[:$name:]";
162             }
163             elsif( $2 )
164             {
165 24         93 "[:$2:]";
166             }
167             elsif( $3 )
168             {
169 21         26 my $part = $3;
170 21 100       44 if( $part =~ m/^\*/ )
171             {
172 10         24 $part =~ s/^\*//;
173 10         29 "[:$part:]"
174             }
175             else
176             {
177 11         36 "[:$part:]"
178             }# end if()
179             }
180             elsif( $4 )
181             {
182 1053         6914 $4;
183             }# end if()
184             !sgxe;
185            
186 1041 100       2658 unless( $copy =~ m{\/[^/]+\.[^/]+$} )
187             {
188 1040 100       2085 $copy .= '/' unless $copy =~ m/\/$/;
189 1040         3173 $copy =~ s{\/$}{\/?};
190 1040         2552 $copy =~ s/\?$//;
191             }# end unless()
192              
193 1041         2040 $copy;
194             };
195            
196 1041         6469 return ($regexp, \@captures, $uri_template);
197             }# end _patternize()
198              
199              
200             # $router->match('/products/all/4/');
201             sub match
202             {
203 2042     2042 1 4239 my ($s, $full_uri, $method) = @_;
204            
205 2042   100     8194 $method ||= '*';
206 2042         3100 $method = uc($method);
207            
208 2042         3315012 my ($uri, $querystring) = split /\?/, $full_uri;
209            
210 2042 100       8690 if( exists( $s->{cache}->{"$method $full_uri"} ) )
211             {
212 1001 100       2757 if( ref($s->{cache}->{"$method $full_uri"}) )
213             {
214 1 50       3 return wantarray ? @{ $s->{cache}->{"$method $full_uri"} } : $s->{cache}->{"$method $full_uri"};
  1         7  
215             }
216             else
217             {
218 1000 50       2300 return unless defined $s->{cache}->{"$method $full_uri"};
219 1000         2899 return $s->{cache}->{"$method $full_uri"};
220             }# end if()
221             }# end if()
222            
223 1041 100 100     1574 foreach my $route ( grep { $method eq '*' || $_->{method} eq $method || $_->{method} eq '*' } @{$s->{routes}} )
  1000155         2702436  
  1041         6342  
224             {
225 500625 100       2395084 if( my @captured = ($uri =~ $route->{regexp}) )
226             {
227 1040 100       3376 if( ref($route->{target}) eq 'ARRAY' )
228             {
229 2         8 $s->{cache}->{"$method $full_uri"} = [
230             map {
231 1         3 $s->_prepare_target( $route, $_, $querystring, @captured )
232 1         2 } @{ $route->{target} }
233             ];
234 1 50       11 return wantarray ? @{ $s->{cache}->{"$method $full_uri"} } : $s->{cache}->{"$method $uri"};
  0         0  
235             }
236             else
237             {
238 1039         6508 return $s->{cache}->{"$method $full_uri"} = $s->_prepare_target( $route, "$route->{target}", $querystring, @captured );
239             }# end if()
240             }# end if()
241             }# end foreach()
242            
243 1         4 $s->{cache}->{"$method $uri"} = undef;
244 1         4 return;
245             }# end match()
246              
247              
248             sub _prepare_target
249             {
250 1041     1041   3110 my ($s, $route, $target, $querystring, @captured) = @_;
251              
252 1041 100       3675 $querystring = '' unless defined($querystring);
253 5     5   33 no warnings 'uninitialized';
  5         11  
  5         4958  
254 1041         4091 my $values = {map { my ($k,$v) = split /\=/, $_; ($k => $v) } split /&/, $querystring};
  1         2  
  1         5  
255            
256 1041         2774 my %defaults = %{ $route->{defaults} };
  1041         4408  
257 1053 50       3424 map {
258 1041         2989 my $value = @captured ? shift(@captured) : $defaults{$_};
259 1053         2650 $value =~ s/\/$//;
260 1053 100       2892 $value = $defaults{$_} unless length($value);
261 1053         3500 $values->{$_} = $value;
262 1053 100       5701 delete($defaults{$_}) if exists $defaults{$_};
263 1041         1719 } @{$route->{captures}};
264            
265 1041         3654 map { $target =~ s/\[\:\Q$_\E\:\]/$values->{$_}/g } keys %$values;
  1054         8996  
266              
267 1041         2281 my %skip = ( );
268 1050         2943 my $form_params = join '&', grep { $_ } map {
  1050         1978  
269 1054         6626 $skip{$_}++;
270 1050 50       5286 urlencode($_) . '=' . urlencode($values->{$_})
271             if defined($values->{$_});
272 1041         3499 } grep { defined($values->{$_}) } sort {lc($a) cmp lc($b)} keys %$values;
  20         48  
273 2 50       9 my $default_params = join '&', map {
274 3 50       13 urlencode($_) . '=' . urlencode($defaults{$_})
275             if defined($defaults{$_});
276 1041         3006 } grep { defined($defaults{$_}) && ! $skip{$_} } sort {lc($a) cmp lc($b)} keys %defaults;
  0         0  
277 1041         1653 my $params = join '&', ( grep { $_ } $form_params, $default_params );
  2082         4746  
278            
279 1041 50       2983 if( $target =~ m/\?/ )
280             {
281 0 0       0 return $target . ($params ? "&$params" : "" );
282             }
283             else
284             {
285 1041 100       22630 return $target . ($params ? "?$params" : "" );
286             }# end if()
287             }# end _prepare_target()
288              
289              
290             # $router->uri_for('Zipcodes', { zip => 90210 }) # eg: /Zipcodes/90201/
291             sub uri_for
292             {
293 1024     1024 1 1531 my ($s, $name, $args) = @_;
294            
295 1024 50       3737 confess "Unknown route '$name'."
296             unless my $route = $s->{names}->{$name};
297            
298 1024         2088 my $template = $route->{uri_template};
299 28 100       143 map {
300 1024         2441 $args->{$_} = $route->{defaults}->{$_}
301             unless defined($args->{$_})
302 1024         1117 } keys %{$route->{defaults}};
303 1024         1440 my %used = ( );
304 1039         7354 map {
305 1024         1850 $template =~ s!
306             \[\:$_\:\](\/?)
307             !
308 1039 100 100     7521 if( defined($args->{$_}) && length($args->{$_}) )
309             {
310 1034         1580 $used{$_}++;
311 1034         5751 "$args->{$_}$1"
312             }
313             else
314             {
315 5         15 "";
316             }# end if()
317             !egx
318 1024         1020 } @{ $route->{captures} };
319            
320 2 100 100     5 my $params = join '&', map { urlencode($_) . '=' . urlencode($args->{$_}) }
  1040         8740  
321 1024         2357 grep { defined($args->{$_}) && length($args->{$_}) && ! $used{$_} }
322             keys %$args;
323 1024 100       2179 if( length($params) )
324             {
325 2 50       8 $template .= $template =~ m/\?/ ? "&$params" : "?$params";
326             }# end if()
327            
328 1024         2970 return $template;
329             }# end uri_for()
330              
331              
332             sub route_for
333             {
334 0     0 1 0 my ($s, $uri, $method) = @_;
335            
336 0   0     0 $method ||= '*';
337 0         0 $method = uc($method);
338            
339 0 0       0 ($uri) = split /\?/, $uri
340             or return;
341            
342 0 0 0     0 foreach my $route ( grep { $method eq '*' || $_->{method} eq $method || $_->{method} eq '*' } @{$s->{routes}} )
  0         0  
  0         0  
343             {
344 0 0       0 if( my @captured = ($uri =~ $route->{regexp}) )
345             {
346 0         0 return $route;
347             }# end if()
348             }# end foreach()
349            
350 0         0 return;
351             }# end route_for()
352              
353              
354             sub urlencode
355             {
356 2108     2108 0 3417 my $toencode = shift;
357 5     5   32 no warnings 'uninitialized';
  5         10  
  5         774  
358 2108         5304 $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/esg;
  5         28  
359 2108         12288 $toencode;
360             }# end urlencode()
361              
362              
363             1;# return true:
364              
365             =pod
366              
367             =head1 NAME
368              
369             Router::Generic - A general-purpose router for the web.
370              
371             =head1 DEPRECATED
372              
373             Do not use for new code.
374              
375             =head1 SYNOPSIS
376              
377             =head2 Constructor
378              
379             use Router::Generic;
380            
381             my $router = Router::Generic->new();
382            
383             =head2 Simple Route
384              
385             $router->add_route(
386             name => 'Simple',
387             path => '/foo/bar',
388             target => '/foobar.asp',
389             );
390            
391             $router->match('/foo/bar/'); # /foobar.asp
392              
393             =head2 Simple Named Capture
394              
395             $router->add_route(
396             name => 'Zipcodes',
397             path => '/zipcodes/:code',
398             target => '/zipcode.asp'
399             );
400            
401             $router->match('/zipcodes/90210/'); # /zipcode.asp?code=90210
402             $router->match('/zipcodes/80104/'); # /zipcode.asp?code=80104
403             $router->match('/zipcodes/00405/'); # /zipcode.asp?code=00405
404              
405             =head2 Another way to spell the same thing
406              
407             $router->add_route(
408             name => 'Zipcodes',
409             path => '/zipcodes/{code}',
410             target => 'zipcode.asp'
411             );
412              
413             =head2 Eager Named Capture
414              
415             $router->add_route(
416             name => 'Eager',
417             path => '/stuff/{*Whatever}',
418             target => '/yay.asp',
419             );
420            
421             $router->match('/stuff/blah/'); # /stuff/blah
422             $router->match('/stuff/a/b/c'); # /yay.asp?Whatever=a%2Fb%2Fc - (a/b/c escaped)
423              
424             =head2 Named Capture with RegExp
425              
426             $router->add_route(
427             name => 'ZipcodeRegexp',
428             path => '/zipcodesRegExp/{code:\d{5}}',
429             target => '/zipcode.asp'
430             );
431            
432             $router->match('/zipcodesRegExp/90210/'); # /zipcode.asp?code=90210
433             $router->match('/zipcodesRegExp/80104/'); # /zipcode.asp?code=80104
434             $router->match('/zipcodesRegExp/00405/'); # /zipcode.asp?code=00405
435              
436             =head2 More Interesting
437              
438             $router->add_route(
439             name => 'WikiPage',
440             path => '/:lang/:locale/{*Article}',
441             target => '/wiki.asp',
442             defaults => {
443             Article => 'Home',
444             lang => 'en',
445             locale => 'us',
446             }
447             );
448            
449             $router->match('/en/us/'); # /wiki.asp?lang=en&locale=us&Article=Home
450             $router->match('/fr/ca/'); # /wiki.asp?lang=fr&locale=ca&Article=Home
451             $router->match('/en/us/Megalomania'); # /wiki.asp?lang=en&locale=us&Article=Megalomania
452              
453             =head2 Route with Default Values
454              
455             $router->add_route({
456             name => 'IceCream',
457             path => '/ice-cream/:flavor',
458             target => '/dessert.asp',
459             defaults => {
460             flavor => 'chocolate'
461             }
462             });
463              
464             =head2 Fairly Complex
465              
466             $router->add_route(
467             name => "ProductReviews",
468             path => '/shop/{Product}/reviews/{reviewPage:\d+}',
469             target => '/product-reviews.asp',
470             defaults => {
471             reviewPage => 1,
472             }
473             );
474            
475             $router->match('/shop/Ford-F-150/reviews/2/'); # /product-reviews.asp?Product=Ford-F-150&reviewPage=2
476              
477             =head2 List of Targets
478              
479             As of version 0.006 you can also pass an arrayref of targets and get them all back, parameterized just the same:
480              
481             $router->add_route(
482             name => "Bank",
483             path => '/banks/:city',
484             target => [ '/bank-[:city:].asp', '/bank-generic.asp' ],
485             defaults => {
486             reviewPage => 1,
487             }
488             );
489            
490             # Scalar context returns an arrayref when there are multiple targets:
491             my $matches = $router->match('/banks/Dallas/');
492             print $matches->[0]; # /bank-Dallas.asp?city=Dallas
493             print $matches->[1]; # /bank-generic.asp?city=Dallas
494            
495             # List context returns a list:
496             my @matches = $router->match('/banks/Dallas/');
497             print $matches[0]; # /bank-Dallas.asp?city=Dallas
498             print $matches[1]; # /bank-generic.asp?city=Dallas
499              
500             B<*> This whole contextual-return-types thing started up in v0.007.
501              
502             =head2 Get the URI for a Route
503              
504             my $uri = $router->uri_for('IceCream');
505             print $uri; # /ice-cream/chocolate/
506            
507             my $uri = $router->uri_for('/ProductReviews', {
508             Product => 'Tissot-T-Sport',
509             reviewPage => 3,
510             });
511             print $uri; # /shop/Tissot-T-Sport/reviews/3/
512            
513             my $uri = $router->uri_for('WikiPage', {
514             Article => 'Self-aggrandizement',
515             lang => 'en',
516             locale => 'ca',
517             });
518             print $uri; # /en/ca/Self-aggrandizement/
519            
520             my $uri = $router->uri_for('Zipcodes', {
521             code => '12345'
522             });
523             print $uri; # /zipcodes/12345/
524              
525             =head2 Get the Route for a URI
526              
527             my $route = $router->route_for('/banks/Dallas/');
528            
529             my $route = $router->route_for('/banks/Dallas/', 'POST');
530            
531             my $route = $router->route_for('/banks/Dallas/', 'GET');
532              
533             =head1 DESCRIPTION
534              
535             C provides B for the web.
536              
537             =head2 What is URL Routing?
538              
539             URL Routing is a way to connect the dots between a URL you see in your browser
540             and the page that the webserver should actually process. You could also say that
541             URL Routing is a way to abstract the URL in the browser from the page that the
542             webserver should actually process. It's all in your perspective.
543              
544             URL Routing is valuable for search engine optimization (SEO) and for reducing
545             work associated with moving files and folders around during the iterative process of building a website.
546              
547             =head2 Anatomy of a Route
548              
549             Every route must have a C, a C and a C. The C hashref is optional.
550              
551             =over 4
552              
553             =item * name (Required)
554              
555             The C parameter should be something friendly that you can remember and reference later.
556              
557             Examples are "Homepage" or "Products" or "SearchResults" - you get the picture.
558              
559             =item * path (Required)
560              
561             The C parameter describes what the incoming URL will look like in the browser.
562              
563             Examples are:
564              
565             =over 8
566              
567             =item C
568              
569             Matches C and C
570              
571             Does B match C
572              
573             =item C
574              
575             Matches C and C
576              
577             Does B match C
578              
579             =item C
580              
581             Matches C, C and C
582              
583             =item C
584              
585             You can use regular expressions to restrict what your paths match.
586              
587             Matches C but does B match C
588              
589             =back
590              
591             =item * target (Required)
592              
593             The url that should be processed instead. Any string is accepted as valid input.
594              
595             So C and C are OK.
596              
597             =item * defaults (Optional)
598              
599             The C parameter is a hashref containing values to be used in place of
600             missing values from the path.
601              
602             So if you have a path like C and your C looks like this:
603              
604             $router->add_route(
605             name => 'Pets',
606             path => '/pets/{*petName}',
607             target => '/pet.asp',
608             defaults => {
609             petName => 'Spot',
610             }
611             );
612              
613             You get this:
614              
615             $router->match('/pets/'); # /pet.asp?petName=Spot
616              
617             And this:
618              
619             $router->uri_for('Pets'); # /pets/Spot/
620              
621             The C are overridden simply by supplying a value:
622              
623             $router->match('/pets/Fluffy/'); # /pet.asp?petName=Fluffy
624              
625             And this:
626              
627             $router->uri_for('Pets', {petName => 'Sparky'}); # /pets/Sparky/
628              
629             Similarly, if you have the following route:
630              
631             $router->add_route(
632             name => "DefaultsAlways",
633             path => "/",
634             target => "/index.asp",
635             method => "*",
636             defaults => {
637             foo => "bar"
638             }
639             );
640              
641             You get this:
642              
643             $router->uri_for("DefaultsAlways"); # /?foo=bar
644              
645             $router->match('/'); # /index.asp?foo=bar
646              
647             =back
648              
649             =head2 Caching
650              
651             C provides route caching - so the expensive work of connecting a
652             uri to a route is only done once.
653              
654             =head1 PUBLIC METHODS
655              
656             =head2 add_route( name => $str, route => $str, [ defaults => \%hashref ] )
657              
658             Adds the given "route" to the routing table. Routes must be unique - so you can't
659             have 2 routes that both look like C for example. An exception will
660             be thrown if an attempt is made to add a route that already exists.
661              
662             Returns true on success.
663              
664             =head2 match( $uri )
665              
666             Returns the 'routed' uri with the intersection of parameters from C<$uri> and the
667             defaults (if any).
668              
669             Returns C if no matching route is found.
670              
671             =head2 uri_for( $routeName, \%params )
672              
673             Returns the uri for a given route with the provided params.
674              
675             Given this route:
676              
677             $router->add_route({
678             name => 'IceCream',
679             path => '/ice-cream/:flavor',
680             target => '/dessert.asp',
681             defaults => {
682             flavor => 'chocolate'
683             }
684             });
685              
686             You would get the following results depending on what params you supply:
687              
688             my $uri = $router->uri_for('IceCream');
689             print $uri; # /ice-cream/chocolate/
690            
691             my $uri = $router->uri_for('IceCream', {
692             flavor => 'strawberry',
693             });
694             print $uri; # /ice-cream/strawberry/
695              
696             =head2 route_for( $path, [ $method ] )
697              
698             Returns the route matching the path and method.
699              
700             =head1 LIMITATIONS
701              
702             Before version 0.009 there were some limitations in the grammar that prevented
703             paths like C from picking up the C<:lang> and C<:locale>
704             correctly. As of version 0.009 this works correctly.
705              
706             =head1 SIMPLE CRUD EXAMPLE
707              
708             $router->add_route(
709             name => 'CreatePage',
710             path => '/main/:type/create',
711             target => '/pages/[:type:].create.asp',
712             method => 'GET'
713             );
714            
715             $router->add_route(
716             name => 'Create',
717             path => '/main/:type/create',
718             target => '/handlers/dev.[:type:].create',
719             method => 'POST'
720             );
721            
722             $router->add_route(
723             name => 'View',
724             path => '/main/:type/{id:\d+}',
725             target => '/pages/[:type:].view.asp',
726             method => '*',
727             );
728            
729             $router->add_route(
730             name => 'List',
731             path => '/main/:type/list/{page:\d+}',
732             target => '/pages/[:type:].list.asp',
733             method => '*',
734             defaults => { page => 1 }
735             );
736            
737             $router->add_route(
738             name => 'Delete',
739             path => '/main/:type/delete/{id:\d+}',
740             target => '/handlers/dev.[:type:].delete',
741             method => 'POST'
742             );
743              
744             This works great with L.
745              
746             =head1 ACKNOWLEDGEMENTS
747              
748             Part of the path parsing logic was originally based on L by
749             Matsuno Tokuhiro L I.
750              
751             The path grammar is a copy of the route grammar used by ASP.Net 4.
752              
753             =head1 AUTHOR
754              
755             John Drago
756              
757             =head1 LICENSE
758              
759             This software is B software and may be used and redistributed under the
760             same terms as any version of Perl itself.
761              
762             =cut
763