File Coverage

blib/lib/Router/Boom.pm
Criterion Covered Total %
statement 85 85 100.0
branch 25 26 96.1
condition 3 3 100.0
subroutine 13 13 100.0
pod 3 4 75.0
total 129 131 98.4


line stmt bran cond sub pod time code
1             package Router::Boom;
2 6     6   26906 use 5.008005;
  6         14  
3 6     6   17 use strict;
  6         7  
  6         94  
4 6     6   22 use warnings;
  6         5  
  6         108  
5 6     6   589 use Carp ();
  6         10  
  6         323  
6              
7             our $VERSION = "1.03";
8              
9             # Matcher stuff
10             our $LEAF_IDX;
11             our @CAPTURED;
12              
13             # Compiler stuff
14             our @LEAVES;
15             our $PAREN_CNT;
16             our @PARENS;
17              
18 6     6   17 use re 'eval';
  6         8  
  6         206  
19              
20 6     6   1682 use Router::Boom::Node;
  6         8  
  6         3803  
21              
22             sub new {
23 6     6 1 401 my $class = shift;
24 6         12 my $self = bless { }, $class;
25 6         23 $self->{root} = Router::Boom::Node->new(key => '/');
26 6         11 return $self;
27             }
28              
29             # True if : ()
30             # False if : (?:)
31             sub _is_normal_capture {
32 6     6   26 $_[0] =~ /
33             \(
34             (?!
35             \?:
36             )
37             /x
38             }
39              
40             sub add {
41 19     19 1 54 my ($self, $path, $stuff) = @_;
42 19         50 $path =~ s!\A/!!;
43              
44 19         20 delete $self->{regexp}; # clear cache
45              
46 19         15 my $p = $self->{root};
47 19         14 my @capture;
48 19         64 while ($path =~ m!\G(?:
49             \{((?:\{[0-9,]+\}|[^{}]+)+)\} | # /blog/{year:\d{4}}
50             :([A-Za-z0-9_]+) | # /blog/:year
51             (\*) | # /blog/*/*
52             ([^{:*]+) # normal string
53             )!xg) {
54              
55 33 100       74 if (defined $1) {
    100          
    100          
56 10         29 my ($name, $pattern) = split /:/, $1, 2;
57 10 100 100     27 if (defined($pattern) && _is_normal_capture($pattern)) {
58 1         106 Carp::croak("You can't include parens in your custom rule.");
59             }
60 9         9 push @capture, $name;
61 9 100       19 $pattern = $pattern ? "($pattern)" : "([^/]+)";
62 9         17 $p = $p->add_node($pattern);
63             } elsif (defined $2) {
64 5         8 push @capture, $2;
65 5         10 $p = $p->add_node("([^/]+)");
66             } elsif (defined $3) {
67 1         2 push @capture, '*';
68 1         2 $p = $p->add_node("(.+)");
69             } else {
70 17         43 $p = $p->add_node(quotemeta $4);
71             }
72             }
73 18         46 $p->leaf([\@capture, $stuff]);
74              
75 18         110 return;
76             }
77              
78             sub _build_regexp {
79 4     4   4 my ($self) = @_;
80              
81 4         4 my $trie = $self->{root};
82 4         6 local @LEAVES;
83 4         10 local $PAREN_CNT = 0;
84 4         7 local @PARENS;
85 4         7 my $re = _to_regexp($trie);
86 4         14 $self->{leaves} = [@LEAVES];
87 4         82 $self->{regexp} = qr{\A$re};
88             }
89              
90             sub match {
91 24     24 1 34 my ($self, $path) = @_;
92              
93             # "I think there was a discussion about that a while ago and it is up to apps to deal with empty PATH_INFO as root / iirc"
94             # -- by @miyagawa
95             #
96             # see http://blog.64p.org/entry/2012/10/05/132354
97 24 100       46 $path = '/' if $path eq '';
98              
99 24 100       31 if ($path =~ $self->regexp) {
100 22         16 my ($captured, $stuff) = @{$self->{leaves}->[$Router::Boom::LEAF_IDX]};
  22         35  
101 22         298 my %captured;
102 22         36 @captured{@$captured} = @Router::Boom::CAPTURED;
103 22         100 return ($stuff, \%captured);
104             } else {
105 2         7 return ();
106             }
107             }
108              
109             sub regexp {
110 25     25 0 25 my $self = shift;
111 25 100       42 if (not exists $self->{regexp}) {
112 4         11 $self->_build_regexp();
113             }
114 25         541 $self->{regexp};
115             }
116              
117             sub _to_regexp {
118 30     30   26 my ($node) = @_;
119              
120 30         18 my %leaves;
121              
122 30         41 local @PARENS = @PARENS;
123              
124 30         52 my $key = $node->key;
125 30 100       131 if ($key =~ /\(/) {
126 11         9 $PAREN_CNT++;
127 11         11 push @PARENS, $PAREN_CNT;
128             }
129 30         23 my @re;
130 30 100       18 if (@{$node->children}>0) {
  30         50  
131 18         60 push @re, map { _to_regexp($_) } @{$node->children};
  26         141  
  18         25  
132             }
133 30 100       94 if ($node->leaf) {
134 17         66 push @Router::Boom::LEAVES, $node->leaf;
135 17         86 push @re, sprintf '\z(?{ $Router::Boom::LEAF_IDX=%s; @Router::Boom::CAPTURED = (%s) })', @Router::Boom::LEAVES-1, join(',', map { "\$$_" } @PARENS);
  14         44  
136             }
137 30         84 my $re = $node->key;
138 30 50       137 if (@re==0) {
    100          
139             # nop
140             } elsif (@re == 1) {
141 23         34 $re .= $re[0];
142             } else {
143 7         28 $re .= '(?:' . join('|', @re) . ')';
144             }
145 30         2580 return qr{$re};
146             }
147              
148             1;
149             __END__