File Coverage

blib/lib/App/YAML/Filter/Regex.pm
Criterion Covered Total %
statement 97 107 90.6
branch 82 92 89.1
condition 22 27 81.4
subroutine 6 6 100.0
pod 0 2 0.0
total 207 234 88.4


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