File Coverage

blib/lib/Plack/App/URLMux.pm
Criterion Covered Total %
statement 208 222 93.6
branch 82 100 82.0
condition 18 25 72.0
subroutine 31 34 91.1
pod 3 5 60.0
total 342 386 88.6


line stmt bran cond sub pod time code
1             package Plack::App::URLMux;
2              
3 9     9   531249 use strict;
  9         105  
  9         261  
4 9     9   184 use 5.008_001;
  9         30  
5             our $VERSION = '0.05';
6              
7 9     9   2219 use parent qw(Plack::Component);
  9         2436  
  9         49  
8 9     9   104839 use Carp qw(croak);
  9         25  
  9         530  
9              
10 9     9   61 use constant MAX_FINE => 65535; # if URL will contain url-path segments more than this number - increment it :)
  9         23  
  9         510  
11              
12             # map[] structure constants
13 9     9   53 use constant _keys => 0; # hash of url-path segments => map[]
  9         19  
  9         381  
14 9     9   53 use constant _level => 1; # level of map tree
  9         21  
  9         398  
15 9     9   53 use constant _quant => 2; # rule for quantifier at branch, this value sensitive for _named array
  9         18  
  9         375  
16 9     9   59 use constant _app => 3; # reference to app on this leaf, undefined if does not mounted any
  9         16  
  9         391  
17 9     9   65 use constant _named => 4; # reference to map[], if url-path has named parameter then mapping searching in it
  9         17  
  9         450  
18 9     9   56 use constant _names => 5; # array of [parameter, index], if app has parameters in the url format
  9         19  
  9         435  
19 9     9   52 use constant _params => 6; # array of pairs name=>value of input parameters specified at map
  9         19  
  9         391  
20 9     9   49 use constant _quants => 7; # array of quantifiers of each url-path segments for mounted app
  9         18  
  9         407  
21              
22             # _quant array constants
23 9     9   52 use constant _quant_n => 0; # start range quanifier
  9         19  
  9         483  
24 9     9   50 use constant _quant_m => 1; # end range qunatifier, -1 unknown maximum as possible
  9         23  
  9         416  
25 9     9   55 use constant _quant_r => 2; # rest of posible search path, -1 unknown, unusefull, FIXME delete this
  9         20  
  9         409  
26 9     9   51 use constant _quant_h => 3; # greedy/lazy flag for search path and gather subpathes for parameter, 0 lazy, 1 greedy
  9         18  
  9         380  
27              
28             # _params array constants
29 9     9   47 use constant _param_name => 0;
  9         19  
  9         379  
30 9     9   49 use constant _param_index => 1;
  9         23  
  9         415  
31              
32             # matched array constants
33 9     9   48 use constant _match_map => 0;
  9         16  
  9         338  
34 9     9   43 use constant _match_length => 1;
  9         23  
  9         350  
35 9     9   58 use constant _match_fine => 2;
  9         15  
  9         466  
36 9     9   52 use constant _match_matching => 3;
  9         19  
  9         378  
37              
38             # matching array constants
39 9     9   51 use constant _matching_index => 0;
  9         16  
  9         343  
40 9     9   47 use constant _matching_fine => 1;
  9         22  
  9         15437  
41              
42 0     0 1 0 sub mount { shift->map(@_) }
43              
44             sub map {
45 29     29 1 1164 my $self = shift;
46 29         62 my $location = shift;
47 29         44 my $app = shift;
48 29         61 my $input_params = \@_;
49              
50              
51 29 100       81 if ($location eq '') {
52             croak "location 'not found' has already mount app"
53 1 50 33     33 if exists $self->{_mapping_not_found} and defined $self->{_mapping_not_found};
54 1         3 $self->{_mapping_not_found} = $app;
55 1         2 return;
56             }
57              
58 28         50 my $host = '*'; #any host
59 28 100       83 if ($location =~ m/^https?:\/\/(.*?)(\/.*)/o) {
60 5         20 $host = $1;
61 5         14 $location = $2;
62             }
63              
64 28 50       103 croak "Path '$location' need to start with '/'"
65             unless $location =~ m/^\//o;
66 28         127 $location =~ s/^\/|\/$//go;
67              
68 28         82 my @paths = split('/', $location);
69              
70 28         86 my ($index, $params, $quants, $quant) = (0, [], [], undef);
71 28   100     367 my $map = $self->{_mapping}->{$host} ||= [{}, $index, [1, 0]]; # Zero mapping for URL '/'
72 28         53 $index++;
73 28         56 foreach my $path (@paths) {
74 30         51 my $r = @paths - $index;
75              
76 30 100       138 if ($path =~
77             /^
78             \:
79              
80             ([^\:\+\*\?\{\}\,]+?) (?# 1: name of parameter )
81              
82             (?:
83             (?:
84             (\+|\*|\?) (?# 2: '+|*|?' quantifier )
85             (\??) (?# 3: '?' greedy|lazy flag )
86             )
87             | (?:
88             \{
89             (\d+) (?# 4: start range quantifier )
90             (?:
91             (\,) (?# 5: range delimeter )
92             (\d+)* (?# 6: end range quantifier )
93             )*
94             \}
95             (\??) (?# 7: '?' greedy|lazy flag )
96             )
97             )?
98              
99             $/xo
100             ){
101 15 50 100     144 my ($name, $quant, $n, $m, $h) = ($1, $2 || $5, $4, $6, (($3 || $7) ? 0 : 1));
      33        
102              
103 15 100       41 ($n, $m) = (0, -1 ) if $quant eq '*';
104 15 100       37 ($n, $m) = (1, -1 ) if $quant eq '+';
105 15 50       32 ($n, $m) = (0, 1 ) if $quant eq '?';
106 15 100       39 ($n, $m) = ($n, ($m ? $m : -1) ) if $quant eq ',';
    100          
107 15 50       45 ($n, $m) = ($n = ($n ? $n : 1), $n ) if $quant eq '';
    100          
108              
109             #check _named now named must be array but we must check
110 15         93 push(@$params, [$name, $index]);
111 15         49 push(@$quants, [$n, $m, $r, $h]);
112              
113 15   100     67 $map->[_named] ||= {};
114 15   100     124 $map = $map->[_named]->{"$n.$m"} ||= [{}, $index, [$n, $m]];
115 15         28 $index++;
116 15         39 next;
117             }
118              
119 15 50       45 if ($path =~ /^\:/o) {
120 0         0 croak "url '$location' is wrong, syntax sub path '$path' is incorrent, expect :[alphanum](*|+|?|{n,m})?\??";
121             }
122              
123             #FIXME check than name contain only valid chars need check RFC for that
124              
125 15   100     85 $map = $map->[_keys]->{$path} ||= [{}, $index, [1, 1]];
126 15         54 push(@$quants, [1, 1, $r, 1]);
127 15         23 $index++;
128 15         32 next;
129              
130             }
131              
132 28 50       70 croak "/$location has already mount app"
133             if defined $map->[_app];
134              
135 28 100       178 @$map[_app,_params,_names,_quants] = ($app, $input_params, @$params ? $params : undef, $quants);
136              
137             }
138              
139       0     sub _parse_quant {
140              
141             }
142              
143             sub prepapre_app {
144 0     0 0 0 my $self = shift;
145             }
146              
147             sub call {
148 59     59 1 221313 my ($self, $env) = @_;
149 59         131 my ($matches, $params);
150              
151             my($http_host, $server_name, $script_name, $path_info)
152 59         115 = @{$env}{qw( HTTP_HOST SERVER_NAME SCRIPT_NAME PATH_INFO )};
  59         217  
153              
154             #FIXME possible BUG
155             # is there cases when port is not the same in HTTP_HOST and SERVER_PORT?
156 59 50 33     378 if ($http_host and $env->{SERVER_PORT}) {
157 59         168 $http_host =~ s/:\d+$//o;
158             }
159              
160 59         234 my @path = split('/', $path_info);
161 59         122 shift @path; # remove zero
162              
163 59         173 my $mapping = $self->{_mapping};
164              
165             my $matched =
166             _matched(
167             $self->search(
168 59   66     530 $mapping->{$http_host} || $mapping->{$server_name} || $mapping->{'*'}
169             , my $i = 0
170             , \@path
171             , @path + 0
172             , []
173             )
174             );
175              
176 59 100       213 unless ($matched) {
177             return [404, [ 'Content-Type' => 'text/plain' ], [ "Not Found" ]]
178 1 50       7 unless $self->{_mapping_not_found};
179 1         13 return $self->{_mapping_not_found}->($env);
180             }
181              
182             #now we have first matched rule and match path, we need fill params if they exists
183              
184 58         118 my ($match, $matching) = @{$matched}[_match_map,_match_matching];
  58         141  
185              
186 58 100       155 if (defined $match->[_names]) {
187 29         52 my ($i, $j);
188             $params = [
189             map {
190 34 100       366 $_->[_param_name] =>
    100          
191             (
192             (
193             ($j = $matching->[$_->[_param_index]-1]->[_matching_index] - 1)
194             -
195             ($i = $_->[_param_index] - 1 > 0 ? $matching->[$_->[_param_index] - 2]->[_matching_index] : 0)
196             ) >= 0
197             ?
198             [(@path[$i..$j])]
199             :
200             []
201             )
202 29         51 } @{$match->[_names]}
  29         76  
203             ];
204             }
205              
206 58         128 my $index = $matching->[-1]->[_matching_index];
207             #clone input params, couse they may be mutated by middlewire
208             @$env{qw( plack.urlmux.params.map plack.urlmux.params.url SCRIPT_NAME PATH_INFO )}
209             = (
210 58 100 100     97 [@{$match->[_params]}]
  58 50       693  
    100          
    100          
211             , $params || []
212             , ($script_name ? $script_name : $index ? '/' : '') . join('/', (@path[0..($index-1)]))
213             , ($index == @path ? @path ? '' : '/' : '/') . join('/', (@path[$index..(@path-1)]))
214             );
215              
216             return $self->response_cb($match->[_app]->($env), sub {
217 58     58   4086 @{$env}{qw( PATH_INFO SCRIPT_NAME )} = ($path_info, $script_name);
  58         195  
218 58         252 });
219             }
220              
221             sub search {
222 202     202 0 481 my ($self, $map, $index, $parts, $l, $matching) = @_;
223              
224 202         371 my $path = $parts->[$index];
225 202         513 my ($_app, $_keys, $_named) = @$map[_app,_keys,_named];
226              
227 202         320 my $matches = [];
228              
229 202 100       514 if (exists $_keys->{$path}) {
230 30 50       87 if ($index < $l) {
231 30         63 push(@$matches, (@{$self->search($_keys->{$path}, $index + 1, $parts, $l, [(@$matching), [$index+1, 0]])}));
  30         209  
232             }
233             else {
234 0 0       0 if (defined $_keys->{$path}->[_app]) {
235 0         0 my $match = [$_keys->{$path}, $index + 1, 0, [(@$matching), [$index+1, 0]]];
236 0         0 push (@$matches, $match);
237 0         0 map {$match->[_match_fine] += $_->[_matching_fine]} @{$match->[_match_matching]};
  0         0  
  0         0  
238             }
239             }
240             }
241              
242 202 100       440 if (defined $_named) {
243 47         77 foreach my $quant (values %{$_named}) {
  47         166  
244 77         134 my ($n, $m) = @{$quant->[_quant]}[_quant_n,_quant_m];
  77         233  
245 77 100       231 my ($ln, $lm) = ($index + $n, ($m==-1 ? MAX_FINE : $index + $m));
246 77         138 my $matches_ = [];
247 77 100       246 for ($ln .. ($lm > $l ? $l : $lm)) {
248 168 100       321 if ($_ < $l) {
249 113         167 push(@$matches_ , (@{$self->search($quant, $_, $parts, $l, [(@$matching), [$_, $lm - $ln + 1]])}));
  113         376  
250             }
251             else {
252 55 100       123 if (defined $quant->[_app]) {
253 39         132 my $match = [$quant, $_, 0, [(@$matching), [$_, $lm - $ln + 1]]];
254 39         82 push(@$matches_, $match);
255 39         53 map {$match->[_match_fine] += $_->[_matching_fine]} @{$match->[_match_matching]};
  60         139  
  39         106  
256             }
257             }
258             }
259 77 100       188 next unless @$matches_;
260 55         140 push(@$matches, _matched_greedy($matches_, $index));
261             }
262             }
263              
264 202 100       485 if (defined $_app) {
265 151         444 my $match = [$map, $index, 0, [(@$matching), [$index, 0]]];
266 151         305 push(@$matches, $match);
267 151         254 map {$match->[_match_fine] += $_->[_matching_fine]} @{$match->[_match_matching]};
  284         577  
  151         374  
268             }
269              
270 202         582 return $matches;
271              
272             }
273              
274             sub _matched {
275 59     59   128 my ($matches) = @_;
276              
277 59 100       154 return unless @$matches;
278 58 100       162 return $matches->[0] if @$matches == 1;
279              
280 37         58 my $match;
281              
282 37         88 foreach (@$matches) {
283 93 100       199 unless (defined $match) {
284 37         64 $match = $_;
285 37         80 next;
286             }
287 56 100       191 if ($_->[_match_length] < $match->[_match_length]) {
288 38         95 next;
289             }
290 18 100       54 if ($_->[_match_length] > $match->[_match_length]) {
291 2         5 $match = $_;
292 2         4 next;
293             }
294 16 100       34 if ($_->[_match_fine] < $match->[_match_fine]) {
295 5         9 $match = $_;
296 5         13 next;
297             }
298             }
299 37         87 return $match;
300             }
301              
302             sub _matched_greedy {
303 55     55   125 my ($matches, $index) = @_;
304              
305 55 50       110 return unless @$matches;
306 55 100       180 return $matches->[0] if @$matches == 1;
307              
308 35         91 my $greedy = $matches->[0]->[_match_map]->[_quants]->[$index-1]->[_quant_h];
309              
310 35         88 my $match;
311 35         70 foreach (@$matches) {
312 111 100       209 unless (defined $match) {
313 35         62 $match = $_;
314 35         62 next;
315             }
316 76 100       433 if ($_->[_match_length] < $match->[_match_length]) {
317 10         19 next;
318             }
319 66 100       149 if ($_->[_match_length] > $match->[_match_length] ) {
320 63         87 $match = $_;
321 63         94 next;
322             }
323 3 50       9 if ($greedy) {
324 3 50       14 if ($_->[_match_matching]->[$index-1]->[_matching_index] > $match->[_match_matching]->[$index-1]->[_matching_index]) {
325 0         0 $match = $_;
326 0         0 next;
327             }
328             }
329             else {
330 0 0       0 if ($_->[_match_matching]->[$index-1]->[_matching_index] < $match->[_match_matching]->[$index-1]->[_matching_index]) {
331 0         0 $match = $_;
332 0         0 next;
333             }
334             }
335             }
336              
337 35         157 return $match;
338             }
339              
340             1;
341              
342             __END__