File Coverage

blib/lib/CGI/Easy/URLconf.pm
Criterion Covered Total %
statement 138 143 96.5
branch 40 54 74.0
condition 12 16 75.0
subroutine 20 20 100.0
pod 6 6 100.0
total 216 239 90.3


line stmt bran cond sub pod time code
1             package CGI::Easy::URLconf;
2              
3 4     4   98834 use warnings;
  4         10  
  4         126  
4 4     4   22 use strict;
  4         8  
  4         121  
5 4     4   22 use Carp;
  4         13  
  4         359  
6              
7 4     4   3633 use version; our $VERSION = qv('1.0.1'); # REMINDER: update Changes
  4         8942  
  4         33  
8              
9             # REMINDER: update dependencies in Makefile.PL
10 4     4   4173 use Perl6::Export::Attrs;
  4         33779  
  4         31  
11 4     4   4143 use URI::Escape qw( uri_escape_utf8 );
  4         6503  
  4         1491  
12              
13              
14             my %PATH2VIEW;
15             my %VIEW2PATH;
16              
17              
18             sub setup_path :Export {
19 6     6 1 1345 my (@data) = @_;
20 6 100 100     51 my $method = ref $data[0] || $data[0] =~ m{\A/}xms ? q{} : shift @data;
21 6         23 for (my $i = 0; $i <= $#data; $i++) {
22 13         21 my $match = $data[$i];
23 13 100       28 if (ref $match) {
24 5 50       21 croak "expect SCALAR or Regexp at parameter $i" if ref $match ne 'Regexp';
25             } else {
26 8 50       33 croak "path at parameter $i must begin with /" if $match !~ m{\A/}xms;
27             }
28 13 50       34 croak 'not enough params' if $i == $#data;
29 13         26 my @code = ($data[++$i]);
30 13 50       32 croak "expect CODE at parameter $i" if ref $code[0] ne 'CODE';
31 13         55 while (ref $data[$i+1] eq 'CODE') {
32 3         70 push @code, $data[++$i];
33             }
34 13         18 my $view = pop @code;
35 13         15 push @{ $PATH2VIEW{$method} }, {
  13         93  
36             match => $match,
37             view => $view,
38             prepare => \@code,
39             };
40             }
41 6         19 return;
42 4     4   31 }
  4         8  
  4         49  
43              
44             sub path2view :Export {
45 17     17 1 1761 my ($r) = @_;
46 17         23 my $path = $r->{path};
47 17         18 my $view;
48 17   100     86 my $for_method = $PATH2VIEW{ $r->{ENV}{REQUEST_METHOD} } || [];
49 17   50     41 my $for_any = $PATH2VIEW{ q{} } || [];
50 17         16 for my $path2view (@{$for_method}, @{$for_any}) {
  17         31  
  17         33  
51 63         56 my @match;
52 63         79 my $match = $path2view->{match};
53 63 100       96 if (!ref $match) {
54 45 100       120 next if $path ne $match;
55             } else {
56 18 100       98 next if $path !~ /$match/xms;
57 5         22 for my $i (0 .. $#-) {
58 9 50       27 if (defined $-[$i]) {
59 9         50 push @match, substr $path, $-[$i], $+[$i] - $-[$i];
60             }
61             else {
62 0         0 push @match, undef;
63             }
64             }
65             }
66 13         17 for my $prepare (@{ $path2view->{prepare} }) {
  13         25  
67 3         11 $prepare->($r, \@match);
68             }
69 13         80 return $path2view->{view};
70             }
71 4         25 return;
72 4     4   2192 }
  4         10  
  4         21  
73              
74             sub set_param :Export {
75 3     3 1 24 my (@names) = @_;
76             return sub {
77 3     3   8 my ($r, $values) = @_;
78 3         9 for my $i (0 .. $#names) {
79 4 50       13 if (defined $values->[$i+1]) {
80 4 50       16 if (ref $r->{GET}{ $names[$i] }) {
81 0         0 $r->{GET}{ $names[$i] } = [ $values->[$i+1] ];
82             } else {
83 4         15 $r->{GET}{ $names[$i] } = $values->[$i+1];
84             }
85             }
86             else {
87 0         0 delete $r->{GET}{ $names[$i] };
88             }
89             }
90 3         10 return;
91 3         27 };
92 4     4   1444 }
  4         8  
  4         19  
93              
94             ###
95              
96             sub setup_view :Export {
97 3     3 1 12 my (@data) = @_;
98 3         12 for (my $i = 0; $i <= $#data; $i++) {
99 4         8 my $view = $data[$i];
100 4 50       26 croak "expect CODE at parameter $i" if ref $view ne 'CODE';
101 4 50       12 croak "already exists CODE at parameter $i" if exists $VIEW2PATH{$view};
102 4 50       37 croak 'not enough params' if $i == $#data;
103 4         9 my $path = $data[++$i];
104 4 50 66     22 croak "expect SCALAR or ARRAY at parameter $i" if ref $path && ref $path ne 'ARRAY';
105 4 50 66     13 croak "expect even elements in parameter $i" if ref $path && @{$path} % 2;
  2         12  
106 4         19 $VIEW2PATH{$view} = $path;
107             }
108 3         8 return;
109 4     4   1722 }
  4         10  
  4         20  
110              
111             sub view2path :Export {
112 13     13 1 4548 my ($view, %p) = @_;
113 13         28 my $path = $VIEW2PATH{$view};
114 13 100       34 if (!defined $path) {
115 4         11 my @path = grep { $_->{view} eq $view } map { @{$_} } values %PATH2VIEW;
  27         69  
  8         10  
  8         20  
116 4 100 66     30 if (@path == 1 && !ref $path[0]{match}) {
117 3         8 $path = $path[0]{match};
118             }
119             }
120 13 100       38 croak 'unknown CODE, use setup_view first' if !defined $path;
121 12 100       28 if (ref $path) {
122 7         8 my @try = @{$path};
  7         19  
123 7         12 $path = undef;
124 7         20 while (@try) {
125 10         17 my $try = shift @try;
126 10         17 my $tmpl= shift @try;
127 10 100       26 my $values = $try->(\%p) or next;
128 5 50       10 if (@{$values} != ($tmpl =~ tr/?//)) {
  5         18  
129 0         0 croak "incorrect values amount for template '$tmpl'";
130             }
131             # WARNING apache не разрешает %2F в пути (nginx разрешает)
132 5         10 $_ = uri_escape_utf8($_), s/%2F/\//g for @{$values}; ## no critic
  5         26  
133 5         180 $tmpl =~ s/[?]/shift @{$values}/xmsge;
  7         10  
  7         18  
134 5         11 $path = $tmpl;
135 5         11 last;
136             }
137 7 100       59 croak 'these parameters do not match configured urls' if !defined $path;
138             }
139 10         14 my @params;
140 10         27 for my $n (keys %p) {
141 2 50       11 my @v = ref $p{$n} ? @{ $p{$n} } : $p{$n};
  0         0  
142 2         4 for my $v (@v) {
143 2         6 push @params, uri_escape_utf8($n).q{=}.uri_escape_utf8($v);
144             }
145             }
146 10 100       73 if (@params) {
147 2         8 $path .= q{?} . join q{&}, @params;
148             }
149 10         35 return $path;
150 4     4   2772 }
  4         9  
  4         19  
151              
152             sub with_params :Export {
153 3     3 1 7 my (@names) = @_;
154             return sub {
155 10     10   14 my ($p) = @_;
156 10         12 my @values;
157 10         17 for my $name (@names) {
158 12 100       48 return if !defined $p->{ $name };
159 7         20 push @values, $p->{ $name };
160             }
161 5         19 delete $p->{$_} for @names; ## no critic
162 5         18 return \@values;
163 3         24 };
164 4     4   1282 }
  4         7  
  4         16  
165              
166              
167             1; # Magic true value required at end of module
168             __END__
169              
170             =encoding utf8
171              
172             =head1 NAME
173              
174             CGI::Easy::URLconf - map url path to handler sub and vice versa
175              
176              
177             =head1 SYNOPSIS
178              
179             use CGI::Easy::URLconf qw( setup_path path2view set_param );
180              
181             setup_path(
182             '/about/' => \&myabout,
183             '/terms.php' => \&terms,
184             qr{\A /articles/ \z}xms => \&list_all_articles,
185             );
186             setup_path(
187             qr{\A /articles/(\d+)/ \z}xms
188             => set_param('year')
189             => \&list_articles,
190             qr{\A /articles/tag/(\w+)/(\d+)/ \z}xms
191             => set_param('tag','year')
192             => \&list_articles,
193             );
194             setup_path( POST =>
195             '/articles/' => \&add_article,
196             );
197              
198             my $r = CGI::Easy::Request->new();
199             my $handler = path2view($r);
200              
201              
202             use CGI::Easy::URLconf qw( setup_view view2path with_params );
203              
204             setup_view(
205             \&list_all_articles => '/articles/',
206             \&list_articles => [
207             with_params('tag','year') => '/articles/tag/?/?/',
208             with_params('year') => '/articles/?/',
209             ],
210             );
211              
212             # set $url to '/about/'
213             my $url = view2path( \&myabout );
214              
215             # set $url to '/articles/'
216             my $url = view2path( \&list_all_articles );
217              
218             # set $url to '/articles/2010/?month=12'
219             my $url = view2path( \&list_articles, year=>2010, month=>12 );
220              
221              
222             =head1 DESCRIPTION
223              
224             This module provide support for clean, user-friendly URLs. This can be
225             archived by configuring web server to run your CGI/FastCGI script for any
226             url requested by user, and let you manually dispatch different urls to
227             corresponding handlers (subroutines). Additionally, you can take some
228             CGI parameters from url's path instead of usual GET parameters.
229              
230             The idea is to set rules when CGI/FastCGI starts using:
231             a) setup_path() - to map url's path to handler subroutine
232             (also called "view")
233             b) setup_view() - to map handler subroutine to url
234             and then use:
235             a) path2view() - to get handler subroutine matching current url's path
236             b) view2path() - to get url matching some handler subroutine
237             (for inserting into HTML templates or sending redirects).
238              
239             Example:
240              
241             # -- while CGI/FastCGI initialization
242             setup_path(
243             '/articles/' => \&list_articles,
244             '/articles.php' => \&list_articles,
245             '/index.php' => \&show_home_page,
246             );
247             setup_path( POST =>
248             '/articles/' => \&add_new_article,
249             );
250              
251             # -- when beginning to handle new CGI/FastCGI request
252             my $r = CGI::Easy::Request->new();
253             my $handler = path2view($r);
254             # $handler now set to:
255             # \&list_articles if url path /articles/ and request method is GET
256             # \&add_new_article if url path /articles/ and request method is POST
257             # \&list_articles if url path /articles.php (any request method)
258             # \&show_home_page if url path /index.php (any request method)
259             # undef (in all other cases)
260              
261             # -- while CGI/FastCGI initialization
262             setup_view(
263             \&list_articles => '/articles/',
264             # we don't have to configure mapping for \&show_home_page
265             # and \&add_new_article because their mappings can be
266             # unambiguously automatically detected from above setup_path()
267             );
268              
269             # -- when preparing reply (HTML escaping omitted for simplicity)
270             printf '<a href="%s">Articles</a>', view2path(\&list_articles);
271             printf '<form method=POST action="%s">', view2path(\&add_new_article);
272             # -- or redirecting to another url
273             my $h = CGI::Easy::Headers->new();
274             $h->redirect(view2path(\&show_home_page));
275              
276             These two parts (setup_path() with path2view() and setup_view() with view2path())
277             can be used independently - for example, you don't have to use
278             setup_view() and view2path() if you prefer to hardcode urls in HTML templates
279             instead of generating them dynamically. But using both parts will let you
280             configure I<all> urls used in your application in single place, which make
281             it easier to control and modify them.
282              
283             In addition to simple constant path to handler and vice versa mapping you
284             can also map any path matching regular expression and even copy some data
285             from path to GET parameters. Example:
286              
287             # make /article/123/ same as /index.php?id=123
288             # use same handler for any url beginning with /old/
289             setup_path(
290             '/article.php' => \&show_article,
291             qr{^/article/(\d+)/$} => set_param('id') => \&show_article,
292             qr{^/old/} => \&unsupported,
293             );
294              
295             # generate urls like /article/123/ dynamically
296             setup_view(
297             \&show_article => [
298             with_params('id') => '/article/?/',
299             ],
300             );
301             $url = view2path(\&show_article, id=>123);
302              
303              
304             =head1 INTERFACE
305              
306             =over
307              
308             =item setup_path( [METHOD =>] MATCH => [CALLBACK => ...] HANDLER, ... )
309              
310             Configure mapping of url's path to handler subroutine (which will be used
311             by path2view()).
312             Can be called multiple times and will just B<add> new mapping rules on each call.
313              
314             If optional METHOD parameter defined, then all mapping rules in this
315             setup_path() call will be applied only for requests with that HTTP method.
316             If METHOD doesn't used, then these rules will be applied to all HTTP methods.
317             If some path match both rules defined for current HTTP method and rules
318             defined for any HTTP methods, will be used rule defined for current HTTP
319             method.
320              
321             MATCH parameter should be either SCALAR (string equal to url path) or
322             Regexp (which can match any part of url path).
323              
324             HANDLER is REF to your subroutine, which will be returned by path2view()
325             when this rule will match current url.
326              
327             Between MATCH and HANDLER any amount of optional CALLBACK subroutines can
328             be used. These CALLBACKs will be called when MATCH rule matches current
329             url with two parameters: CGI::Easy::Request object and ARRAYREF with
330             contents of all capturing parentheses (when MATCH rule is Regexp with
331             capturing parentheses). Usual task for such CALLBACKs is convert "hidden"
332             CGI parameters included in url path into usual C<< $r->{GET} >> parameters.
333              
334             Return nothing.
335              
336              
337             =item path2view( $r )
338              
339             Take CGI::Easy::Request object as parameter, and analyse this request
340             according to rules defined previously using setup_path().
341              
342             Return: HANDLER if find rule which match current request, else undef().
343              
344              
345             =item set_param( @names )
346              
347             Take names of C<< {GET} >> parameters which should be set using parts of
348             url path selected by capturing parentheses in MATCH Regexp.
349              
350             Return CALLBACK subroutine suitable for using in setup_path().
351              
352              
353             =item setup_view( HANDLER => PATH, ... )
354              
355             Configure mapping of handler subroutine to url path (which will be used by
356             view2path()).
357             Can be called multiple times and will just B<add> new mapping rules on each call.
358              
359             HANDLER must be REF to user's subroutine used to handle requests on PATH.
360              
361             PATH can be either STRING or ARRAYREF.
362              
363             If PATH is ARRAYREF, then this array should consist of CALLBACK =>
364             TEMPLATE pairs. CALLBACK is subroutine which will be executed by
365             view2path() with single parameter C<< \%params >>, and should return
366             either FALSE if this CALLBACK unable to handle these %params, or ARRAYREF
367             with values to substitute into path TEMPLATE. TEMPLATE is path STRING
368             which may contain '?' symbols - these will be replaced by values returned
369             in ARRAYREF by CALLBACK which successfully handle %params.
370              
371             Example: map \&handler to /first/ or /second/ with 50% probability
372              
373             setup_view(
374             \&handler => [
375             sub { return rand < 0.5 ? [] : undef } => '/first/',
376             sub { return [] } => '/second/',
377             ],
378             );
379              
380             Example: map \&handler to random article with id 0-999
381              
382             setup_view(
383             \&handler => [
384             sub { return [ int rand 1000 ] } => '/article/?/',
385             ],
386             );
387              
388             Return nothing.
389              
390              
391             =item view2path( HANDLER, %params )
392              
393             Take user handler subroutine and it parameters, and convert it to url
394             according to rules defined previously using setup_view().
395              
396             Example:
397              
398             setup_view(
399             \&handler => 'index.php',
400             );
401             my $url = view2path(\&handler, a=>'some string', b=>[6,7]);
402             # $url will be: 'index.php?a=some%20string&b=6&b=7'
403              
404             If simple mapping from STRING to HANDLER was defined using setup_path(),
405             and this is only mapping to HANDLER defined, then it's not necessary to
406             define reverse mapping using setup_view() - it will be defined
407             automatically.
408              
409             Example:
410              
411             setup_path(
412             '/articles/' => \&list_articles,
413             );
414             my $url = view2path(\&list_articles);
415             # $url will be: '/articles/'
416              
417             Return: url. Throw exception if unable to make url.
418              
419              
420             =item with_params( @names )
421              
422             Take names of parameters which B<must> exists in %params given to
423             view2path().
424              
425             Return CALLBACK subroutine suitable for using in setup_view().
426              
427              
428             =back
429              
430              
431             =head1 BUGS AND LIMITATIONS
432              
433             No bugs have been reported.
434              
435              
436             =head1 SUPPORT
437              
438             Please report any bugs or feature requests through the web interface at
439             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CGI-Easy-URLconf>.
440             I will be notified, and then you'll automatically be notified of progress
441             on your bug as I make changes.
442              
443             You can also look for information at:
444              
445             =over
446              
447             =item * RT: CPAN's request tracker
448              
449             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=CGI-Easy-URLconf>
450              
451             =item * AnnoCPAN: Annotated CPAN documentation
452              
453             L<http://annocpan.org/dist/CGI-Easy-URLconf>
454              
455             =item * CPAN Ratings
456              
457             L<http://cpanratings.perl.org/d/CGI-Easy-URLconf>
458              
459             =item * Search CPAN
460              
461             L<http://search.cpan.org/dist/CGI-Easy-URLconf/>
462              
463             =back
464              
465              
466             =head1 AUTHOR
467              
468             Alex Efros C<< <powerman-asdf@ya.ru> >>
469              
470              
471             =head1 LICENSE AND COPYRIGHT
472              
473             Copyright 2009-2010 Alex Efros <powerman-asdf@ya.ru>.
474              
475             This program is distributed under the MIT (X11) License:
476             L<http://www.opensource.org/licenses/mit-license.php>
477              
478             Permission is hereby granted, free of charge, to any person
479             obtaining a copy of this software and associated documentation
480             files (the "Software"), to deal in the Software without
481             restriction, including without limitation the rights to use,
482             copy, modify, merge, publish, distribute, sublicense, and/or sell
483             copies of the Software, and to permit persons to whom the
484             Software is furnished to do so, subject to the following
485             conditions:
486              
487             The above copyright notice and this permission notice shall be
488             included in all copies or substantial portions of the Software.
489              
490             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
491             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
492             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
493             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
494             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
495             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
496             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
497             OTHER DEALINGS IN THE SOFTWARE.
498