File Coverage

blib/lib/Router/PathInfo/Controller.pm
Criterion Covered Total %
statement 91 97 93.8
branch 45 58 77.5
condition 12 18 66.6
subroutine 8 8 100.0
pod 3 3 100.0
total 159 184 86.4


line stmt bran cond sub pod time code
1             package Router::PathInfo::Controller;
2 2     2   4857 use strict;
  2         4  
  2         113  
3 2     2   11 use warnings;
  2         4  
  2         121  
4              
5             =head1 NAME
6              
7             B provides a mapping PATH_INFO to controllers.
8              
9             =head1 SYNOPSIS
10            
11             # create instance
12             my $r = Router::PathInfo::Controller->new();
13            
14             # describe connect
15             $r->add_rule(connect => '/foo/:enum(bar|baz)/:any', action => ['some','bar']);
16            
17             # prepare arguments (this action to prepare $env hidden from you in the module Router::PathInfo)
18             my $env = {PATH_INFO => '/foo/baz/bar', REQUEST_METHOD => 'GET'};
19             my @segment = split '/', $env->{PATH_INFO}, -1;
20             shift @segment;
21             $env->{'psgix.tmp.RouterPathInfo'} = {
22             segments => [@segment],
23             depth => scalar @segment
24             };
25            
26             # match
27             my $res = $r->match($env);
28             # $res = HASH(0x93d74d8)
29             # 'action' => ARRAY(0x99294e8)
30             # 0 'some'
31             # 1 'bar'
32             # 'segment' => ARRAY(0x93d8038)
33             # 0 'baz'
34             # 1 'bar'
35             # 'type' => 'controller'
36              
37             # or $res may by undef
38              
39             =head1 DESCRIPTION
40              
41             C is used for matching sets of trees.
42             Therefore, search matching is faster and more efficient,
43             than a simple enumeration of regular expressions to search for a suitable result.
44              
45             In the descriptions of 'C' by adding rules, you can use these tokens:
46              
47             :any - match with any segment
48             :re(...some regular expression...) - match with the specified regular expression
49             :enum(...|...) - match with a segment from the set
50              
51             and sub-attribute for rules
52            
53             :name(...)
54              
55             For example
56            
57             '/foo/:name(some_name)bar/:any'
58             '/foo/:re(^\d{4}\w{4}$)/:name(my_token):any'
59             '/:enum(foo|bar|baz)/:re(^\d{4}\w{4}$)/:any'
60            
61             All descriptions of the segments have a certain weight.
62             Thus, the description C<:enum> has the greatest weight, a description of C<:re> weighs even less. Weakest coincidence is C<:any>.
63              
64             For all descriptions 'C' using these tokens in the match will be returned to a special key 'C'
65             in which stores a list of all segments C they are responsible.
66              
67             An important point: description 'C' dominates over http method. Example:
68            
69             $r->add_rule(connect => '/foo/:any/baz', action => 'one', methods => ['GET','DELETE']);
70             $r->add_rule(connect => '/foo/bar/:any', action => 'two');
71            
72             for '/foo/bar/baz' with GET -> 'two'
73              
74             In C you can pass any value: object, arrayref, hashref or a scalar.
75              
76             =head1 METHODS
77              
78             =cut
79              
80 2     2   5455 use namespace::autoclean;
  2         45627  
  2         12  
81 2     2   112 use Carp;
  2         3  
  2         3127  
82              
83             my $http_methods = {
84             GET => 1,
85             POST => 1,
86             PUT => 1,
87             OPTIONS => 1,
88             DELETE => 1,
89             HEAD => 1
90             };
91              
92             =head2 new()
93              
94             Simple constructor
95              
96             =cut
97             sub new {
98 3     3 1 1169 bless {
99             rule => {},
100             re_compile => {},
101             }, shift;
102             }
103              
104             =head2 add_rule(connect => $describe_connect, action => $action_token[, methods => $arrayref, match_callback => $code_ref])
105              
106             Add your description to match.
107              
108             'C' - arrayref of items GET, POST, PUT, OPTIONS, DELETE, HEAD
109              
110             'C' - coderef is called after match found. It takes two arguments: a match found and heshref passed parameters (see method C).
111             Example:
112              
113             $r->add_rule(
114             connect => '/foo/:enum(bar|baz)/:any',
115             action => ['any thing'],
116             methods => ['POST'],
117             match_callback => sub {
118             my ($match, $env) = @_;
119            
120             if ($env->{...} == ..) {
121             # $match->{action}->[0] eq 'any thing'
122             return $match;
123             } else {
124             return {
125             type => 'error',
126             code => 403,
127             desc => 'blah-blah'
128             };
129             }
130             }
131             );
132              
133             =cut
134             sub add_rule {
135 7     7 1 3792 my ($self, %args) = @_;
136            
137 7         20 for ( ('connect', 'action') ) {
138 14 50       55 unless ($args{$_}) {
139 0         0 carp "missing '$_'";
140 0         0 return;
141             };
142             }
143 7 50       39 $args{methods} = $args{methods} ? [grep {$http_methods->{$_}} (ref $args{methods} eq 'ARRAY' ? @{$args{methods}} : $args{methods})] : [];
  3 100       13  
  2         7  
144 7 100       46 my @methods = $args{methods}->[0] ? @{$args{methods}} : keys %$http_methods;
  2         8  
145 7         17 my $methods_weight = $#methods;
146            
147 7 100       29 my $sub_after_match = $args{match_callback} if ref $args{match_callback} eq 'CODE';
148            
149 7         39 my @depth = split '/',$args{connect},-1;
150            
151 7         17 my $named_segment = {}; my $i = 0;
  7         11  
152            
153 7         12 my $res = [];
154 7         14 for (@methods) {
155 33   100     183 $self->{rule}->{$_}->{$#depth} ||= {};
156 33         108 push @$res, $self->{rule}->{$_}->{$#depth};
157             }
158            
159 7         100 (my $tmp = $args{connect}) =~ s!
160             (/)(?=/) | # double slash
161             (/$) | # end slash
162             /(:name\(["']?(.*?)["']?\))?:enum\(([^/]+)\)(?= $|/) | # enum
163             /(:name\(["']?(.*?)["']?\))?:re\(([^/]+)\)(?= $|/) | # re
164             /(:name\(["']?(.*?)["']?\))?(:any)(?= $|/) | # any
165             /(:name\(["']?(.*?)["']?\))?([^/]+)(?= $|/) # eq
166             !
167 20 50 33     175 if ($1 or $2) {
    100          
    100          
    100          
    50          
168 0   0     0 $_->{exactly}->{''} ||= {} for @$res;
169 0         0 $res = [map {$_->{exactly}->{''}} @$res];
  0         0  
170             } elsif ($5) {
171 6         25 my @val = split('\|',$5);
172 6         22 my @tmp;
173 6         15 for my $val (@val) {
174 12         23 for (@$res) {
175 54   100     187 $_->{exactly}->{$val} ||= {};
176 54         156 push @tmp, $_->{exactly}->{$val};
177             };
178             }
179 6         20 $res = [@tmp];
180 6 100       41 $named_segment->{$i} = $4 if $4;
181             } elsif ($8) {
182 1         43 $self->{re_compile}->{$8} = qr{$8}s;
183 1   50     59 $_->{regexp}->{$8} ||= {} for @$res;
184 1         3 $res = [map {$_->{regexp}->{$8}} @$res];
  12         30  
185 1 50       9 $named_segment->{$i} = $7 if $7;
186             } elsif ($11) {
187 5   100     139 $_->{default}->{''} ||= {} for @$res;
188 5         10 $res = [map {$_->{default}->{''}} @$res];
  42         70  
189 5 100       24 $named_segment->{$i} = $10 if $10;
190             } elsif ($14) {
191 8   100     164 $_->{exactly}->{$14} ||= {} for @$res;
192 8         18 $res = [map {$_->{exactly}->{$14}} @$res];
  39         88  
193 8 50       30 $named_segment->{$i} = $13 if $13;
194             } else {
195             # default as word
196 0         0 croak "cant't resolve connect '$args{connect}'"
197             }
198 20         142 $i++;
199             !gex;
200            
201 7         17 for (@$res) {
202 60 50 66     162 if (not $_->{match} or $_->{match}->[3] >= $methods_weight) {
203             # set only if no match or a match for a more accurate description
204 60 100       359 $_->{match} = [$args{action}, keys %$named_segment ? $named_segment : undef, $sub_after_match, $methods_weight];
205             }
206             }
207              
208 7         58 return 1;
209             }
210              
211             sub _match {
212 22     22   64 my ($self, $reserch, $size_el, @el) = @_;
213 22         24 my $ret;
214 22         27 my $not_exactly = 0;
215 22         33 my $segment = shift @el;
216 22         24 $size_el--;
217 22         44 my $exactly = $reserch->{exactly}->{$segment};
218 22 100       51 if (defined $exactly) {
219 14 100       82 ($ret, $not_exactly) = $size_el ? $self->_match($exactly, $size_el, @el) : $exactly->{match};
220 14 50       57 return ($ret, $not_exactly) if $ret;
221             };
222            
223 8 100       35 if ($reserch->{regexp}) {
224 1         4 for (keys %{$reserch->{regexp}}) {
  1         17  
225 1 50       9 if ($segment =~ $self->{re_compile}->{$_}) {
226 1 50       5 ($ret) = $size_el ? $self->_match($reserch->{regexp}->{$_}, $size_el, @el) : $reserch->{regexp}->{$_}->{match};
227 1 50       7 return ($ret, 1) if $ret;
228             };
229             }
230             };
231            
232 7 100       21 if ($reserch->{default}) {
233 5 50       17 ($ret) = $size_el ? $self->_match($reserch->{default}->{''}, $size_el, @el) : $reserch->{default}->{''}->{match};
234 5 50       35 return ($ret, 1) if $ret;
235             }
236            
237 2         8 return;
238             }
239              
240             =head2 match({REQUEST_METHOD => ..., 'psgix.tmp.RouterPathInfo' => ...})
241              
242             Search match. See SYNOPSIS.
243              
244             If a match is found, it returns hashref:
245              
246             {
247             type => 'controller',
248             action => $action,
249             name_segments => $arrayref
250             }
251              
252             Otherwise, undef.
253              
254             =cut
255             sub match {
256 9     9 1 2740 my $self = shift;
257 9         16 my $env = shift;
258            
259 9         18 my $depth = $env->{'psgix.tmp.RouterPathInfo'}->{depth};
260            
261 9         31 my ($match, $not_exactly) = $self->_match(
262             $self->{rule}->{$env->{REQUEST_METHOD}}->{$depth},
263             $depth,
264 9         33 @{$env->{'psgix.tmp.RouterPathInfo'}->{segments}}
265             );
266              
267 9 100       26 if ($match) {
268 7         22 my $ret = {
269             type => 'controller',
270             action => $match->[0]
271             };
272 7 100       20 unless ($match->[1]) {
273 5         11 $ret->{name_segments} = {};
274             } else {
275 2         3 $ret->{name_segments}->{$match->[1]->{$_}} = $env->{'psgix.tmp.RouterPathInfo'}->{segments}->[$_] for keys %{$match->[1]};
  2         22  
276             }
277 7 100       24 $ret->{_callback} = $match->[2] if $match->[2];
278 7         22 return ($not_exactly, $ret);
279             } else {
280 2         7 return;
281             }
282            
283             # if ($match) {
284             # my $ret = {
285             # type => 'controller',
286             # action => $match->[0],
287             # segment => $match->[1] ? [map {$env->{'psgix.tmp.RouterPathInfo'}->{segments}->[$_]} @{$match->[1]}] : []
288             # };
289             # if ($match->[2]) {
290             # return ($not_exactly, $match->[2]->($ret,$env));
291             # } else {
292             # return ($not_exactly, $ret);
293             # }
294             # } else {
295             # return;
296             # }
297            
298             }
299              
300             =head1 SEE ALSO
301              
302             L, L
303              
304             =head1 AUTHOR
305              
306             mr.Rico
307              
308             =cut
309              
310             1;
311             __END__