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   578803 use 5.014;
  4         8  
2             package Mojolicious::Plugin::PlainRoutes;
3             # ABSTRACT: Plaintext route definitions for Mojolicious
4             $Mojolicious::Plugin::PlainRoutes::VERSION = '0.06';
5 4     4   424 use Mojo::Base 'Mojolicious::Plugin';
  4         7152  
  4         22  
6 4     4   4139 use Mojo::Util qw/decamelize/;
  4         77175  
  4         9535  
7              
8             has autoname => 0;
9              
10             sub register {
11 4     4 1 24696 my ($self, $app, $conf) = @_;
12              
13 4         16 $self->autoname($conf->{autoname});
14              
15 4   33     34 $conf->{file} //= $app->home->rel_file("lib/".$app->moniker.".routes");
16              
17 4     2   109 open my $fh, '<:encoding(UTF-8)', $conf->{file};
  2         13  
  2         2  
  2         16  
18 4         2177 my $tree = $self->tokenise($fh);
19 4         103 close $fh;
20              
21 4         21 $self->process($app->routes, $tree);
22             }
23              
24             sub tokenise {
25 13     13 0 26859 my ($self, $input) = @_;
26              
27 13 100       48 if (ref $input eq 'GLOB') {
    50          
28 4         5 $input = do { local $/; <$input> };
  4         14  
  4         75  
29             } elsif (ref $input) {
30 0         0 Carp::carp "Non-filehandle reference passed to tokenise";
31 0         0 return [];
32             }
33              
34 13         79 return $self->_tokenise($input);
35             }
36              
37             sub _tokenise {
38 13     13   18 my ($self, $input) = @_;
39              
40 13         26 $input =~ s/\r\n/\n/g;
41 13         24 $input =~ s/\n\r/\n/g;
42 13         20 $input =~ s/\r/\n/g;
43              
44 13         195 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       3324 my @words = grep { defined && length }
  526         1281  
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         92 my @annotated_words;
71 13         25 for my $word (@words) {
72 263         571 my @cats = grep { $word =~ /^$grammar{$_}$/ } keys %grammar;
  2367         85004  
73              
74 263 50       733 if (@cats > 1) {
75 0         0 warn "$word has multiple lexical categories: @cats";
76             }
77              
78 263   50     1026 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         30 push @annotated_words, { text => '', category => 'eof' };
83              
84             # Initialise
85 13         17 my $root = [];
86 13         21 my @nodes = ($root);
87 13         25 my %clause = ();
88 13         16 my $context = 'default';
89              
90             # Track for helpful error messages
91 13         14 my $col = 1;
92 13         12 my $line = 1;
93 13         13 my $error = 0;
94              
95             # Define outside the loop scope so that the closure can access it
96 13         13 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         58 };
105              
106 13         24 for (@annotated_words) {
107 276         639 %word = %$_;
108 276         355 $col += length $word{text};
109 276 100       408 if ($word{category} eq 'eol') {
110 27         23 $line += 1;
111 27         26 $col = 1;
112             }
113              
114             # While in comment context, the parser checks for newlines and
115             # otherwise does nothing.
116 276 100 100     1102 if ($context eq 'comment') {
    100          
    100          
    100          
    100          
    100          
117 2 50       6 if ($word{category} eq 'eol') {
118 2         3 $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         5 $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     63 if ($word{category} eq 'verb') {
    100          
    50          
134 22         41 $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       46 if ($word{category} eq 'path') {
158 28         49 $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     188 if (!exists $clause{arrow} && $word{category} eq 'arrow') {
    50          
167 27         38 $clause{arrow} = 1;
168             } elsif ($word{category} eq 'action') {
169 28         79 my ($action, $controller) = split /\./, $word{text};
170 28         77 $clause{action} = decamelize($action) . "#$controller";
171              
172             # The clause needn't carry this useless information after this
173             # point.
174 28         447 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     132 if (!exists $clause{name} && $word{category} eq 'name') {
    100          
    100          
    50          
186 2         12 $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       28 if ($word{text} eq '{') {
    50          
194 7         25 my $newNode = [ { %clause } ];
195 7         10 push @{ $nodes[-1] }, $newNode;
  7         13  
196 7         8 push @nodes, $newNode;
197              
198 7         16 %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         5 push @{ $nodes[-1] }, { %clause };
  6         22  
205 6         11 %clause = ();
206              
207             # Can't exit a scope if we haven't entered one
208 6 50       13 if (@nodes == 1) {
209 0         0 'verb'->$syntax_error;
210             } else {
211 6         8 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         6 push @{ $nodes[-1] }, { %clause };
  6         21  
219 6         19 %clause = ( verb => $word{text} );
220             }
221              
222             # Last chance, the clause is terminated by eof
223             elsif ($word{category} eq 'eof') {
224 9         18 push @{ $nodes[-1] }, { %clause };
  9         35  
225 9         22 %clause = ();
226             }
227              
228             else {
229 0         0 'terminator'->$syntax_error;
230             }
231             }
232             }
233              
234 13 50       28 if (@nodes != 1) {
235 0         0 'verb or end of scope'->$syntax_error;
236             }
237              
238 13 50       26 if ($error) {
239 0         0 Carp::croak "Parsing routes failed due to syntax errors";
240             }
241              
242 13         274 $root;
243             }
244              
245             sub process {
246 5     5 0 58 my ($self, $bridge, $tree) = @_;
247              
248 5         11 for my $node (@$tree) {
249 9 100       21 my $token = ref $node eq 'ARRAY' ? shift @$node : $node;
250              
251             my $route = $bridge->route($token->{path})
252 9         51 ->to($token->{action});
253 9 100       1670 if ($token->{verb} ne 'ANY') {
254 8         26 $route->via($token->{verb});
255             }
256              
257 9         93 my $p = $route->pattern;
258 9 50       50 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->via->[0], $p->unparsed,
263 1         7 @{$p->defaults}{qw/controller action/});
  1         9  
264              
265 1 50       18 if (ref $name) {
266 0         0 Carp::croak "Autoname callback did not return a string";
267             }
268              
269 1         7 $route->name($name);
270             }
271             elsif ($self->autoname) {
272 1         7 $route->name(join '-', @{$p->defaults}{qw/controller action/});
  1         3  
273             }
274              
275 9 100       124 if (ref $node eq 'ARRAY') {
276 1         6 $route->inline(1);
277 1         10 $self->process($route, $node);
278             }
279             }
280             }
281              
282             1;
283              
284             __END__