File Coverage

blib/lib/Router/Dumb.pm
Criterion Covered Total %
statement 37 43 86.0
branch 15 26 57.6
condition 6 6 100.0
subroutine 8 9 88.8
pod 4 4 100.0
total 70 88 79.5


line stmt bran cond sub pod time code
1 1     1   337397 use 5.14.0;
  1         13  
2             package Router::Dumb 0.006;
3 1     1   472 use Moose;
  1         142657  
  1         6  
4             # ABSTRACT: yet another dumb path router for URLs
5              
6             #pod =head1 SYNOPSIS
7             #pod
8             #pod my $r = Router::Dumb->new;
9             #pod
10             #pod $r->add_route(
11             #pod Router::Dumb::Route->new({
12             #pod parts => [ qw(group :group uid :uid) ],
13             #pod target => 'pants',
14             #pod constraints => {
15             #pod group => find_type_constraint('Int'),
16             #pod },
17             #pod }),
18             #pod );
19             #pod
20             #pod my $match = $r->route( '/group/123/uid/321' );
21             #pod
22             #pod # $match->target returns 'pants'
23             #pod # $match->matches returns (group => 123, uid => 321)
24             #pod
25             #pod =head1 DESCRIPTION
26             #pod
27             #pod Router::Dumb provides a pretty dumb router. You can add routes and then ask
28             #pod how to route a given path string.
29             #pod
30             #pod Routes have a path. A path is an arrayref of names. Names that start with a
31             #pod colon are placeholders. Everything else is a literal. Literals pieces must
32             #pod appear, literally, in the string being routed. A placeholder can be satisfied
33             #pod by any value, as long as it satisfies the placeholder's constraint. If there's
34             #pod no constraint, any value works.
35             #pod
36             #pod The special part C<*> can be used to mean "...then capture everything else into
37             #pod the placeholder named C<REST>."
38             #pod
39             #pod Most of the time, you won't be calling C<add_route>, but using some other
40             #pod helper to figure out routes to add for you. Router::Dumb ships with
41             #pod L<Router::Dumb::Helper::FileMapper> and L<Router::Dumb::Helper::RouteFile>.
42             #pod
43             #pod =cut
44              
45 1     1   6686 use Router::Dumb::Route;
  1         3  
  1         33  
46              
47 1     1   9 use namespace::autoclean;
  1         1  
  1         10  
48              
49             #pod =method add_route
50             #pod
51             #pod $router->add_route(
52             #pod Router::Dumb::Route->new({
53             #pod parts => [ qw( the :path parts ) ],
54             #pod target => 'target-string',
55             #pod constraints => {
56             #pod path => $moose_tc,
57             #pod },
58             #pod })
59             #pod );
60             #pod
61             #pod This method adds a new L<route|Router::Dumb::Route> to the router.
62             #pod
63             #pod =cut
64              
65             sub add_route {
66 8     8 1 33 my ($self, $route) = @_;
67              
68 8 50       26 confess "invalid route" unless $route->isa('Router::Dumb::Route');
69              
70 8         18 my $npath = $route->normalized_path;
71 8 50       212 if (my $existing = $self->_route_at( $npath )) {
72 0         0 confess sprintf(
73             "route conflict: %s would conflict with %s",
74             $route->path,
75             $existing->path,
76             );
77             }
78              
79 8         210 $self->_add_route($npath, $route);
80             }
81              
82             #pod =method add_route_unless_exists
83             #pod
84             #pod $router->add_route_unless_exists(
85             #pod Router::Dumb::Route->new({
86             #pod parts => [ qw( the :path parts ) ],
87             #pod target => 'target-string',
88             #pod ...
89             #pod })
90             #pod );
91             #pod
92             #pod This method adds a new L<route|Router::Dumb::Route> to the router unless it
93             #pod would conflict, in which case it does nothing.
94             #pod
95             #pod =cut
96              
97             sub add_route_unless_exists {
98 0     0 1 0 my ($self, $route) = @_;
99              
100 0 0       0 confess "invalid route" unless $route->isa('Router::Dumb::Route');
101              
102 0         0 my $npath = $route->normalized_path;
103 0 0       0 return if $self->_route_at( $npath );
104              
105 0         0 $self->_add_route($npath, $route);
106             }
107              
108             #pod =method route
109             #pod
110             #pod my $match_or_undef = $router->route( $str );
111             #pod
112             #pod If the given string can be routed to a match, the L<match|Router::Dumb::Match>
113             #pod is returned. If not, the method returns false.
114             #pod
115             #pod The string must begin with a C</>.
116             #pod
117             #pod =cut
118              
119             sub route {
120 9     9 1 13065 my ($self, $str) = @_;
121              
122             # Shamelessly stolen from Path::Router 0.10 -- rjbs, 2011-07-13
123 9         22 $str =~ s|/{2,}|/|g; # xx////xx -> xx/xx
124 9         11 $str =~ s{(?:/\.)+(?:/|\z)}{/}g; # xx/././xx -> xx/xx
125 9 50       25 $str =~ s|^(?:\./)+||s unless $str eq "./"; # ./xx -> xx
126 9         12 $str =~ s|^/(?:\.\./)+|/|; # /../../xx -> xx
127 9         11 $str =~ s|^/\.\.$|/|; # /.. -> /
128 9 100       19 $str =~ s|/\z|| unless $str eq "/"; # xx/ -> xx
129              
130 9 50       37 confess "path didn't start with /" unless $str =~ s{^/}{};
131              
132 9 100       279 if (my $route = $self->_route_at($str)) {
133             # should always match! -- rjbs, 2011-07-13
134 3 50       10 confess "empty route didn't match empty path"
135             unless my $match = $route->check($str);
136              
137 3         2309 return $match;
138             }
139              
140 6         19 my @parts = split m{/}, $str;
141              
142 6         27 for my $candidate ($self->ordered_routes(
143             sub {
144 48 100 100 48   1235 ($_->part_count == @parts and $_->has_params)
      100        
145             or ($_->part_count <= @parts and $_->is_slurpy)
146             }
147             )) {
148 8 100       146 next unless my $match = $candidate->check($str);
149 3         2315 return $match;
150             }
151              
152 3         19 return;
153             }
154              
155             has _route_map => (
156             is => 'ro',
157             isa => 'HashRef',
158             init_arg => undef,
159             default => sub { {} },
160             traits => [ 'Hash' ],
161             handles => {
162             _routes => 'values',
163             _route_at => 'get',
164             _add_route => 'set',
165             },
166             );
167              
168             #pod =method ordered_routes
169             #pod
170             #pod my @routes = $router->ordered_routes;
171             #pod
172             #pod This method returns the router's routes, in the order that they will be
173             #pod checked. You probably do not want to use this method unless you really know
174             #pod what you're doing.
175             #pod
176             #pod =cut
177              
178             sub ordered_routes {
179 6     6 1 13 my ($self, $filter) = @_;
180              
181 5 50       131 return sort { $b->part_count <=> $a->part_count
182             || $a->is_slurpy <=> $b->is_slurpy }
183 6 50       162 grep { $filter ? $filter->() : 1 }
  48         91  
184             $self->_routes;
185             }
186              
187             1;
188              
189             __END__
190              
191             =pod
192              
193             =encoding UTF-8
194              
195             =head1 NAME
196              
197             Router::Dumb - yet another dumb path router for URLs
198              
199             =head1 VERSION
200              
201             version 0.006
202              
203             =head1 SYNOPSIS
204              
205             my $r = Router::Dumb->new;
206              
207             $r->add_route(
208             Router::Dumb::Route->new({
209             parts => [ qw(group :group uid :uid) ],
210             target => 'pants',
211             constraints => {
212             group => find_type_constraint('Int'),
213             },
214             }),
215             );
216              
217             my $match = $r->route( '/group/123/uid/321' );
218            
219             # $match->target returns 'pants'
220             # $match->matches returns (group => 123, uid => 321)
221              
222             =head1 DESCRIPTION
223              
224             Router::Dumb provides a pretty dumb router. You can add routes and then ask
225             how to route a given path string.
226              
227             Routes have a path. A path is an arrayref of names. Names that start with a
228             colon are placeholders. Everything else is a literal. Literals pieces must
229             appear, literally, in the string being routed. A placeholder can be satisfied
230             by any value, as long as it satisfies the placeholder's constraint. If there's
231             no constraint, any value works.
232              
233             The special part C<*> can be used to mean "...then capture everything else into
234             the placeholder named C<REST>."
235              
236             Most of the time, you won't be calling C<add_route>, but using some other
237             helper to figure out routes to add for you. Router::Dumb ships with
238             L<Router::Dumb::Helper::FileMapper> and L<Router::Dumb::Helper::RouteFile>.
239              
240             =head1 PERL VERSION
241              
242             This library should run on perls released even a long time ago. It should work
243             on any version of perl released in the last five years.
244              
245             Although it may work on older versions of perl, no guarantee is made that the
246             minimum required version will not be increased. The version may be increased
247             for any reason, and there is no promise that patches will be accepted to lower
248             the minimum required perl.
249              
250             =head1 METHODS
251              
252             =head2 add_route
253              
254             $router->add_route(
255             Router::Dumb::Route->new({
256             parts => [ qw( the :path parts ) ],
257             target => 'target-string',
258             constraints => {
259             path => $moose_tc,
260             },
261             })
262             );
263              
264             This method adds a new L<route|Router::Dumb::Route> to the router.
265              
266             =head2 add_route_unless_exists
267              
268             $router->add_route_unless_exists(
269             Router::Dumb::Route->new({
270             parts => [ qw( the :path parts ) ],
271             target => 'target-string',
272             ...
273             })
274             );
275              
276             This method adds a new L<route|Router::Dumb::Route> to the router unless it
277             would conflict, in which case it does nothing.
278              
279             =head2 route
280              
281             my $match_or_undef = $router->route( $str );
282              
283             If the given string can be routed to a match, the L<match|Router::Dumb::Match>
284             is returned. If not, the method returns false.
285              
286             The string must begin with a C</>.
287              
288             =head2 ordered_routes
289              
290             my @routes = $router->ordered_routes;
291              
292             This method returns the router's routes, in the order that they will be
293             checked. You probably do not want to use this method unless you really know
294             what you're doing.
295              
296             =head1 AUTHOR
297              
298             Ricardo Signes <cpan@semiotic.systems>
299              
300             =head1 CONTRIBUTORS
301              
302             =for stopwords Karen Etheridge Ricardo Signes
303              
304             =over 4
305              
306             =item *
307              
308             Karen Etheridge <ether@cpan.org>
309              
310             =item *
311              
312             Ricardo Signes <rjbs@semiotic.systems>
313              
314             =back
315              
316             =head1 COPYRIGHT AND LICENSE
317              
318             This software is copyright (c) 2022 by Ricardo Signes.
319              
320             This is free software; you can redistribute it and/or modify it under
321             the same terms as the Perl 5 programming language system itself.
322              
323             =cut