File Coverage

blib/lib/Query/Tags/To/AST.pm
Criterion Covered Total %
statement 106 114 92.9
branch 27 34 79.4
condition 7 12 58.3
subroutine 35 40 87.5
pod 0 6 0.0
total 175 206 84.9


line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =head1 NAME
4              
5             Query::Tags::To::AST - Build AST from Query
6              
7             =cut
8              
9 5     5   1894 use v5.16;
  5         22  
10 5     5   30 use strict;
  5         9  
  5         227  
11 5     5   28 use warnings;
  5         10  
  5         474  
12              
13             =head1 DESCRIPTION
14              
15             The Query::Tags::To::AST package implements a L based
16             on L. It is invoked from the Pegex parser engine to build
17             the syntax tree contained in a L object. What follows is
18             a list of all node types which appear in this syntax tree.
19              
20             =cut
21              
22             =head2 Query::Tags::To::AST::Query
23              
24             This is the root object and represents the entire query.
25             Each assertion in the query is a pair object.
26              
27             =head3 new
28              
29             my $query = Query::Tags::To::AST::Query->new(@pairs);
30              
31             Create a new query from a list of assertions.
32              
33             =head3 pairs
34              
35             my @pairs = $query->pairs;
36              
37             Return the list of assertions.
38              
39             =head3 test
40              
41             $query->test($x, \%opts) ? 'PASS' : 'FAIL'
42              
43             Check if C<$x> passes all assertions.
44              
45             =cut
46              
47             package Query::Tags::To::AST::Query {
48 5     5   3108 use List::SomeUtils qw(all);
  5         85025  
  5         1492  
49              
50             sub new {
51 11     11   30 my $class = shift;
52 11         59 bless [ @_ ], $class
53             }
54              
55 1     1   11 sub pairs { @{+shift} }
  1         7  
56              
57             sub test {
58 10     10   25 my ($self, $arg, $opts) = @_;
59 10     10   129 all { $_->test($arg, $opts) } @$self
  10         38  
60             }
61             }
62              
63             =head2 Query::Tags::To::AST::Pair
64              
65             A key-value pair represents the assertion that the key should
66             exist and the values should match.
67              
68             =head3 new
69              
70             my $pair = Query::Tags::To::AST::Pair->new($key, $value);
71              
72             Create a new pair object.
73              
74             =head3 key
75              
76             my $key = $pair->key;
77              
78             Return the key as a Perl string.
79              
80             =head3 value
81              
82             my $value = $pair->value;
83              
84             Return the value (another C object).
85              
86             =head3 test
87              
88             $pair->test($x, \%opts) ? 'PASS' : 'FAIL'
89              
90             Check if C<$x> matches the pair. This means the following:
91             if C<$x> is a blessed object and it has a method named C<$key>,
92             then it is invoked and its return value tested against C<$value>.
93             Otherwise, if C<$x> is a hashref, the C<$key> is looked up
94             and its value is used. Otherwise the test fails.
95              
96             If C<$key> is undefined, the C is looked up in
97             the options hashref C<\%opts>. See L for
98             an explanation of its behavior. If both C<$key> and C
99             are undefined, the match fails.
100              
101             If C<$value> is C, then only existence of the method
102             or the hash key is required and its value is ignored. If instead
103             C<$value> is (not blessed and) equal to the string C,
104             then the value behind C<$key> is checked for truthiness.
105              
106             =cut
107              
108             package Query::Tags::To::AST::Pair {
109 5     5   60 use Scalar::Util qw(blessed reftype);
  5         15  
  5         2636  
110              
111             sub new {
112 27     27   207 my $class = shift;
113 27         55 my ($key, $value) = @_;
114 27         143 bless [ $key, $value ], $class
115             }
116              
117             # When the value is undefined, this means that we check for existence
118             # of the key, not for undefinedness of the value!
119 4     4   4554 sub key { shift->[0] }
120 4     4   59 sub value { shift->[1] }
121              
122             sub test {
123 23     23   672 my ($self, $arg, $opts) = @_;
124 23         63 my ($key, $value) = @$self;
125              
126 23 100       70 if (not defined $key) {
127 10         24 $key = $opts->{default_key};
128 10 50       22 return 0 if not defined $key;
129 10 100 66     49 if (ref($key) and reftype($key) eq 'CODE') {
130 4         15 return $key->($arg, $value, $opts);
131             }
132             }
133              
134 19 100 66     121 if (blessed($arg) and $arg->can($key)) {
    50          
135 8 100       30 return 1 if not defined $value;
136 6         25 my $v = $arg->$key;
137 6 100       46 return 0 if not defined $v;
138 4 100 66     43 return !!$v if not blessed($value) and $value eq '?';
139 1         4 return $value->test($v);
140             }
141             elsif (reftype($arg) eq 'HASH') {
142 11 100       34 return exists $arg->{$key} if not defined $value;
143 9         19 my $v = $arg->{$key};
144 9 50       14 return 0 if not defined $v;
145 9 50 33     26 return !!$v if not blessed($value) and $value eq '?';
146 9         60 return $value->test($v);
147             }
148 0         0 return 0;
149             }
150             }
151              
152             =head2 Query::Tags::To::AST::String
153              
154             Represents a string. This object has the stringification
155             and string comparison operators overloaded.
156              
157             =head3 new
158              
159             my $string = Query::Tags::To::AST::String->new($s);
160              
161             Create a new string object from a Perl scalar string.
162              
163             =head3 value
164              
165             my $s = $string->value;
166              
167             Return the Perl scalar string.
168              
169             =head3 test
170              
171             $string->test($x) ? 'PASS' : 'FAIL'
172              
173             Check if C<< $x eq $s >>.
174              
175             =cut
176              
177             package Query::Tags::To::AST::String {
178             use overload
179 5         57 '""' => \&to_string,
180 5     5   3583 'cmp' => \&_cmp;
  5         20450  
181              
182             sub new {
183 35     35   73 my $class = shift;
184 35         64 my $string = shift;
185 35         169 bless \$string, $class
186             }
187              
188 21     21   32 sub value { "". ${+shift} }
  21         212  
189 21     21   83 sub to_string { shift->value }
190              
191             sub _cmp {
192 17     17   41 my ($self, $other, $swap) = @_;
193 17 50       63 ($self, $other) = ($other, $self) if $swap;
194 17         50 "$self" cmp "$other"
195             }
196              
197             sub test {
198 17     17   59 my ($self, $arg) = @_;
199 17         128 $self eq $arg
200             }
201             }
202              
203             =head2 Query::Tags::To::AST::Regex
204              
205             Represents a regex. This object stringifies to the regex pattern.
206              
207             =head3 new
208              
209             my $regex = Query::Tags::To::AST::Regex->new($re);
210              
211             Create a new regex object from a Perl regex.
212              
213             =head3 value
214              
215             my $re = $regex->value;
216              
217             Return the underlying Perl regex.
218              
219             =head3 test
220              
221             $regex->test($x) ? 'PASS' : 'FAIL'
222              
223             Check if C<< $x =~ m/$re/ >>.
224              
225             =cut
226              
227             package Query::Tags::To::AST::Regex {
228 5     5   1668 use overload '""' => \&to_string;
  5         12  
  5         29  
229              
230             sub new {
231 31     31   251855 my $class = shift;
232 31         64 my $regex = shift;
233 31         196 bless [ $regex ], $class
234             }
235              
236 0     0   0 sub value { shift->[0] }
237 0     0   0 sub to_string { "". shift->value }
238              
239             sub test {
240 23     23   52 my ($self, $arg) = @_;
241 23         125 my $re = $self->[0];
242 23         202 $arg =~ m/$re/
243             }
244             }
245              
246             =head2 Query::Tags::To::AST::Junction
247              
248             Represents a junction, a superposition of multiple values which compares
249             to a single value using a given mode.
250              
251             =head3 new
252              
253             my $j = Query::Tags::To::AST::Junction->new($negate, $type, @values);
254              
255             Create a new junction of C<$type> (optionally negated if C<$negate>
256             is truthy) over the given C<@values>.
257              
258             =head3 negated
259              
260             my $negated = $j->negated;
261              
262             Whether the junction is negated.
263              
264             =head3 type
265              
266             my $type = $j->type;
267              
268             Return the junction type as a string C<&>, C<|> or C.
269              
270             =head3 values
271              
272             my @values = $j->values;
273              
274             Return the values in the junction (as C objects).
275              
276             =head3 test
277              
278             $j->test($x) ? 'PASS' : 'FAIL'
279              
280             Check if C<$x> matches the junction. The type C<&> implements an L
281             junction, C<|> implements L and C implements L.
282             These modes govern how the results of testing C<$x> against the C<@values>
283             are interpreted. If the junction is negated, then the result will be inverted
284             after it was computed.
285              
286             =cut
287              
288             package Query::Tags::To::AST::Junction {
289 5     5   1050 use List::SomeUtils qw(any all none);
  5         21  
  5         1775  
290              
291             sub new {
292 15     15   29 my $class = shift;
293 15         46 my ($negate, $type, @list) = @_;
294 15         110 bless [ $negate, $type, [ @list ] ], $class
295             }
296              
297 0     0   0 sub negated { !!shift->[0] }
298 0     0   0 sub type { shift->[1] }
299 0     0   0 sub values { @{shift->[2]} }
  0         0  
300              
301             sub test {
302 15     15   645 my ($self, $arg) = @_;
303 15         50 my ($negate, $type, $list) = @$self;
304 15         24 my $res = do {
305 15 100       81 if ($type eq '&') {
    100          
    50          
306 5     12   41 all { $_->test($arg) } @$list;
  12         31  
307             }
308             elsif ($type eq '|') {
309 7     12   90 any { $_->test($arg) } @$list;
  12         37  
310             }
311             elsif ($type eq '!') {
312 3     7   23 none { $_->test($arg) } @$list;
  7         21  
313             }
314             else {
315 0         0 die "unknown junction type '$type'";
316             }
317             };
318 15 100       135 $negate ? !$res : $res
319             }
320             }
321              
322             package Query::Tags::To::AST;
323              
324 5     5   40 use Pegex::Base;
  5         8  
  5         48  
325             extends 'Pegex::Tree';
326              
327             sub got_query {
328 11     11 0 4093 my ($items) = @{+pop};
  11         21  
329 11         17 my @pairs;
330 11         22 for my $item (@$items) {
331 14 50       144 $item = Query::Tags::To::AST::Pair->new(undef, $item)
332             unless $item->isa('Query::Tags::To::AST::Pair');
333 14         30 push @pairs, $item;
334             }
335 11         50 Query::Tags::To::AST::Query->new(@pairs)
336             }
337              
338             sub got_pair {
339 13     13 0 7202 my ($key, $value) = @{+pop};
  13         45  
340 13         76 Query::Tags::To::AST::Pair->new($key => $value)
341             }
342              
343             sub got_bareword {
344 29     29 0 32930 my $word = pop;
345 29         138 Query::Tags::To::AST::String->new($word)
346             }
347              
348             sub got_string {
349 3     3 0 4714 my $string = pop;
350 3         20 Query::Tags::To::AST::String->new($string)
351             }
352              
353             sub got_regex {
354 30     30 0 24949 my $regex = pop;
355 30         680 Query::Tags::To::AST::Regex->new(qr/$regex/x)
356             }
357              
358             sub got_junction {
359 15     15 0 12344 my ($negate, $type, $values) = @{+pop};
  15         48  
360 15         78 Query::Tags::To::AST::Junction->new($negate, $type, @$values)
361             }
362              
363             ":wq"