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.013';
3             # ABSTRACT: Lexer/parser for Perlish lens
4              
5 4     4   516 use strict;
  4         7  
  4         92  
6 4     4   16 use warnings;
  4         6  
  4         86  
7              
8 4     4   16 use Exporter qw(import);
  4         6  
  4         134  
9             our @EXPORT_OK = qw(parse lex);
10              
11 4     4   24 use Carp qw(croak);
  4         16  
  4         311  
12             our @CARP_NOT = qw(Devel::Optic::Lens::Perlish Devel::Optic);
13              
14 4     4   1129 use Devel::Optic::Lens::Perlish::Constants qw(:all);
  4         9  
  4         618  
15              
16             use constant {
17 4         4378 'ACCESS_OPERATOR' => '->',
18             'HASHKEY_OPEN' => '{',
19             'HASHKEY_CLOSE' => '}',
20             'ARRAYINDEX_OPEN' => '[',
21             'ARRAYINDEX_CLOSE' => ']',
22 4     4   26 };
  4         7  
23              
24             my %symbols = map { $_ => 1 } qw({ } [ ]);
25              
26             sub parse {
27 74     74 0 9684 my ($route) = @_;
28 74         123 my @tokens = lex($route);
29 71         143 return _parse_tokens(@tokens);
30             }
31              
32             # %foo->{'bar'}->[-2]->{$baz->{'asdf'}}->{'blorg}'}
33             sub lex {
34 93     93 0 11639 my ($str) = @_;
35              
36 93 100       192 if (!defined $str) {
37 1         177 croak "invalid syntax: undefined query";
38             }
39              
40             # ignore whitespace
41 92         319 my @chars = grep { $_ !~ /\s/ } split //, $str;
  1457         2433  
42 92         192 my ( $elem, @items );
43              
44 92 100       178 if (scalar @chars == 0) {
45 2         245 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         176 croak 'invalid syntax: query must start with a Perl symbol (prefixed by a $, @, or % sigil)';
50             }
51              
52 88         109 my $in_string;
53 88         177 for ( my $idx = 0; $idx <= $#chars; $idx++ ) {
54 1309         1532 my $char = $chars[$idx];
55 1309         1569 my $has_next = $#chars >= $idx + 1;
56 1309         1568 my $next = $chars[ $idx + 1 ];
57              
58             # Special case: escaped characters
59 1309 100 66     2014 if ( $char eq '\\' && $has_next ) {
60 4         5 $elem .= $next;
61 4         5 $idx++;
62 4         7 next;
63             }
64              
65             # Special case: string handling
66 1305 100       1735 if ( $char eq "'") {
67 69         81 $in_string = !$in_string;
68 69         84 $elem .= $char;
69 69         112 next;
70             }
71              
72             # Special case: arrow
73 1236 100 100     2912 if ( !$in_string && $char eq '-' && $has_next ) {
      66        
74 123 100       199 if ( $next eq '>' ) {
75 114 100       165 if (defined $elem) {
76 78         146 push @items, $elem;
77 78         100 undef $elem;
78             }
79 114         136 $idx++;
80 114         136 push @items, '->';
81 114         206 next;
82             }
83             }
84              
85 1122 100 100     2385 if ( !$in_string && exists $symbols{$char} ) {
86 221 100       321 if (defined $elem) {
87 97         133 push @items, $elem;
88 97         127 undef $elem;
89             }
90 221         268 push @items, $char;
91 221         372 next;
92             }
93              
94             # Special case: last item
95 901 100       1225 if ( !$has_next ) {
96 24         31 $elem .= $char;
97 24         35 push @items, $elem;
98 24         40 last; # unnecessary, but more readable, I think
99             }
100              
101             # Normal case
102 877         1487 $elem .= $char;
103             }
104              
105 88 100       133 if ($in_string) {
106 1         85 croak "invalid syntax: unclosed string";
107             }
108              
109 87         413 return @items;
110             }
111              
112             sub _parse_hash {
113 46     46   107 my @tokens = @_;
114 46         51 my $brace_count = 0;
115 46         74 my $close_index;
116 46         88 for (my $i = 0; $i <= $#tokens; $i++) {
117 180 100       262 if ($tokens[$i] eq HASHKEY_OPEN) {
118 53         66 $brace_count++;
119             }
120 180 100       323 if ($tokens[$i] eq HASHKEY_CLOSE) {
121 52         58 $brace_count--;
122 52 100       78 if ($brace_count == 0) {
123 45         54 $close_index = $i;
124 45         60 last;
125             }
126             }
127             }
128              
129 46 100       176 croak sprintf("invalid syntax: unclosed hash key (missing '%s')", HASHKEY_CLOSE) if !defined $close_index;
130 45 50       67 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   132 my @tokens = @_;
137 47         79 my $bracket_count = 0;
138 47         50 my $close_index;
139 47         92 for (my $i = 0; $i <= $#tokens; $i++) {
140 172 100       257 if ($tokens[$i] eq ARRAYINDEX_OPEN) {
141 54         66 $bracket_count++;
142             }
143              
144 172 100       287 if ($tokens[$i] eq ARRAYINDEX_CLOSE) {
145 53         60 $bracket_count--;
146 53 100       82 if ($bracket_count == 0) {
147 46         64 $close_index = $i;
148 46         56 last;
149             }
150             }
151             }
152              
153 47 100       172 croak sprintf("invalid syntax: unclosed array index (missing '%s')", ARRAYINDEX_CLOSE) if !defined $close_index;
154 46 50       65 croak "invalid syntax: empty array index" if $close_index == 1;
155              
156 46         95 return $close_index, [OP_ARRAYINDEX, _parse_tokens(@tokens[1 .. $close_index-1])];
157             }
158              
159             sub _parse_tokens {
160 162     162   286 my (@tokens) = @_;
161 162         188 my $left_node;
162 162         268 for (my $i = 0; $i <= $#tokens; $i++) {
163 257         332 my $token = $tokens[$i];
164              
165 257 100       552 if ($token =~ /^[\$\%\@]/) {
166 98 100       289 if ($token !~ /^[\$\%\@]\w+$/) {
167 1         129 croak sprintf 'invalid symbol: "%s". symbols must start with a Perl sigil ($, %%, or @) and contain only word characters', $token;
168             }
169              
170 97         178 $left_node = [SYMBOL, $token];
171 97         201 next;
172             }
173              
174 159 100       351 if ($token =~ /^-?\d+$/) {
175 34         69 $left_node = [NUMBER, 0+$token];
176 34         78 next;
177             }
178              
179 125 50       191 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       181 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       194 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       173 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       191 if ($token eq ACCESS_OPERATOR) {
198 95         132 my $next = $tokens[++$i];
199 95 100       137 if (!defined $next) {
200 1         103 croak sprintf "invalid syntax: '%s' needs something on the right hand side", ACCESS_OPERATOR;
201             }
202              
203 94         101 my $right_node;
204 94 100       159 if ($next eq HASHKEY_OPEN) {
    100          
205 46         122 my ($close_index, $hash_node) = _parse_hash(@tokens[$i .. $#tokens]);
206 44         68 $right_node = $hash_node;
207 44         56 $i += $close_index;
208             } elsif ($next eq ARRAYINDEX_OPEN) {
209 47         103 my ($close_index, $array_node) = _parse_array(@tokens[$i .. $#tokens]);
210 46         64 $right_node = $array_node;
211 46         62 $i += $close_index;
212             } else {
213 1         103 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       132 if (!defined $left_node) {
223 0         0 croak sprintf("%s requires something on the left side", ACCESS_OPERATOR);
224             }
225              
226 90         139 $left_node = [OP_ACCESS, [
227             $left_node,
228             $right_node,
229             ]];
230              
231 90         163 next;
232             }
233              
234 30 100       87 if ($token =~ /^'(.+)'$/) {
235 29         69 $left_node = [STRING, $1];
236 29         58 next;
237             }
238              
239 1         102 croak "unrecognized token '$token'. hash key strings must be quoted with single quotes"
240             }
241              
242 155         473 return $left_node;
243             }
244              
245             1;
246              
247             __END__