File Coverage

blib/lib/CGI/Application/URIMapping.pm
Criterion Covered Total %
statement 186 209 89.0
branch 56 78 71.7
condition 42 101 41.5
subroutine 30 31 96.7
pod 2 2 100.0
total 316 421 75.0


line stmt bran cond sub pod time code
1             package CGI::Application::URIMapping;
2              
3 3     3   51394 use strict;
  3         6  
  3         94  
4 3     3   16 use warnings;
  3         6  
  3         78  
5              
6 3     3   6004 use CGI;
  3         50067  
  3         23  
7 3     3   3962 use CGI::Application;
  3         18880  
  3         109  
8 3     3   2879 use List::MoreUtils qw(uniq);
  3         3770  
  3         270  
9 3     3   2370 use URI::Escape;
  3         4346  
  3         196  
10              
11 3     3   20 use base qw/CGI::Application::Dispatch Exporter/;
  3         5  
  3         3302  
12              
13             our %EXPORT_TAGS = (
14             constants => [
15             qw/URI_IS_PERMALINK URI_UNKNOWN_PARAM URI_PATH_PARAM_IN_QUERY/,
16             qw/URI_PARAM_NOT_IN_ORDER URI_OMITTABLE_PARAM/,
17             ],
18             );
19             $EXPORT_TAGS{all} = [ uniq map { @$_ } values %EXPORT_TAGS ];
20             our @EXPORT_OK = @{$EXPORT_TAGS{all}};
21              
22             our $VERSION = 0.04;
23              
24 3     3   74006 use constant URI_IS_PERMALINK => 0;
  3         9  
  3         201  
25 3     3   16 use constant URI_UNKNOWN_PARAM => 1;
  3         6  
  3         120  
26 3     3   29 use constant URI_PATH_PARAM_IN_QUERY => 2;
  3         5  
  3         125  
27 3     3   15 use constant URI_PARAM_NOT_IN_ORDER => 3;
  3         7  
  3         117  
28 3     3   14 use constant URI_OMITTABLE_PARAM => 4;
  3         6  
  3         1717  
29              
30             our %dispatch_table;
31             our %uri_table;
32             our %app_init_map;
33              
34             sub register {
35 4     4 1 79 my ($self, @entries) = @_;
36              
37 4   33     45 my $dispatch_table = ($dispatch_table{ref($self) || $self} ||= {});
      100        
38 4   33     37 my $uri_table = ($uri_table{ref($self) || $self} ||= {});
      100        
39            
40 4         9 foreach my $entry (@entries) {
41 4 100       36 $entry = {
42             path => $entry,
43             } unless ref $entry;
44 4   33     35 my $app = $entry->{app} || (caller)[0];
45 4   50     25 my $host = $entry->{host} || '*';
46 4   100     23 my $proto = $entry->{protocol} || 'http';
47 4         10 my $uri_table_entry;
48             my $rm;
49 4 100       12 unless ($rm = $entry->{rm}) {
50 3 50       12 unless (ref $entry->{path}) {
51 3 50       36 (split '/:', $entry->{path}, 2)[0] =~ m|([^/]+)/?$|
52             and $rm = $1;
53             }
54             }
55 4 50       23 die "no 'rm'\n" unless $rm;
56 4 50       14 if (ref($entry->{path}) eq 'ARRAY') {
57 0         0 die "unexpected number of elements in 'path'\n"
58 0 0 0     0 unless @{$entry->{path}} && @{$entry->{path}} % 2 == 0;
  0         0  
59 0         0 while (@{$entry->{path}}) {
  0         0  
60 0         0 my $path = shift @{$entry->{path}};
  0         0  
61 0         0 my $action = shift @{$entry->{path}};
  0         0  
62 0         0 $action->{app} = $app;
63 0         0 $action->{rm} = $rm;
64 0   0     0 my $host2 = delete $action->{host} || $host;
65 0   0     0 my $proto2 = delete $action->{protocol} || $proto;
66 0   0     0 $dispatch_table->{$host} ||= [];
67 0         0 push @{$dispatch_table->{$host2}}, $path, $action;
  0         0  
68 0   0     0 $uri_table_entry ||= _build_uri_table_entry({
      0        
69             %$entry,
70             protocol => $proto2,
71             host => $host2,
72             path => $path,
73             query => delete $action->{query} || [],
74             action => $action,
75             });
76             }
77             } else {
78 4         17 my $action = {
79             app => $app,
80             rm => $rm,
81             };
82 4   100     23 $dispatch_table->{$host} ||= [];
83 4         36 push @{$dispatch_table->{$host}}, $entry->{path}, $action;
  4         17  
84 4   100     62 $uri_table_entry ||= _build_uri_table_entry({
      33        
85             %$entry,
86             protocol => $proto,
87             host => $host,
88             path => $entry->{path},
89             query => $entry->{query} || [],
90             action => $action,
91             });
92             }
93 4   50     24 $uri_table_entry->{build_uri} = $entry->{build_uri} || undef;
94 4         17 $uri_table->{"$app/$rm"} = $uri_table_entry;
95 4 50       13 unless ($app_init_map{$app}) {
96 4         10 $app_init_map{$app} = 1;
97 3     3   25 no strict 'refs';
  3         8  
  3         8268  
98 4   33     20 my $self_klass = ref($self) || $self;
99             $app->add_callback(
100             'prerun',
101             sub {
102 1     1   1172 my $app = shift;
103 1         4 _setup_runmodes($app, $self_klass);
104             },
105 4         64 );
106 4         42 *{"${app}::_uri_mapping"} = sub {
107 15     15   39 _uri_mapping_of($self_klass, $app, $_[1]);
108 4         125 };
109             }
110             };
111             }
112              
113             sub dispatch_args {
114 1     1 1 3935 my $self = shift;
115 1   33     19 my $dispatch_table = ($dispatch_table{ref($self) || $self} ||= {});
      50        
116            
117             return {
118 1   50     9 prefix => '',
119             table => $dispatch_table->{CGI::virtual_host()}
120             || $dispatch_table->{'*'}
121             || {},
122             };
123             }
124              
125             *CGI::Application::all_param = sub {
126 6     6   2147 my $app = shift;
127            
128 6 100       17 if (@_ == 1) {
129 5         5 my $n = shift;
130 5         14 my $v = $app->param($n);
131 5 100 66     76 return $v
132             if defined $v && $v ne '';
133 3         8 return $app->query->param($n);
134             }
135            
136 1         4 $app->param(@_);
137             };
138              
139             *CGI::Application::uri_mapping = sub {
140 15     15   1523 my $app = shift;
141 15         18 my $mapping;
142            
143 15         16 eval {
144 15         39 $mapping = $app->_uri_mapping(@_);
145             };
146 15 50       33 die "no mapping for $app, did you register the class?\n"
147             unless $mapping;
148            
149 15         45 $mapping;
150             };
151              
152             *CGI::Application::build_uri = sub {
153 8     8   3379 my ($app, $args) = @_;
154 8 100 50     33 my $rm = $args->{rm} || undef
155             if ref($args) eq 'HASH';
156            
157 8         34 _build_uri($app->uri_mapping($rm), $args);
158             };
159              
160             *CGI::Application::validate_uri = sub {
161 6     6   4391 my ($app, $args) = @_;
162 6   50     51 my $mapping = $app->uri_mapping($args->{rm} || undef);
163            
164 6   50     38 return _validate_uri($mapping, $app, $args->{extra} || []);
165             };
166              
167             *CGI::Application::normalize_uri = sub {
168 0     0   0 my ($app, $args) = @_;
169 0   0     0 my $mapping = $app->uri_mapping($args->{rm} || undef);
170            
171             return
172 0 0 0     0 if _validate_uri($mapping, $app, $args->{extra} || [])
173             == URI_IS_PERMALINK;
174            
175 0   0     0 return $app->redirect(_build_uri(
176             $mapping,
177             {
178             rm => $args->{rm} || undef,
179             params => [
180             $app,
181             ],
182             },
183             ));
184             };
185              
186             sub _run_modes_of {
187 1     1   2 my ($self, $app) = @_;
188 1   33     8 my $dispatch_table = ($dispatch_table{ref($self) || $self} ||= []);
      50        
189            
190 1   33     21 $dispatch_table = $dispatch_table->{CGI::virtual_host()}
191             || $dispatch_table->{'*'};
192            
193 1         7 my @rm = uniq map {
194 2 100       10 $_->{rm}
195             } grep {
196 1         164 ref($_) && $_->{app} eq $app
197             } @$dispatch_table;
198            
199 1         8 \@rm;
200             }
201              
202             sub _uri_mapping_of {
203 15     15   26 my ($self, $app, $rm) = @_;
204            
205 15   33     47 $rm ||= _pkg2rm($app);
206            
207 15 50 33     112 my $mapping = ($uri_table{ref($self) || $self} ||= {})->{"$app/$rm"}
      50        
208             or die "mapping for $app/$rm not found, did you register $app?\n";
209            
210 15         38 $mapping;
211             }
212              
213             sub _build_uri {
214 8     8   12 my ($prototype, $args) = @_;
215            
216 8 100       27 $args = { params => $args }
217             if ref($args) eq 'ARRAY';
218 8   100     34 my $params = $args->{params} || [];
219            
220             ($prototype->{build_uri} || \&_default_build_uri)->(
221             {
222             %$prototype,
223             protocol => $args->{protocol} || $prototype->{protocol},
224             },
225             sub {
226 18     18   25 my $n = shift;
227 18         28 foreach my $h (@$params) {
228 15 100       31 if (ref $h eq 'HASH') {
229 13 100       48 return ($h->{$n}) if exists $h->{$n};
230             } else {
231 2         3 my @v;
232 2         3 local $@ = undef;
233 2         4 eval {
234 2         16 @v = $h->all_param($n);
235             };
236 2 50       1827 @v = $h->param($n)
237             if $@;
238 2 50       53 return wantarray ? @v : $v[0]
    50          
239             if @v;
240             }
241             }
242 11         23 ();
243 8   50     143 });
      66        
244             }
245              
246             sub _default_build_uri {
247 8     8   13 my ($prototype, $get_param) = @_;
248            
249             # determine hostport
250 8         13 my $host = $prototype->{host};
251 8 50       193 $host = CGI::virtual_host() if $host eq '*';
252             # build path
253 8         6138 my @path;
254 8         12 foreach my $p (@{$prototype->{path_array}}) {
  8         24  
255 14 100       49 if ($p =~ m|^:(.*?)(\??)$|) {
256 6         15 my ($n, $optional) = ($1, $2);
257 6         14 my @v = $get_param->($n);
258 6 100       17 unless (@v) {
259 3 100       15 die "required parameter '$n' is missing\n"
260             unless $optional;
261 2         5 last;
262             }
263 3 50       9 die "more than one value assigned for path parameter: '$n'\n"
264             if @v != 1;
265 3         8 push @path, @v;
266             } else {
267 8         23 push @path, $p;
268             }
269             }
270             # build query params
271 7         11 my @qp;
272 7         9 foreach my $p (@{$prototype->{query}}) {
  7         18  
273 12         111 my @v = $get_param->($p->{name});
274 12         28 foreach my $v (@v) {
275 4 50       10 if ($p->{omit}) {
276 0 0       0 next if $v eq $p->{omit};
277             }
278 4         21 push @qp, "$p->{name}=" . uri_escape($v);
279             }
280             }
281             # build and return
282 7         39 my $uri = "$prototype->{protocol}://$host/" . join('/', @path);
283 7 100       22 $uri .= '?' . join('&', @qp)
284             if @qp;
285 7         49 $uri;
286             }
287              
288             sub _validate_uri {
289 6     6   12 my ($mapping, $app, $extra) = @_;
290 6         10 my $param_map = $mapping->{param_map};
291 6         19 my $query = $app->query;
292 6   50     174 my $meth = $query->request_method || 'GET';
293 6         162 $extra = { map { $_ => 1 } @$extra };
  0         0  
294            
295 6 50 33     22 return URI_IS_PERMALINK
296             unless $meth eq 'GET' || $meth eq 'HEAD';
297            
298 6         9 my $max_rank = 0;
299 6         169 foreach my $n (
  11         1606  
300             map { (split '=', $_, 2)[0] } split(/[&;]/, $query->query_string)
301             ) {
302 10 100       31 if (my $ref = $param_map->{$n}) {
303 9 100       31 return URI_PATH_PARAM_IN_QUERY
304             if $ref->{rank} < 0;
305 8 100       24 return URI_PARAM_NOT_IN_ORDER
306             if $ref->{rank} < $max_rank;
307 7 100       34 if (my $omit = $ref->{omit}) {
308 3         11 foreach my $v ($query->param($n)) {
309 3 100       78 return URI_OMITTABLE_PARAM
310             if $v eq $omit;
311             }
312             }
313 6         14 $max_rank = $ref->{rank};
314             } else {
315 1 50       10 return URI_UNKNOWN_PARAM
316             unless $extra->{$n};
317             }
318             }
319            
320 2         346 URI_IS_PERMALINK;
321             }
322              
323             sub _setup_runmodes {
324 1     1   2 my ($app, $mapping) = @_;
325 1         6 $app->run_modes(_run_modes_of($mapping, ref $app));
326             }
327              
328             sub _build_uri_table_entry {
329 4     4   9 my $table = shift;
330            
331             # setup path_array
332 4         9 my $p = $table->{path};
333 4         29 $p =~ s|^/?(.*)/?$|$1|;
334 4         23 $table->{path_array} = [ split '/', $p ];
335            
336             # normalize query array
337 4   50     17 $table->{query} ||= [];
338 4         7 foreach my $p (@{$table->{query}}) {
  4         13  
339 7 100       32 $p = {
340             name => $p,
341             } unless ref $p;
342             }
343            
344             # setup param_map
345 4         24 $table->{param_map} = {};
346 4         12 for (my $i = 0; $i < @{$table->{query}}; $i++) {
  11         34  
347 7   100     80 $table->{param_map}->{$table->{query}->[$i]->{name}} = {
348             rank => $i + 1,
349             omit => $table->{query}->[$i]->{omit} || undef,
350             };
351             }
352 4         7 foreach my $e (@{$table->{path_array}}) {
  4         17  
353 8 100       40 if ($e =~ /^:(.*?)\??$/) {
354 5         23 $table->{param_map}->{$1} = {
355             rank => -1,
356             };
357             }
358             }
359            
360             # set build_uri
361 4   50     39 $table->{build_uri} ||= \&_default_build_uri;
362            
363 4         16 $table;
364             }
365              
366             sub _pkg2rm {
367 15     15   20 my $pkg = shift;
368            
369 15         80 $pkg =~ m|[^:]*$|;
370 15         30 my $rm = $&;
371 15 50       75 $rm =~ s/([a-z]?)([A-Z])/($1 ? "$1_" : '') . lc($2)/ego;
  15         114  
372            
373 15         68 $rm;
374             }
375              
376             1;
377              
378             __END__