File Coverage

blib/lib/Path/Dispatcher/Rule/Tokens.pm
Criterion Covered Total %
statement 62 76 81.5
branch 15 30 50.0
condition 13 15 86.6
subroutine 11 12 91.6
pod 0 3 0.0
total 101 136 74.2


line stmt bran cond sub pod time code
1             package Path::Dispatcher::Rule::Tokens;
2             # ABSTRACT: predicate is a list of tokens
3              
4             our $VERSION = '1.08';
5              
6 32     32   73711 use Moo;
  32         9469  
  32         203  
7 32     32   13329 use MooX::TypeTiny;
  32         375  
  32         194  
8 32     32   35498 use Types::Standard qw(Str ArrayRef Bool);
  32         102075  
  32         275  
9              
10             extends 'Path::Dispatcher::Rule';
11              
12             has tokens => (
13             is => 'ro',
14             isa => ArrayRef,
15             required => 1,
16             );
17              
18             has delimiter => (
19             is => 'ro',
20             isa => Str,
21             default => ' ',
22             );
23              
24             has case_sensitive => (
25             is => 'ro',
26             isa => Bool,
27             default => 1,
28             );
29              
30             sub _match_as_far_as_possible {
31 84     84   127 my $self = shift;
32 84         121 my $path = shift;
33              
34 84         230 my @got = $self->tokenize($path->path);
35 84         162 my @expected = @{ $self->tokens };
  84         232  
36 84         131 my @matched;
37              
38 84   100     360 while (@got && @expected) {
39 102         288 my $expected = $expected[0];
40 102         162 my $got = $got[0];
41              
42 102 100       213 last unless $self->_match_token($got, $expected);
43              
44 62         142 push @matched, $got;
45 62         99 shift @expected;
46 62         214 shift @got;
47             }
48              
49 84         241 return (\@matched, \@got, \@expected);
50             }
51              
52             sub _match {
53 84     84   134 my $self = shift;
54 84         128 my $path = shift;
55              
56 84         186 my ($matched, $got, $expected) = $self->_match_as_far_as_possible($path);
57              
58 84 100       596 return if @$expected; # didn't provide everything necessary
59 43 100 100     152 return if @$got && !$self->prefix; # had tokens left over
60              
61 39         107 my $leftover = $self->untokenize(@$got);
62              
63 39 50       101 return if !$matched;
64              
65             return {
66 39         179 positional_captures => $matched,
67             leftover => $leftover,
68             };
69             }
70              
71             sub complete {
72 0     0 0 0 my $self = shift;
73 0         0 my $path = shift;
74              
75 0         0 my ($matched, $got, $expected) = $self->_match_as_far_as_possible($path);
76 0 0       0 return if @$got > 1; # had tokens leftover
77 0 0       0 return if !@$expected; # consumed all tokens
78              
79 0         0 my $next = shift @$expected;
80 0 0       0 my $part = @$got ? shift @$got : '';
81 0         0 my @completions;
82              
83 0 0       0 for my $completion (ref($next) eq 'ARRAY' ? @$next : $next) {
84 0 0       0 next if ref($completion);
85              
86 0 0       0 next unless substr($completion, 0, length($part)) eq $part;
87 0         0 push @completions, $self->untokenize(@$matched, $completion);
88             }
89              
90 0         0 return @completions;
91             }
92              
93             sub _each_token {
94 118     118   175 my $self = shift;
95 118         181 my $got = shift;
96 118         175 my $expected = shift;
97 118         172 my $callback = shift;
98              
99 118 100 66     388 if (ref($expected) eq 'ARRAY') {
    50          
100 11         20 for my $alternative (@$expected) {
101 16         38 $self->_each_token($got, $alternative, $callback);
102             }
103             }
104             elsif (!ref($expected) || ref($expected) eq 'Regexp') {
105 107         221 $callback->($got, $expected);
106             }
107             else {
108 0         0 die "Unexpected token '$expected'"; # the irony is not lost on me :)
109             }
110             }
111              
112             sub _match_token {
113 102     102   159 my $self = shift;
114 102         260 my $got = shift;
115 102         148 my $expected = shift;
116              
117 102         163 my $matched = 0;
118             $self->_each_token($got, $expected, sub {
119 107     107   225 my ($g, $e) = @_;
120 107 100       240 if (!ref($e)) {
    50          
121 102 100       269 ($g, $e) = (lc $g, lc $e) if !$self->case_sensitive;
122 102   100     385 $matched ||= $g eq $e;
123             }
124             elsif (ref($e) eq 'Regexp') {
125 5   66     34 $matched ||= $g =~ $e;
126             }
127 102         521 });
128              
129 102         536 return $matched;
130             }
131              
132             sub tokenize {
133 84     84 0 128 my $self = shift;
134 84         137 my $path = shift;
135 84         1002 return grep { length } split $self->delimiter, $path;
  171         545  
136             }
137              
138             sub untokenize {
139 39     39 0 61 my $self = shift;
140 39         90 my @tokens = @_;
141             return join $self->delimiter,
142 24         85 grep { length }
143 39         116 map { split $self->delimiter, $_ }
  24         212  
144             @tokens;
145             }
146              
147             __PACKAGE__->meta->make_immutable;
148 32     32   52266 no Moo;
  32         103  
  32         211  
149              
150             1;
151              
152             __END__
153              
154             =pod
155              
156             =encoding UTF-8
157              
158             =head1 NAME
159              
160             Path::Dispatcher::Rule::Tokens - predicate is a list of tokens
161              
162             =head1 VERSION
163              
164             version 1.08
165              
166             =head1 SYNOPSIS
167              
168             my $rule = Path::Dispatcher::Rule::Tokens->new(
169             tokens => [ "comment", "show", qr/^\d+$/ ],
170             delimiter => '/',
171             block => sub { display_comment(shift->pos(3)) },
172             );
173              
174             $rule->match("/comment/show/25");
175              
176             =head1 DESCRIPTION
177              
178             Rules of this class use a list of tokens to match the path.
179              
180             =head1 ATTRIBUTES
181              
182             =head2 tokens
183              
184             Each token can be a literal string, a regular expression, or a list of either
185             (which are taken to mean alternations). For example, the tokens:
186              
187             [ 'ticket', [ 'show', 'display' ], [ qr/^\d+$/, qr/^#\w{3}/ ] ]
188              
189             first matches "ticket". Then, the next token must be "show" or "display". The
190             final token must be a number or a pound sign followed by three word characters.
191              
192             The results are the tokens in the original string, as they were matched. If you
193             have three tokens, then C<< match->pos(1) >> will be the string's first token
194             ("ticket"), C<< match->pos(2) >> its second ("display"), and C<< match->pos(3)
195             >> its third ("#AAA").
196              
197             Capture groups inside a regex token are completely ignored.
198              
199             =head2 delimiter
200              
201             A string that is used to tokenize the path. The delimiter must be a string
202             because prefix matches use C<join> on unmatched tokens to return the leftover
203             path. In the future this may be extended to support having a regex delimiter.
204              
205             The default is a space, but if you're matching URLs you probably want to change
206             this to a slash.
207              
208             =head2 case_sensitive
209              
210             Decide whether the rule matching is case sensitive. Default is 1, case
211             sensitive matching.
212              
213             =head1 SUPPORT
214              
215             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Path-Dispatcher>
216             (or L<bug-Path-Dispatcher@rt.cpan.org|mailto:bug-Path-Dispatcher@rt.cpan.org>).
217              
218             =head1 AUTHOR
219              
220             Shawn M Moore, C<< <sartak at bestpractical.com> >>
221              
222             =head1 COPYRIGHT AND LICENSE
223              
224             This software is copyright (c) 2020 by Shawn M Moore.
225              
226             This is free software; you can redistribute it and/or modify it under
227             the same terms as the Perl 5 programming language system itself.
228              
229             =cut