File Coverage

blib/lib/Path/Map.pm
Criterion Covered Total %
statement 61 61 100.0
branch 20 20 100.0
condition 4 5 80.0
subroutine 18 18 100.0
pod 4 4 100.0
total 107 108 99.0


line stmt bran cond sub pod time code
1             package Path::Map;
2              
3 2     2   55608 use strict;
  2         7  
  2         135  
4 2     2   11 use warnings;
  2         4  
  2         129  
5              
6             =head1 NAME
7              
8             Path::Map - map paths to handlers
9              
10             =head1 VERSION
11              
12             0.04
13              
14             =cut
15              
16             our $VERSION = '0.04';
17              
18             =head1 SYNOPSIS
19              
20             my $mapper = Path::Map->new(
21             '/x/y/z' => 'XYZ',
22             '/a/b/c' => 'ABC',
23             '/a/b' => 'AB',
24              
25             '/date/:year/:month/:day' => 'Date',
26              
27             # Every path beginning 'SEO' is mapped the same.
28             '/seo/*' => 'slurpy',
29             );
30              
31             if (my $match = $mapper->lookup('/date/2013/12/25')) {
32             # $match->handler is 'Date'
33             # $match->variables is { year => 2012, month => 12, day => 25 }
34             }
35              
36             # Add more mappings later
37             $mapper->add_handler($path => $target)
38              
39             =head1 DESCRIPTION
40              
41             This class maps paths to handlers. The paths can contain variable path
42             segments, which match against any incoming path segment, where the matching
43             segments are saved as named variables for later retrieval.
44              
45             Note that the handlers being mapped to can be any arbitrary data, not just
46             strings as illustrated in the synopsis.
47              
48             =head2 Comparison with Path::Router
49              
50             This class fulfills some of the same jobs as L, with slightly
51             different design goals. Broadly speaking, Path::Map is a lighter, faster,
52             but less featureful version of Path::Router.
53              
54             I've listed a few points of difference here to help highlight the pros and
55             cons of each class.
56              
57             =over
58              
59             =item Speed
60              
61             The main goal for Path::Map is lookup speed. Path::Router uses regexes to
62             do lookups, but Path::Map uses hash lookups. Path::Map seems to be at
63             least an order of magnitude faster based on my benchmarks, and performance
64             doesn't degrade with the number of routes that are added. The main source of
65             performance degradation for Path::Map is path I, Path::Router
66             degrades less with depth but more with width.
67              
68             This approach also means that the order in which routes are added makes no
69             difference to Path::Map.
70              
71             =item Reversibility
72              
73             Path::Router has a specific aim of being reversible. That is to say you can
74             construct a path from a set of parameters. Path::Map does not currently
75             have this ability, patches welcome!
76              
77             =item Validation
78              
79             Path::Map has no built-in ability to validate path variables in any way.
80             Obviously validation can be done externally after the fact, but that doesn't
81             allow for the more complex routing rules possible in Path::Router.
82              
83             In other words, it's not possible for Path::Map to differentiate two path
84             templates which differ only in the variable segments (e.g. C<< /blog/:name >>
85             vs C<< /blog/:id >> where C matches C<\d+> and C matches C<\D+>).
86              
87             =item Dependencies
88              
89             Path::Map has a very small dependency chain, whereas Path::Router is based
90             on L, so has a relatively high dependency footprint. If you're already
91             using Moose, there's obviously no additional cost in using Path::Router.
92              
93             =back
94              
95             =cut
96              
97 2     2   11 use List::Util qw( reduce );
  2         8  
  2         270  
98 2     2   2023 use List::MoreUtils qw( uniq natatime );
  2         2985  
  2         167  
99              
100 2     2   1051 use Path::Map::Match;
  2         6  
  2         1579  
101              
102             =head1 METHODS
103              
104             =head2 new
105              
106             $mapper = $class->new(@pairs)
107              
108             The constructor.
109              
110             Takes an even-sized list and passes each pair to L.
111              
112             =cut
113              
114             sub new {
115 16     16 1 43 my $class = shift;
116 16         36 my $self = bless {}, $class;
117              
118 16         115 my $iterator = natatime 2, @_;
119 16         96 while (my @pair = $iterator->()) {
120 6         18 $self->add_handler(@pair);
121             }
122              
123 16         93 return $self;
124             }
125              
126             =head2 add_handler
127              
128             $mapper->add_handler($path_template, $handler)
129              
130             Adds a single item to the mapping.
131              
132             The path template should be a string comprising slash-delimited path segments,
133             where a path segment may contain any character other than the slash. Any
134             segment beginning with a colon (C<:>) denotes a mandatory named variable.
135             Empty segments, including those implied by leading or trailing slashes are
136             ignored.
137              
138             For example, these are all identical path templates:
139              
140             /a/:var/b
141             a/:var/b/
142             //a//:var//b//
143              
144             The order in which these templates are added has no bearing on the lookup,
145             except that later additions with identical templates overwrite earlier ones.
146              
147             Templates containing a segment consisting entirely of C<'*'> match instantly
148             at that point, with all remaining segments assigned to the C of the
149             match as normal, but without any variable names. Any remaining segments in the
150             template are ignored, so it only makes sense for the wildcard to be the last
151             segment.
152              
153             my $map = Path::Map->new('foo/:foo/*' => 'Something');
154             my match = $map->lookup('foo/bar/baz/qux');
155             $match->variables; # { foo => 'bar' }
156             $match->values; # [ qw( bar baz qux ) ]
157              
158             =cut
159              
160             sub add_handler {
161 7     7 1 636 my ($self, $path, $handler) = @_;
162 7         13 my $class = ref $self;
163              
164 7         21 my @parts = $self->_tokenise_path($path);
165 7         15 my (@vars, $slurpy);
166             my $mapper = reduce {
167 22 100   22   98 $b =~ s{^:(.*)}{/} and push @vars, $1;
168 22 100       142 $b eq '*' and $slurpy = 1;
169 22 100 66     63 $slurpy ? $a : $a->_map->{$b} ||= $class->new;
170 7         104 } $self, @parts;
171              
172 7         38 $mapper->_set_target($handler);
173 7         19 $mapper->_set_variables(\@vars);
174 7 100       713 $mapper->_set_slurpy if $slurpy;
175              
176 7         42 return;
177             }
178              
179             =head2 lookup
180              
181             $match = $mapper->lookup($path)
182              
183             Returns a L object if the path matches a known path
184             template, C otherwise.
185              
186             The two main methods on the match object are:
187              
188             =over
189              
190             =item handler
191              
192             The handler that was matched, identical to whatever was originally passed to
193             L.
194              
195             =item variables
196              
197             The named path variables as a hashref.
198              
199             =back
200              
201             =cut
202              
203             sub lookup {
204 16     16 1 4940 my ($mapper, $path) = @_;
205              
206 16         42 my @parts = $mapper->_tokenise_path($path);
207 16         29 my @values;
208             my $slurpy_match;
209              
210 16         20 while () {
211 66 100       126 if ($mapper->_is_slurpy) {
212 6         30 $slurpy_match = Path::Map::Match->new(
213             mapper => $mapper,
214             values => [ @values, @parts ],
215             );
216             }
217              
218 66 100       165 if (my $segment = shift @parts) {
    100          
219 54         138 my $map = $mapper->_map;
220              
221 54         66 my $next;
222 54 100       163 if ($next = $map->{$segment}) {
    100          
    100          
223             # Nothing
224             }
225             elsif ($next = $map->{'/'}) {
226 30         660 push @values, $segment;
227             }
228             elsif ($slurpy_match) {
229 3         12 return $slurpy_match;
230             }
231             else {
232 1         7 return undef;
233             }
234              
235 50         101 $mapper = $next;
236             }
237             elsif (defined $mapper->_target) {
238 9         40 return Path::Map::Match->new(
239             mapper => $mapper,
240             values => \@values
241             );
242             }
243             else {
244 3         19 return undef;
245             }
246             }
247             }
248              
249             =head2 handlers
250              
251             @handlers = $mapper->handlers()
252              
253             Returns all of the handlers in no particular order.
254              
255             =cut
256              
257             sub handlers {
258 9     9 1 285 my $self = shift;
259              
260 9         18 return uniq(
261 9         19 grep defined, $self->_target, map $_->handlers, values %{ $self->_map }
262             );
263             }
264              
265             sub _tokenise_path {
266 23     23   38 my ($self, $path) = @_;
267              
268 23         151 return grep length, split '/', $path;
269             }
270              
271             sub _is_slurpy {
272 66     66   214 return defined $_[0]->{slurpy};
273             }
274              
275 3     3   7 sub _set_slurpy { $_[0]->{slurpy} = 1 }
276              
277 81   100 81   488 sub _map { $_[0]->{map} ||= {} }
278              
279 33     33   110 sub _target { $_[0]->{target} }
280 7     7   23 sub _set_target { $_[0]->{target} = $_[1] }
281              
282 12     12   63 sub _variables { $_[0]->{vars} }
283 7     7   13 sub _set_variables { $_[0]->{vars} = $_[1] }
284              
285             =head1 SEE ALSO
286              
287             L
288              
289             =head1 AUTHOR
290              
291             Matt Lawrence Emattlaw@cpan.orgE
292              
293             =head1 COPYRIGHT
294              
295             This library is free software; you can redistribute it and/or modify it under
296             the same terms as Perl itself.
297              
298             =cut
299              
300             1;