File Coverage

blib/lib/Mojolicious/Plugin/PlainRoutes.pm
Criterion Covered Total %
statement 107 123 86.9
branch 55 70 78.5
condition 13 17 76.4
subroutine 8 9 88.8
pod 1 3 33.3
total 184 222 82.8


line stmt bran cond sub pod time code
1 4     4   1386289 use 5.014;
  4         35  
2             package Mojolicious::Plugin::PlainRoutes;
3             # ABSTRACT: Plaintext route definitions for Mojolicious
4             $Mojolicious::Plugin::PlainRoutes::VERSION = '0.07';
5 4     4   455 use Mojo::Base 'Mojolicious::Plugin';
  4         159942  
  4         30  
6 4     4   3664 use Mojo::Util qw/decamelize/;
  4         8  
  4         7714  
7              
8             has autoname => 0;
9              
10             sub register {
11 4     4 1 58702 my ($self, $app, $conf) = @_;
12              
13 4         22 $self->autoname($conf->{autoname});
14              
15 4   33     41 $conf->{file} //= $app->home->rel_file("lib/".$app->moniker.".routes");
16              
17 4     2   144 open my $fh, '<:encoding(UTF-8)', $conf->{file};
  2         17  
  2         5  
  2         15  
18 4         2532 my $tree = $self->tokenise($fh);
19 4         86 close $fh;
20              
21 4         31 $self->process($app->routes, $tree);
22             }
23              
24             sub tokenise {
25 13     13 0 32867 my ($self, $input) = @_;
26              
27 13 100       77 if (ref $input eq 'GLOB') {
    50          
28 4         6 $input = do { local $/; <$input> };
  4         20  
  4         119  
29             } elsif (ref $input) {
30 0         0 Carp::carp "Non-filehandle reference passed to tokenise";
31 0         0 return [];
32             }
33              
34 13         122 return $self->_tokenise($input);
35             }
36              
37             sub _tokenise {
38 13     13   37 my ($self, $input) = @_;
39              
40 13         65 $input =~ s/\r\n/\n/g;
41 13         34 $input =~ s/\n\r/\n/g;
42 13         38 $input =~ s/\r/\n/g;
43              
44 13         271 my %grammar = (
45             comment => qr{ \# [^\n]* }x,
46             verb => qr{ ANY | DELETE | GET | PATCH | POST | PUT }x,
47             path => qr{ / [^#\s]* }x,
48             arrow => qr{ -> }x,
49             scope => qr( { | } )x,
50             action => qr{ [\w\-:]* \. \w* }x,
51             name => qr{ \( [^)]+ \) }x,
52             eol => qr{ \n }x,
53             space => qr{ [^\S\n]+ }x,
54             );
55              
56 13 50       1015 my @words = grep { defined && length }
  526         1188  
57             split m{( $grammar{comment}
58             | $grammar{verb}
59             | $grammar{path}
60             | $grammar{arrow}
61             | $grammar{scope}
62             | $grammar{action}
63             | $grammar{name}
64             | $grammar{eol}
65             | $grammar{space}
66             )}x, $input;
67              
68             # Include the lexical category with the word, e.g., map:
69             # "/foo" -> { text => "/foo", category => "path" }
70 13         63 my @annotated_words;
71 13         35 for my $word (@words) {
72 263         653 my @cats = grep { $word =~ /^$grammar{$_}$/ } keys %grammar;
  2367         33350  
73              
74 263 50       829 if (@cats > 1) {
75 0         0 warn "$word has multiple lexical categories: @cats";
76             }
77              
78 263   50     937 push @annotated_words, { text => $word, category => $cats[0] // '' };
79             }
80              
81             # Add special EOF word to act as a clause terminator if necessary
82 13         51 push @annotated_words, { text => '', category => 'eof' };
83              
84             # Initialise
85 13         34 my $root = [];
86 13         31 my @nodes = ($root);
87 13         27 my %clause = ();
88 13         27 my $context = 'default';
89              
90             # Track for helpful error messages
91 13         26 my $col = 1;
92 13         22 my $line = 1;
93 13         25 my $error = 0;
94              
95             # Define outside the loop scope so that the closure can access it
96 13         21 my %word;
97              
98             # Called whenever a syntax error is encountered.
99             my $syntax_error = sub {
100 0     0   0 $error = 1;
101 0         0 my $_col = $col - length $word{text};
102 0         0 print STDERR qq{Syntax error in routes on line $line, col $_col: }
103             . qq{"$word{text}" (expected a @_)\n};
104 13         108 };
105              
106 13         64 for (@annotated_words) {
107 276         703 %word = %$_;
108 276         401 $col += length $word{text};
109 276 100       461 if ($word{category} eq 'eol') {
110 27         35 $line += 1;
111 27         42 $col = 1;
112             }
113              
114             # While in comment context, the parser checks for newlines and
115             # otherwise does nothing.
116 276 100 100     934 if ($context eq 'comment') {
    100          
    100          
    100          
    100          
    100          
117 2 50       7 if ($word{category} eq 'eol') {
118 2         6 $context = 'default';
119             }
120             }
121              
122             # The comment indicator puts the parser into comment context and
123             # otherwise does nothing.
124             elsif ($word{category} eq 'comment') {
125 2         4 $context = 'comment';
126             }
127              
128             # Whitespace is ignored
129             elsif ($word{category} eq 'space' || $word{category} eq 'eol') {}
130              
131             # First word in clause must be a HTTP verb
132             elsif (!exists $clause{verb}) {
133 27 100 66     79 if ($word{category} eq 'verb') {
    100          
    50          
134 22         56 $clause{verb} = $word{text};
135             }
136              
137             # The end of scope may be encountered here if there were two ends
138             # of scope in a row.
139             elsif ($word{category} eq 'scope' && $word{text} eq '}') {
140 1 50       4 if (@nodes == 1) {
141 0         0 'verb'->$syntax_error;
142             } else {
143 1         2 pop @nodes;
144             }
145             }
146              
147             # It's possible we encounter the EOF word here if we just
148             # encountered the end of a scope (or if the input is empty).
149             # Anything else is still a syntax error.
150             elsif ($word{category} ne 'eof') {
151 0         0 'verb'->$syntax_error;
152             }
153             }
154              
155             # Second word must be a path part
156             elsif (!exists $clause{path}) {
157 28 50       66 if ($word{category} eq 'path') {
158 28         61 $clause{path} = $word{text};
159             } else {
160 0         0 'path'->$syntax_error;
161             }
162             }
163              
164             # Third word must be an action, optionally preceded by an arrow (->)
165             elsif (!exists $clause{action}) {
166 55 100 100     179 if (!exists $clause{arrow} && $word{category} eq 'arrow') {
    50          
167 27         51 $clause{arrow} = 1;
168             } elsif ($word{category} eq 'action') {
169 28         131 my ($action, $controller) = split /\./, $word{text};
170 28         131 $clause{action} = decamelize($action) . "#$controller";
171              
172             # The clause needn't carry this useless information after this
173             # point.
174 28         640 delete $clause{arrow};
175             } else {
176 0         0 'action'->$syntax_error;
177             }
178             }
179              
180             # The final word should be some kind of terminator: scope indicators,
181             # the beginning of a new clause (i.e., a verb), or the end of input.
182             else {
183             # An optional name for the clause can be appended before the
184             # terminator.
185 30 100 100     143 if (!exists $clause{name} && $word{category} eq 'name') {
    100          
    100          
    50          
186 2         15 $clause{name} = $word{text} =~ s/ ^\( | \)$ //xgr;
187             }
188              
189             # The clause is terminated by a new scope.
190             elsif ($word{category} eq 'scope') {
191             # A new scope means that the preceding clause is a bridge, and
192             # therefore the head of a new branch in the tree.
193 13 100       40 if ($word{text} eq '{') {
    50          
194 7         26 my $newNode = [ { %clause } ];
195 7         15 push @{ $nodes[-1] }, $newNode;
  7         19  
196 7         14 push @nodes, $newNode;
197              
198 7         18 %clause = ();
199             }
200              
201             # The end of a scope means that the preceding clause is the
202             # last clause in a bridge.
203             elsif ($word{text} eq '}') {
204 6         11 push @{ $nodes[-1] }, { %clause };
  6         25  
205 6         15 %clause = ();
206              
207             # Can't exit a scope if we haven't entered one
208 6 50       21 if (@nodes == 1) {
209 0         0 'verb'->$syntax_error;
210             } else {
211 6         14 pop @nodes;
212             }
213             }
214             }
215              
216             # The clause is terminated by the start of a new one
217             elsif ($word{category} eq 'verb') {
218 6         9 push @{ $nodes[-1] }, { %clause };
  6         25  
219 6         21 %clause = ( verb => $word{text} );
220             }
221              
222             # Last chance, the clause is terminated by eof
223             elsif ($word{category} eq 'eof') {
224 9         16 push @{ $nodes[-1] }, { %clause };
  9         39  
225 9         28 %clause = ();
226             }
227              
228             else {
229 0         0 'terminator'->$syntax_error;
230             }
231             }
232             }
233              
234 13 50       48 if (@nodes != 1) {
235 0         0 'verb or end of scope'->$syntax_error;
236             }
237              
238 13 50       31 if ($error) {
239 0         0 Carp::croak "Parsing routes failed due to syntax errors";
240             }
241              
242 13         280 $root;
243             }
244              
245             sub process {
246 5     5 0 43 my ($self, $bridge, $tree) = @_;
247              
248 5         13 for my $node (@$tree) {
249 9 100       25 my $token = ref $node eq 'ARRAY' ? shift @$node : $node;
250              
251             my $route = $bridge->any($token->{path})
252 9         52 ->to($token->{action});
253 9 100       2907 if ($token->{verb} ne 'ANY') {
254 8         22 $route->methods($token->{verb});
255             }
256              
257 9         102 my $p = $route->pattern;
258 9 50       48 if (exists $token->{name}) {
    100          
    100          
259 0         0 $route->name($token->{name});
260             }
261             elsif (ref $self->autoname eq 'CODE') {
262             my $name = $self->autoname->($route->methods->[0], $p->unparsed,
263 1         7 @{$p->defaults}{qw/controller action/});
  1         9  
264              
265 1 50       17 if (ref $name) {
266 0         0 Carp::croak "Autoname callback did not return a string";
267             }
268              
269 1         2 $route->name($name);
270             }
271             elsif ($self->autoname) {
272 1         9 $route->name(join '-', @{$p->defaults}{qw/controller action/});
  1         3  
273             }
274              
275 9 100       129 if (ref $node eq 'ARRAY') {
276 1         6 $route->inline(1);
277 1         11 $self->process($route, $node);
278             }
279             }
280             }
281              
282             1;
283              
284             __END__