File Coverage

blib/lib/ETL/Yertl/Command/yq/Regex.pm
Criterion Covered Total %
statement 116 130 89.2
branch 97 110 88.1
condition 28 40 70.0
subroutine 7 7 100.0
pod 0 3 0.0
total 248 290 85.5


line stmt bran cond sub pod time code
1             package ETL::Yertl::Command::yq::Regex;
2             our $VERSION = '0.035';
3             # ABSTRACT: A regex-based parser for programs
4              
5 7     7   2357 use ETL::Yertl;
  7         13  
  7         29  
6 7     7   225 use boolean qw( :all );
  7         13  
  7         29  
7 7     7   2659 use Regexp::Common;
  7         13549  
  7         26  
8              
9             sub empty() {
10 5     5 0 69 bless {}, 'empty';
11             }
12              
13             sub is_empty($) {
14 139     139 0 412 return ref $_[0] eq 'empty';
15             }
16              
17             *diag = *yertl::diag;
18              
19             my $QUOTE_STRING = $RE{delimited}{-delim=>q{'"}};
20             my $EVAL_NUMS = qr{(?:0b$RE{num}{bin}|0$RE{num}{oct}|0x$RE{num}{hex})};
21              
22             # Match a document path
23             our $GRAMMAR = qr{
24             (?(DEFINE)
25             (?
26             (?:\$?[.](?:\w+|\[\d*\]))+ # hash/array lookup
27             |
28             \$?[.] # entire document
29             |
30             $QUOTE_STRING
31             |
32             $RE{num}{real}|$EVAL_NUMS
33             |
34             \w+ # Constant/bareword
35             )
36             (?eq|ne|==?|!=|>=?|<=?)
37             (?empty|select|grep|group_by|keys|length|sort|each)
38             (?
39             \{(\s*(?&FILTER)\s*:\s*(?0)\s*(?:,(?-1))*)\} # Hash constructor
40             |
41             \[(\s*(?0)\s*(?:,(?-1))*)\] # Array constructor
42             |
43             (?&FUNC_NAME)(?:\(\s*((?&EXPR))\s*\))? # Function with optional argument
44             |
45             (?:(?&FILTER)|(?&FUNC_NAME)(?:\(\s*((?&EXPR))\s*\))?)\s+(?&OP)\s+(?&EXPR) # Binop with filter
46             |
47             (?&FILTER)
48             )
49             )
50             }x;
51              
52             my $FILTER = qr{(?&FILTER)$GRAMMAR};
53             my $OP = qr{(?&OP)$GRAMMAR};
54             my $FUNC_NAME = qr{(?&FUNC_NAME)$GRAMMAR};
55             my $EXPR = qr{(?&EXPR)$GRAMMAR};
56             my $PIPE = qr{[|]};
57              
58             # Filter MUST NOT mutate $doc!
59             sub filter {
60 327     327 0 264770 my ( $class, $filter, $doc, $scope, $orig_doc ) = @_;
61 327   66     879 $orig_doc ||= $doc;
62              
63             # Pipes: LEFT | RIGHT pipes the output of LEFT to the input of RIGHT
64 327 100       12010 if ( $filter =~ $PIPE ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
65 11         511 my @exprs = split /\s*$PIPE\s*/, $filter;
66 11         36 my @in = ( $doc );
67 11         27 for my $expr ( @exprs ) {
68 22         35 my @out = ();
69 22         36 for my $doc ( @in ) {
70 27         107 push @out, $class->filter( $expr, $doc, $scope, $orig_doc );
71             }
72 22         137 @in = @out;
73             }
74 11         38 return @in;
75             }
76             # Hash constructor
77             elsif ( $filter =~ /^{/ ) {
78 6         13 my %out;
79 6         40 my ( $inner ) = $filter =~ /^\{\s*([^\}]+?)\s*\}$/;
80 6         24 for my $pair ( split /\s*,\s*/, $inner ) {
81 8         34 my ( $key_filter, $value_expr ) = split /\s*:\s*/, $pair;
82 8         39 my $key = $class->filter( $key_filter, $doc, $scope, $orig_doc );
83 8         23 $out{ $key } = $class->filter( $value_expr, $doc, $scope, $orig_doc );
84             }
85 6         25 return \%out;
86             }
87             # Array constructor
88             elsif ( $filter =~ /^\[/ ) {
89 1         2 my @out;
90 1         8 my ( $inner ) = $filter =~ /^\[\s*([^\]]+?)\s*\]$/;
91 1         8 for my $value_expr ( split /\s*,\s*/, $inner ) {
92 5         11 push @out, $class->filter( $value_expr, $doc, $scope, $orig_doc );
93             }
94 1         6 return \@out;
95             }
96             # , does multiple filters, yielding multiple documents
97             elsif ( $filter =~ /,/ ) {
98 1         13 my @filters = split /\s*,\s*/, $filter;
99 1         3 return map { $class->filter( $_, $doc, $scope, $orig_doc ) } @filters;
  3         26  
100             }
101             # Function calls
102             elsif ( $filter =~ /^((?&FUNC_NAME))(?:\(\s*((?&EXPR))\s*\))?$GRAMMAR$/ ) {
103 43         156 my ( $func, $expr ) = ( $1, $2 );
104 43   100     238 diag( 1, "F: $func, ARG: " . ( $expr || '' ) );
105 43 100 100     266 if ( $func eq 'empty' ) {
    100          
    100          
    100          
    100          
    100          
    50          
106 1 50       3 if ( $expr ) {
107 0         0 warn "empty does not take arguments\n";
108             }
109 1         4 return empty;
110             }
111             elsif ( $func eq 'select' || $func eq 'grep' ) {
112 8 50       17 if ( !$expr ) {
113 0         0 warn "'$func' takes an expression argument";
114 0         0 return empty;
115             }
116 8 100       39 return $class->filter( $expr, $doc, $scope, $orig_doc ) ? $doc : empty;
117             }
118             elsif ( $func eq 'group_by' ) {
119 6         14 my $grouping = $class->filter( $expr, $doc, $scope, $orig_doc );
120 6         12 push @{ $scope->{ group_by }{ $grouping } }, $doc;
  6         21  
121 6         17 return;
122             }
123             elsif ( $func eq 'sort' ) {
124 3   50     8 $expr ||= '.';
125 3         7 my $value = $class->filter( $expr, $doc, $scope, $orig_doc );
126 3         6 push @{ $scope->{sort} }, [ "$value", $doc ];
  3         10  
127 3         10 return;
128             }
129             elsif ( $func eq 'keys' ) {
130 5   100     13 $expr ||= '.';
131 5         14 my $value = $class->filter( $expr, $doc, $scope, $orig_doc );
132 5 100       14 if ( ref $value eq 'HASH' ) {
    50          
133 4         20 return [ keys %$value ];
134             }
135             elsif ( ref $value eq 'ARRAY' ) {
136 1         4 return [ 0..$#{ $value } ];
  1         4  
137             }
138             else {
139 0         0 warn "keys() requires a hash or array";
140 0         0 return empty;
141             }
142             }
143             elsif ( $func eq 'each' ) {
144 7   100     25 $expr ||= '.';
145 7         22 my $value = $class->filter( $expr, $doc, $scope, $orig_doc );
146 7 100       30 if ( ref $value eq 'HASH' ) {
    50          
147 6         59 return map +{ key => $_, value => $value->{ $_ } }, keys %$value;
148             }
149             elsif ( ref $value eq 'ARRAY' ) {
150 1         18 return map +{ key => $_, value => $value->[ $_ ] }, 0..$#$value;
151             }
152             else {
153 0         0 warn "each() requires a hash or array";
154 0         0 return empty;
155             }
156             }
157             elsif ( $func eq 'length' ) {
158 13   100     35 $expr ||= '.';
159 13         48 my $value = $class->filter( $expr, $doc, $scope, $orig_doc );
160 13 100       47 if ( ref $value eq 'HASH' ) {
    100          
    50          
161 3         11 return scalar keys %$value;
162             }
163             elsif ( ref $value eq 'ARRAY' ) {
164 7         20 return scalar @$value;
165             }
166             elsif ( !ref $value ) {
167 3         10 return length $value;
168             }
169             else {
170 0         0 warn "length() requires a hash, array, string, or number";
171 0         0 return empty;
172             }
173             }
174             }
175             # Hash and array keys to traverse the data structure
176             elsif ( $filter =~ /^((?&FILTER))$GRAMMAR$/ ) {
177             # Extract quoted strings
178 194 100       1843 if ( $filter =~ /^(['"])(.+)(\1)$/ ) {
    100          
    100          
179 9         48 return $2;
180             }
181             # Eval numbers to allow bin, hex, and oct
182             elsif ( $filter =~ /^$EVAL_NUMS$/ ) {
183             ## no critic ( ProhibitStringyEval )
184 3         230 return eval $filter;
185             }
186             # Constants/barewords do not begin with .
187             elsif ( $filter !~ /^[\$.]/ ) {
188             # If it's not a reserved word, it's a string
189             # XXX: This is a very poor decision...
190 43         132 return $filter;
191             }
192              
193 139 100       354 if ( is_empty $doc ) {
194 2         11 return empty;
195             }
196              
197 137         431 my @keys = split /[.]/, $filter;
198 137 100 66     362 my $subdoc = $keys[0] && $keys[0] eq '$' ? $orig_doc : $doc;
199 137         354 for my $key ( @keys[1..$#keys] ) {
200 135 100       534 if ( $key =~ /^\[\]$/ ) {
    100          
    50          
201 1         3 return @{ $subdoc };
  1         6  
202             }
203             elsif ( $key =~ /^\[(\d+)\]$/ ) {
204 4         15 $subdoc = $subdoc->[ $1 ];
205             }
206             elsif ( $key =~ /^\w+$/ ) {
207 130         302 $subdoc = $subdoc->{ $key };
208             }
209             else {
210 0         0 die "Invalid filter key '$key'";
211             }
212             }
213 136         488 return $subdoc;
214             }
215              
216             # Binary operators (binops)
217             elsif ( $filter =~ /^((?&FILTER)|(?&FUNC_NAME)(?:\(\s*(?&EXPR)\s*\))?)\s+((?&OP))\s+((?&EXPR))$GRAMMAR$/ ) {
218 68         286 my ( $lhs_filter, $cond, $rhs_filter ) = ( $1, $2, $3 );
219 68 100       143 if ( $cond eq '=' ) {
220             # Get the referent from the left-hand side
221 10         33 my @keys = split /[.]/, $lhs_filter;
222 10 50 33     34 my $subdoc = $keys[0] && $keys[0] eq '$' ? \$orig_doc : \$doc;
223 10         26 for my $key ( @keys[1..$#keys] ) {
224 10 50       45 if ( $key =~ /^\[(\d+)\]$/ ) {
    50          
225 0         0 $subdoc = \( $$subdoc->[ $1 ] );
226             }
227             elsif ( $key =~ /^\w+$/ ) {
228 10         30 $subdoc = \( $$subdoc->{ $key } );
229             }
230             else {
231 0         0 die "Invalid filter key '$key'";
232             }
233             }
234              
235 10         53 my $rhs_value = $class->filter( $rhs_filter, $doc, $scope, $orig_doc );
236 10   50     56 diag( 1, join " ", "BINOP:", $lhs_filter, $cond, $rhs_value // '' );
237 10         28 $$subdoc = $rhs_value;
238 10         37 return $doc; # Assignment does not change current document
239             }
240             else {
241 58         190 my $lhs_value = $class->filter( $lhs_filter, $doc, $scope, $orig_doc );
242 58         131 my $rhs_value = $class->filter( $rhs_filter, $doc, $scope, $orig_doc );
243 58   50     366 diag( 1, join " ", "BINOP:", $lhs_value // '', $cond, $rhs_value // '' );
      50        
244             # These operators suppress undef warnings, treating undef as just
245             # another value. Undef will never be treated as '' or 0 here.
246 58 100       230 if ( $cond eq 'eq' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
247 9 100 66     64 return defined $lhs_value == defined $rhs_value
248             && $lhs_value eq $rhs_value ? true : false;
249             }
250             elsif ( $cond eq 'ne' ) {
251 4 100 66     21 return defined $lhs_value != defined $rhs_value
252             || $lhs_value ne $rhs_value ? true : false;
253             }
254             elsif ( $cond eq '==' ) {
255 14 100 66     95 return defined $lhs_value == defined $rhs_value
256             && $lhs_value == $rhs_value ? true : false;
257             }
258             elsif ( $cond eq '!=' ) {
259 4 100 66     21 return defined $lhs_value != defined $rhs_value
260             || $lhs_value != $rhs_value ? true : false;
261             }
262             # These operators allow undef warnings, since equating undef to 0 or ''
263             # can be a cause of problems.
264             elsif ( $cond eq '>' ) {
265 6 100       18 return $lhs_value > $rhs_value ? true : false;
266             }
267             elsif ( $cond eq '>=' ) {
268 9 100       34 return $lhs_value >= $rhs_value ? true : false;
269             }
270             elsif ( $cond eq '<' ) {
271 6 100       20 return $lhs_value < $rhs_value ? true : false;
272             }
273             elsif ( $cond eq '<=' ) {
274 6 100       20 return $lhs_value <= $rhs_value ? true : false;
275             }
276             }
277             }
278             # Conditional (if/then/else)
279             # NOTE: If we're capturing using $EXPR, then we _must_ use named captures,
280             # because $EXPR has captures in itself
281             elsif ( $filter =~ /^if\s+(?$EXPR)\s+then\s+(?$FILTER)(?:\s+else\s+(?$FILTER))?$/ ) {
282 7     7   902454 my ( $expr, $true_filter, $false_filter ) = @+{qw( expr true false )};
  7         2367  
  7         1824  
  3         35  
283 3         19 my $expr_value = $class->filter( $expr, $doc, $scope, $orig_doc );
284 3 100       49 if ( $expr_value ) {
285 2         19 return $class->filter( $true_filter, $doc, $scope, $orig_doc );
286             }
287             else {
288 1 50       10 return $false_filter ? $class->filter( $false_filter, $doc, $scope, $orig_doc ) : ();
289             }
290             }
291             else {
292 0           die "Could not parse filter '$filter'\n";
293             }
294 0           return;
295             }
296              
297             1;
298              
299             __END__