File Coverage

blib/lib/Data/Seek/Search.pm
Criterion Covered Total %
statement 129 131 98.4
branch 34 38 89.4
condition 11 15 73.3
subroutine 15 15 100.0
pod 3 3 100.0
total 192 202 95.0


line stmt bran cond sub pod time code
1             # ABSTRACT: Data::Seek Search Execution Class
2             package Data::Seek::Search;
3              
4 3     3   684 use 5.10.0;
  3         10  
  3         137  
5 3     3   14 use strict;
  3         6  
  3         94  
6 3     3   15 use warnings;
  3         6  
  3         105  
7              
8 3     3   1328 use Data::Seek::Exception;
  3         25  
  3         68  
9 3     3   1880 use Data::Seek::Exception::RootInvalid;
  3         8  
  3         72  
10 3     3   1899 use Data::Seek::Exception::RootUnknown;
  3         7  
  3         76  
11 3     3   1911 use Data::Seek::Exception::NodeInvalid;
  3         7  
  3         80  
12 3     3   1944 use Data::Seek::Exception::NodeUnknown;
  3         7  
  3         71  
13 3     3   1334 use Data::Seek::Search::Result;
  3         8  
  3         87  
14              
15 3     3   15 use Mo 'builder', 'default';
  3         4  
  3         11  
16              
17             our $VERSION = '0.05'; # VERSION
18              
19             has 'cache',
20             default => 0;
21              
22             has 'criteria',
23             default => sub {{}};
24              
25             has 'data',
26             default => sub {{}};
27              
28             has 'data_cache',
29             builder => '_build_data_cache';
30              
31             sub _build_data_cache {
32             shift->data->encode
33 1     1   20 }
34              
35             has 'ignore',
36             default => 0;
37              
38             sub criterion {
39 52     52 1 4463 my $self = shift;
40 52         84 my $expr = shift;
41              
42             Data::Seek::Exception->throw(
43             message => 'INVALID CRITERION PROVIDED'
44             )
45 52 100 66     142 unless $expr && $expr =~ do {
46 52         72 my $name = '[\w\*]+';
47 52         80 my $indx = ':\d+';
48 52         87 my $iter = '\@';
49              
50 52         353 my $sect = join '|',
51             "(?:${indx}\\.|${indx}\$)",
52             "(?:${iter}\\.|${iter}\$)",
53             "(?:${name}\\.|${name}\$)",
54             "(?:${name}${indx}\\.|${name}${indx}\$)";
55              
56 52         13659 qr/^(?:${indx}|(${sect})+(?:${iter}|${name}(?:${indx})?)?)$/;
57             };
58              
59 51         213 $self->criteria->{$expr} = keys %{$self->criteria};
  51         191  
60 51         586 return $self;
61             }
62              
63             sub perform {
64 47     47 1 63 my $self = shift;
65              
66 47         120 my $criteria = $self->criteria;
67 47         436 $criteria = { reverse %$criteria };
68              
69 47 100       180 my $dataset = $self->cache ?
70             $self->data_cache : $self->data->encode;
71              
72 47         325146 my @orders = sort keys %$criteria;
73 47         155 my @criteria = @$criteria{@orders};
74              
75 47         79 my @results;
76              
77 47         119 for my $criterion (@criteria) {
78 48         1204 my @nodes = keys %$dataset;
79 48         466 my @selectors = split /\./, $criterion;
80 48 100       109 @selectors = map { /([^\.]+):(\d+)/ ? ($1, ":$2") : $_ } @selectors;
  100         532  
81              
82 48         92 my @expressions;
83 48         210 for (my $i=0; $i<@selectors; $i++) {
84 107         181 my $first = $i == 0;
85 107         172 my $last = $i == $#selectors;
86              
87 107         183 my $selector = $selectors[$i];
88 107         178 my $query = quotemeta $selector;
89 107         161 my $keys = [];
90              
91 107         168 my $token;
92             my $regex;
93              
94 107         167 $token = quotemeta '\@';
95 107         145 $regex = ':\d+';
96 107         423 $query =~ s/^$token$/$regex/g;
97              
98 107         166 $token = quotemeta '\*';
99 107         354 $token = sprintf '(?:%s){2,}', $token;
100 107         152 $regex = '.*';
101 107         316 $query =~ s/$token/$regex/g;
102              
103 107         176 $token = quotemeta '\*';
104 107         196 $token = sprintf '(?:%s){1}', $token;
105 107         139 $regex = '[^\.]+';
106 107         236 $query =~ s/$token/$regex/g;
107              
108 107         466 push @expressions, $query;
109             }
110              
111 48         98 my $was_match = 0;
112 48         62 my $was_array = 0;
113 48         70 my $was_hash = 0;
114 48         75 my $was_ending = 0;
115              
116 48         147 for (my $i=0; $i<@expressions; $i++) {
117 97         161 my $first = $i == 0;
118 97         158 my $last = $i == $#expressions;
119 97         402 my @steps = grep defined, @selectors[0..$i];
120 97         242 my @query = @expressions[0..$i];
121              
122 97         159 my $query = shift @query;
123 97 100       208 $query .= join '', map { /^\\?:/ ? $_ : '\.' . $_ } @query;
  71         319  
124              
125 97         543 my $index = $steps[-1] =~ /^\@|[^\.]+:\d+$/;
126              
127 97         132 my $is_match = 0;
128 97         138 my $is_array = 0;
129 97         109 my $is_hash = 0;
130 97         132 my $is_ending = 0;
131              
132 97 100       713 $is_array = grep /^$query/, @nodes if $index;
133 97 100       6806 $is_array = grep /^$query:\d+/, @nodes if !$index;
134 97         7036 $is_hash = grep /^$query\./, @nodes;
135              
136 97         6798 $is_match = @nodes = grep /^$query/, @nodes;
137 97 100       2519 $is_ending = @nodes = grep /^$query$/, @nodes if $last;
138              
139 97 100       247 if (@nodes) {
140 74         95 $was_match = $is_match;
141 74         107 $was_array = $is_array;
142 74         80 $was_hash = $is_hash;
143 74         96 $was_ending = $is_ending;
144             }
145              
146 97 100       432 unless (@nodes) {
147 23 100       130 last if $self->ignore;
148              
149             my $format = sub {
150 36     36   58 my $expr = shift;
151 36 100       116 return "[0]" if $expr =~ /\@/;
152 28 100       79 return "[$1]" if $expr =~ /:(\d+)/;
153 26 50       68 return ($1, "[$2]") if $expr =~ /(.*):(\d+)/;
154 26 50       56 return "{...}" if $expr =~ /\*+/;
155 26         83 return $_;
156 11         205 };
157              
158 11         38 my $subject = [ map $format->($_), @steps ];
159 11         34 my $target = [ map $format->($_), @selectors ];
160              
161 11 100       35 if ($first) {
162 5 100 66     30 if (!$is_match) {
    50 66        
163 3         40 Data::Seek::Exception::RootUnknown->throw(
164             criterion => $criterion,
165             subject => $subject,
166             target => $target,
167             is_array => $is_array,
168             is_ending => $is_ending,
169             is_hash => $is_hash,
170             is_match => $is_match,
171             );
172             }
173             elsif ($is_match && !$is_hash && !$is_array) {
174 0         0 Data::Seek::Exception::RootUnknown->throw(
175             criterion => $criterion,
176             subject => $subject,
177             target => $target,
178             is_array => $is_array,
179             is_ending => $is_ending,
180             is_hash => $is_hash,
181             is_match => $is_match,
182             );
183             }
184             else {
185 2         34 Data::Seek::Exception::RootInvalid->throw(
186             criterion => $criterion,
187             subject => $subject,
188             target => $target,
189             is_array => $is_array,
190             is_ending => $is_ending,
191             is_hash => $is_hash,
192             is_match => $is_match,
193             );
194             }
195             }
196             else {
197 6 50 66     69 if (!$was_match) {
    100 100        
198 0         0 Data::Seek::Exception::NodeUnknown->throw(
199             criterion => $criterion,
200             subject => $subject,
201             target => $target,
202             was_array => $was_array,
203             was_ending => $was_ending,
204             was_hash => $was_hash,
205             was_match => $was_match,
206             );
207             }
208             elsif ($was_match && !$was_hash && !$was_array) {
209 3         42 Data::Seek::Exception::NodeUnknown->throw(
210             criterion => $criterion,
211             subject => $subject,
212             target => $target,
213             was_array => $was_array,
214             was_ending => $was_ending,
215             was_hash => $was_hash,
216             was_match => $was_match,
217             );
218             }
219             else {
220 3         42 Data::Seek::Exception::NodeInvalid->throw(
221             criterion => $criterion,
222             subject => $subject,
223             target => $target,
224             was_array => $was_array,
225             was_ending => $was_ending,
226             was_hash => $was_hash,
227             was_match => $was_match,
228             );
229             }
230             }
231             }
232             }
233 37         724 my $result = {nodes => [sort @nodes], criterion => $criterion};
234 37         186 push @results, $result;
235             }
236              
237 36         68 my $output = [];
238 36         81 for my $result (@results) {
239 37         53 $$result{dataset} = {map { $_ => $$dataset{$_} } @{$$result{nodes}}};
  460         849  
  37         95  
240 37         151 push @$output, $result;
241             }
242              
243 36         799 return $output;
244             }
245              
246             sub result {
247 47     47 1 8491 return Data::Seek::Search::Result->new(
248             search => shift
249             );
250             }
251              
252             1;
253              
254             __END__
255              
256             =pod
257              
258             =encoding UTF-8
259              
260             =head1 NAME
261              
262             Data::Seek::Search - Data::Seek Search Execution Class
263              
264             =head1 VERSION
265              
266             version 0.05
267              
268             =head1 SYNOPSIS
269              
270             use Data::Seek::Search;
271              
272             =head1 DESCRIPTION
273              
274             Data::Seek::Search is a class within L<Data::Seek> which provides the search
275             mechanism for introspecting data structures.
276              
277             =head1 ATTRIBUTES
278              
279             =head2 cache
280              
281             $search->cache;
282             $search->cache(1);
283              
284             Encode the data structure and cache the result. Allows multiple queries to
285             execute faster. Caching is disabled by default.
286              
287             =head2 criteria
288              
289             $search->criteria;
290             $search->criteria({
291             '*' => 0,
292             'person.name.first' => 1,
293             'person.name.last' => 2,
294             'person.settings.@.name' => 3,
295             'person.settings.@.type' => 4,
296             'person.settings.@.value' => 5,
297             });
298              
299             A collection of criterion which will be used to match nodes within the data
300             structure when introspected, in the order registered.
301              
302             =head2 data
303              
304             $search->data;
305             $search->data(Data::Seek::Data->new(...));
306              
307             The data structure to be introspected, must be a hash reference, blessed or not,
308             which defaults to or becomes a L<Data::Seek::Data> object.
309              
310             =head2 data_cache
311              
312             $search->data_cache;
313             $search->data_cache(Data::Seek::Data->new(...)->encode);
314              
315             The encoded data structure to be introspected, must be an encoded hash
316             reference, e.g. the result from calling the encode method on a
317             L<Data::Seek::Data> object.
318              
319             =head2 ignore
320              
321             $search->ignore;
322             $search->ignore(1);
323              
324             Bypass exceptions thrown when a criterion finds an unknown or invalid node in
325             the data structure.
326              
327             =head1 METHODS
328              
329             =head2 criterion
330              
331             $search->criterion('*');
332              
333             Register a criterion to be used to introspect the registered data structure. A
334             criterion is only valid if it begins with a array index, and array iterator, or
335             a node key; Also can only contain letters, numbers, underscores, periods, and
336             semi-colons. See L<Data::Seek::Concepts> for more information.
337              
338             =head2 perform
339              
340             my $dataset = $search->perform;
341              
342             Introspect the data structure using the registered criteria and settings, and
343             return a result set of operations and matching data nodes.
344              
345             =head2 result
346              
347             my $result = $search->result;
348              
349             Return a search result object, L<Data::Seek::Search::Result>, based on the
350             current search object.
351              
352             =encoding utf8
353              
354             =head1 AUTHOR
355              
356             Al Newkirk <anewkirk@ana.io>
357              
358             =head1 COPYRIGHT AND LICENSE
359              
360             This software is copyright (c) 2014 by Al Newkirk.
361              
362             This is free software; you can redistribute it and/or modify it under
363             the same terms as the Perl 5 programming language system itself.
364              
365             =cut