File Coverage

blib/lib/Devel/Optic/Lens/Perlish/Parser.pm
Criterion Covered Total %
statement 131 136 96.3
branch 63 70 90.0
condition 16 18 88.8
subroutine 11 11 100.0
pod 0 2 0.0
total 221 237 93.2


line stmt bran cond sub pod time code
1             package Devel::Optic::Lens::Perlish::Parser;
2             $Devel::Optic::Lens::Perlish::Parser::VERSION = '0.014';
3             # ABSTRACT: Lexer/parser for Perlish lens
4              
5 4     4   559 use strict;
  4         6  
  4         94  
6 4     4   16 use warnings;
  4         7  
  4         87  
7              
8 4     4   16 use Exporter qw(import);
  4         7  
  4         151  
9             our @EXPORT_OK = qw(parse lex);
10              
11 4     4   20 use Carp qw(croak);
  4         7  
  4         248  
12             our @CARP_NOT = qw(Devel::Optic::Lens::Perlish Devel::Optic);
13              
14 4     4   1085 use Devel::Optic::Lens::Perlish::Constants qw(:all);
  4         8  
  4         542  
15              
16             use constant {
17 4         4309 'ACCESS_OPERATOR' => '->',
18             'HASHKEY_OPEN' => '{',
19             'HASHKEY_CLOSE' => '}',
20             'ARRAYINDEX_OPEN' => '[',
21             'ARRAYINDEX_CLOSE' => ']',
22 4     4   23 };
  4         8  
23              
24             my %symbols = map { $_ => 1 } qw({ } [ ]);
25              
26             sub parse {
27 74     74 0 9651 my ($route) = @_;
28 74         125 my @tokens = lex($route);
29 71         134 return _parse_tokens(@tokens);
30             }
31              
32             # %foo->{'bar'}->[-2]->{$baz->{'asdf'}}->{'blorg}'}
33             sub lex {
34 93     93 0 12014 my ($str) = @_;
35              
36 93 100       190 if (!defined $str) {
37 1         177 croak "invalid syntax: undefined query";
38             }
39              
40             # ignore whitespace
41 92         313 my @chars = grep { $_ !~ /\s/ } split //, $str;
  1457         2363  
42 92         198 my ( $elem, @items );
43              
44 92 100       172 if (scalar @chars == 0) {
45 2         260 croak "invalid syntax: empty query";
46             }
47              
48 90 100 100     324 if ($chars[0] ne '$' && $chars[0] ne '@' && $chars[0] ne '%') {
      100        
49 2         181 croak 'invalid syntax: query must start with a Perl symbol (prefixed by a $, @, or % sigil)';
50             }
51              
52 88         117 my $in_string;
53 88         184 for ( my $idx = 0; $idx <= $#chars; $idx++ ) {
54 1309         1503 my $char = $chars[$idx];
55 1309         1598 my $has_next = $#chars >= $idx + 1;
56 1309         1587 my $next = $chars[ $idx + 1 ];
57              
58             # Special case: escaped characters
59 1309 100 66     1984 if ( $char eq '\\' && $has_next ) {
60 4         6 $elem .= $next;
61 4         4 $idx++;
62 4         8 next;
63             }
64              
65             # Special case: string handling
66 1305 100       1840 if ( $char eq "'") {
67 69         88 $in_string = !$in_string;
68 69         82 $elem .= $char;
69 69         98 next;
70             }
71              
72             # Special case: arrow
73 1236 100 100     2831 if ( !$in_string && $char eq '-' && $has_next ) {
      66        
74 123 100       198 if ( $next eq '>' ) {
75 114 100       179 if (defined $elem) {
76 78         138 push @items, $elem;
77 78         97 undef $elem;
78             }
79 114         148 $idx++;
80 114         153 push @items, '->';
81 114         213 next;
82             }
83             }
84              
85 1122 100 100     2378 if ( !$in_string && exists $symbols{$char} ) {
86 221 100       318 if (defined $elem) {
87 97         146 push @items, $elem;
88 97         114 undef $elem;
89             }
90 221         270 push @items, $char;
91 221         374 next;
92             }
93              
94             # Special case: last item
95 901 100       1230 if ( !$has_next ) {
96 24         32 $elem .= $char;
97 24         41 push @items, $elem;
98 24         35 last; # unnecessary, but more readable, I think
99             }
100              
101             # Normal case
102 877         1463 $elem .= $char;
103             }
104              
105 88 100       134 if ($in_string) {
106 1         87 croak "invalid syntax: unclosed string";
107             }
108              
109 87         384 return @items;
110             }
111              
112             sub _parse_hash {
113 46     46   98 my @tokens = @_;
114 46         55 my $brace_count = 0;
115 46         57 my $close_index;
116 46         86 for (my $i = 0; $i <= $#tokens; $i++) {
117 180 100       277 if ($tokens[$i] eq HASHKEY_OPEN) {
118 53         63 $brace_count++;
119             }
120 180 100       298 if ($tokens[$i] eq HASHKEY_CLOSE) {
121 52         54 $brace_count--;
122 52 100       85 if ($brace_count == 0) {
123 45         55 $close_index = $i;
124 45         66 last;
125             }
126             }
127             }
128              
129 46 100       164 croak sprintf("invalid syntax: unclosed hash key (missing '%s')", HASHKEY_CLOSE) if !defined $close_index;
130 45 50       70 croak "invalid syntax: empty hash key" if $close_index == 1;
131              
132 45         89 return $close_index, [OP_HASHKEY, _parse_tokens(@tokens[1 .. $close_index-1])];
133             }
134              
135             sub _parse_array {
136 47     47   124 my @tokens = @_;
137 47         59 my $bracket_count = 0;
138 47         52 my $close_index;
139 47         83 for (my $i = 0; $i <= $#tokens; $i++) {
140 172 100       240 if ($tokens[$i] eq ARRAYINDEX_OPEN) {
141 54         61 $bracket_count++;
142             }
143              
144 172 100       400 if ($tokens[$i] eq ARRAYINDEX_CLOSE) {
145 53         60 $bracket_count--;
146 53 100       79 if ($bracket_count == 0) {
147 46         49 $close_index = $i;
148 46         63 last;
149             }
150             }
151             }
152              
153 47 100       168 croak sprintf("invalid syntax: unclosed array index (missing '%s')", ARRAYINDEX_CLOSE) if !defined $close_index;
154 46 50       69 croak "invalid syntax: empty array index" if $close_index == 1;
155              
156 46         96 return $close_index, [OP_ARRAYINDEX, _parse_tokens(@tokens[1 .. $close_index-1])];
157             }
158              
159             sub _parse_tokens {
160 162     162   297 my (@tokens) = @_;
161 162         195 my $left_node;
162 162         266 for (my $i = 0; $i <= $#tokens; $i++) {
163 257         326 my $token = $tokens[$i];
164              
165 257 100       536 if ($token =~ /^[\$\%\@]/) {
166 98 100       296 if ($token !~ /^[\$\%\@]\w+$/) {
167 1         176 croak sprintf 'invalid symbol: "%s". symbols must start with a Perl sigil ($, %%, or @) and contain only word characters', $token;
168             }
169              
170 97         186 $left_node = [SYMBOL, $token];
171 97         186 next;
172             }
173              
174 159 100       371 if ($token =~ /^-?\d+$/) {
175 34         75 $left_node = [NUMBER, 0+$token];
176 34         68 next;
177             }
178              
179 125 50       199 if ($token eq HASHKEY_OPEN) {
180 0         0 croak sprintf "found '%s' outside of a %s operator. use %s regardless of sigil",
181             HASHKEY_OPEN, ACCESS_OPERATOR, ACCESS_OPERATOR;
182             }
183              
184 125 50       184 if ($token eq HASHKEY_CLOSE) {
185 0         0 croak sprintf "found '%s' outside of a %s operator", HASHKEY_CLOSE, ACCESS_OPERATOR;
186             }
187              
188 125 50       176 if ($token eq ARRAYINDEX_OPEN) {
189 0         0 croak sprintf "found '%s' outside of a %s operator. use %s regardess of sigil",
190             ARRAYINDEX_OPEN, ACCESS_OPERATOR, ACCESS_OPERATOR;
191             }
192              
193 125 50       166 if ($token eq ARRAYINDEX_CLOSE) {
194 0         0 croak sprintf "found '%s' outside of a %s operator", ARRAYINDEX_CLOSE, ACCESS_OPERATOR;
195             }
196              
197 125 100       193 if ($token eq ACCESS_OPERATOR) {
198 95         114 my $next = $tokens[++$i];
199 95 100       149 if (!defined $next) {
200 1         92 croak sprintf "invalid syntax: '%s' needs something on the right hand side", ACCESS_OPERATOR;
201             }
202              
203 94         102 my $right_node;
204 94 100       164 if ($next eq HASHKEY_OPEN) {
    100          
205 46         116 my ($close_index, $hash_node) = _parse_hash(@tokens[$i .. $#tokens]);
206 44         65 $right_node = $hash_node;
207 44         51 $i += $close_index;
208             } elsif ($next eq ARRAYINDEX_OPEN) {
209 47         108 my ($close_index, $array_node) = _parse_array(@tokens[$i .. $#tokens]);
210 46         66 $right_node = $array_node;
211 46         55 $i += $close_index;
212             } else {
213 1         94 croak sprintf(
214             q|invalid syntax: %s expects either hash key "%s'foo'%s" or array index "%s0%s" on the right hand side. found '%s' instead|,
215             ACCESS_OPERATOR,
216             HASHKEY_OPEN, HASHKEY_CLOSE,
217             ARRAYINDEX_OPEN, ARRAYINDEX_CLOSE,
218             $next,
219             );
220             }
221              
222 90 50       142 if (!defined $left_node) {
223 0         0 croak sprintf("%s requires something on the left side", ACCESS_OPERATOR);
224             }
225              
226 90         144 $left_node = [OP_ACCESS, [
227             $left_node,
228             $right_node,
229             ]];
230              
231 90         164 next;
232             }
233              
234 30 100       89 if ($token =~ /^'(.+)'$/) {
235 29         80 $left_node = [STRING, $1];
236 29         65 next;
237             }
238              
239 1         98 croak "unrecognized token '$token'. hash key strings must be quoted with single quotes"
240             }
241              
242 155         468 return $left_node;
243             }
244              
245             1;
246              
247             __END__