File Coverage

blib/lib/App/RecordStream/KeyGroups.pm
Criterion Covered Total %
statement 132 137 96.3
branch 35 40 87.5
condition 7 9 77.7
subroutine 16 17 94.1
pod 0 6 0.0
total 190 209 90.9


line stmt bran cond sub pod time code
1             package App::RecordStream::KeyGroups;
2             our $VERSION = "4.0.24";
3              
4 72     72   86960 use strict;
  72         158  
  72         2039  
5 72     72   397 use warnings;
  72         174  
  72         96769  
6              
7             sub new {
8 80     80 0 556 my $class = shift;
9 80         185 my @args = @_;
10              
11 80         228 my $this = {
12             KEY_GROUPS => [],
13             };
14              
15 80         167 bless $this, $class;
16              
17 80         221 $this->add_groups($_) foreach @args;
18              
19 80         376 return $this;
20             };
21              
22             sub has_any_group {
23 41     41 0 1355 my $this = shift;
24 41         66 return (scalar @{$this->{'KEY_GROUPS'}}) > 0;
  41         191  
25             }
26              
27             sub add_groups {
28 68     68 0 148 my $this = shift;
29 68         129 my $groups = shift;
30              
31 68         241 foreach my $group_spec (split(',', $groups)) {
32 86         147 my $group;
33 86 100       288 if ( $group_spec =~ m/^!/ ) {
34 27         166 $group = App::RecordStream::KeyGroups::Group->new($group_spec);
35             }
36             else {
37 59         356 $group = App::RecordStream::KeyGroups::KeySpec->new($group_spec);
38             }
39              
40 86         153 push @{$this->{'KEY_GROUPS'}}, $group;
  86         345  
41             }
42             }
43              
44             sub get_keyspecs_for_record {
45 752     752 0 1209 my $this = shift;
46 752         1007 my $record = shift;
47              
48 752         950 my @specs;
49              
50 752         1026 foreach my $group ( @{$this->{'KEY_GROUPS'}} ) {
  752         1313  
51 1190         1871 push @specs, @{$group->get_fields($record)};
  1190         1992  
52             }
53              
54 752         2113 return \@specs;
55             }
56              
57             # This is a cached version
58             sub get_keyspecs {
59 38     38 0 91 my $this = shift;
60 38         123 my $record = shift;
61              
62 38 100       106 if ( !$this->{'KEY_SPECS'} ) {
63 25         83 $this->{'KEY_SPECS'} = $this->get_keyspecs_for_record($record);
64             }
65              
66 38         120 return $this->{'KEY_SPECS'};
67             }
68              
69             sub usage {
70 0     0 0 0 return <
71             KEY GROUPS
72             __FORMAT_TEXT__
73             SYNTAX: !regex!opt1!opt2...
74             Key groups are a way of specifying multiple fields to a recs command with a
75             single argument or function. They are generally regexes, and have several
76             options to control what fields they match. By default you give a regex, and
77             it will be matched against all first level keys of a record to come up with
78             the record list. For instance, in a record like this:
79             __FORMAT_TEXT__
80              
81             { 'zip': 1, 'zap': 2, 'foo': { 'bar': 3 } }
82              
83             __FORMAT_TEXT__
84             Key group: !z! would get the keys 'zip' and 'zap'
85              
86             You can have a literal '!' in your regex, just escape it with a \\.
87              
88             Normally, key groups will only match keys whose values are scalars. This
89             can be changed with the 'returnrefs' or rr flag.
90              
91             With the above record !f! would match no fields, but !f!rr would match foo
92             (which has a value of a hash ref)
93              
94             Options on KeyGroups:
95             __FORMAT_TEXT__
96             returnrefs, rr - Return keys that have reference values (default:off)
97             full, f - Regex should match against full keys (recurse fully)
98             depth=NUM,d=NUM - Only match keys at NUM depth (regex will match against
99             full keyspec)
100             sort, s - sort keyspecs lexically
101             HELP
102             }
103              
104             1;
105              
106             package App::RecordStream::KeyGroups::KeySpec;
107              
108             sub new {
109 59     59   114 my $class = shift;
110 59         100 my $spec = shift;
111              
112 59         169 my $this = {
113             SPEC => $spec,
114             };
115              
116 59         146 return bless $this, $class;
117             }
118              
119             sub get_fields {
120 1035     1035   1492 my $this = shift;
121 1035         1316 my $record = shift;
122              
123 1035 50       2125 if ( $record->has_key_spec($this->{'SPEC'}) ) {
124 1035         1378 return [join('/', @{$record->get_key_list_for_spec($this->{'SPEC'})})];
  1035         2153  
125             }
126              
127 0         0 return [];
128             }
129              
130             1;
131              
132             package App::RecordStream::KeyGroups::Group;
133              
134             my $VALID_OPTIONS = {
135             d => 'depth',
136             depth => 'depth',
137             s => 'sort',
138             'sort' => 'sort',
139             f => 'full_match',
140             full => 'full_match',
141             rr => 'return_refs',
142             returnrefs => 'return_refs'
143             };
144              
145             sub new {
146 28     28   189 my $class = shift;
147 28         251 my $group_spec = shift;
148              
149 28         68 my $this = {
150             };
151              
152 28         65 bless $this, $class;
153              
154 28         120 $this->parse_group($group_spec);
155 28         64 return $this;
156             }
157              
158             sub get_fields {
159 163     163   314 my $this = shift;
160 163         262 my $record = shift;
161              
162 163         239 my @specs;
163 163         350 my $regex = $this->{'REGEX'};
164 163         258 foreach my $spec (@{$this->get_specs($record)}) {
  163         376  
165 751 100       4120 if ( $spec =~ m/$regex/ ) {
166 283         697 push @specs, $spec;
167             }
168             }
169              
170             #TODO: deal with sorts
171 163 100       510 if ( $this->has_option('sort') ) {
172 99         293 @specs = sort @specs;
173             }
174 163         650 return \@specs;
175             }
176              
177             sub get_specs {
178 163     163   268 my $this = shift;
179 163         257 my $record = shift;
180              
181 163         255 my $min_depth = 1;
182 163         255 my $max_depth = 1;
183              
184 163 100       349 if ( $this->has_option('full_match') ) {
    100          
185 5         10 $max_depth = -1;
186              
187             }
188             elsif ( $this->has_option('depth') ) {
189 1         5 my $depth = $this->option_value('depth');
190 1         2 $min_depth = $depth;
191 1         2 $max_depth = $depth;
192             }
193              
194 163         381 my $paths = [];
195 163         1084 $this->_get_paths({%$record}, 1, $min_depth, $max_depth, [], $paths);
196 163         613 return [map { join('/', @$_) } @$paths];
  751         2321  
197             }
198              
199             sub _get_paths {
200 891     891   1602 my $this = shift;
201 891         1378 my $data = shift;
202 891         1345 my $current_depth = shift;
203 891         1306 my $min_depth = shift;
204 891         1334 my $max_depth = shift;
205 891         1322 my $current_keys = shift;
206 891         1310 my $found_paths = shift;
207              
208 891 100       1794 if ( $current_depth >= $min_depth ) {
209 890 100 100     2249 if ( ref($data) eq '' || $this->has_option('return_refs') ) {
210 751         1739 push @$found_paths, [@$current_keys];
211             }
212             }
213              
214 891 100       2094 if ( ref($data) eq 'ARRAY' ) {
215 2         6 my $index = -1;
216 2         7 foreach my $value ( @$data ) {
217 3         6 $index++;
218 3 50 33     18 if ( $current_depth <= $max_depth || $max_depth == -1 ) {
219 0         0 $this->_get_paths($value,
220             $current_depth+1,
221             $min_depth,
222             $max_depth,
223             [@$current_keys, "\#index"],
224             $found_paths);
225             }
226             }
227             }
228 891 100       2636 if ( ref($data) eq 'HASH') {
229 175         577 foreach my $key (keys %$data) {
230 733 100 100     1769 if ( $current_depth <= $max_depth || $max_depth == -1 ) {
231 728         2341 $this->_get_paths($data->{$key},
232             $current_depth+1,
233             $min_depth,
234             $max_depth,
235             [@$current_keys, $key],
236             $found_paths);
237             }
238             }
239             }
240             }
241              
242             sub parse_group {
243 41     41   4049 my $this = shift;
244 41         84 my $spec = shift;
245              
246 41 100       162 if ( '!' ne substr($spec, 0, 1) ) {
247 2         15 die "Malformed group spec: '$spec', does not start with '!'\n";
248             }
249              
250 39 50       111 if ( length($spec) < 2 ) {
251 0         0 die "Malformed group spec: '$spec', does not have enough length\n";
252             }
253              
254 39         82 my $regex = '';
255 39         101 my $last_char = '';
256 39         199 my $found_end = 0;
257 39         70 my $start_option_index = 1;
258              
259 39         117 for (my $index = 1; $index < length($spec); $index++) {
260 148         219 $start_option_index++;
261 148         372 my $current_char = substr($spec, $index, 1);
262              
263 148 100       311 if ( $current_char eq '!' ) {
264 38 50       116 if ( $last_char ne '\\' ) {
265 38         76 $last_char = $current_char;
266 38         71 $found_end = 1;
267 38         81 last;
268             }
269             }
270 110         184 $last_char = $current_char;
271 110         178 $regex .= $current_char;
272 110         252 next;
273             }
274              
275 39 100       106 die "Malformed group spec: Did not find terminating '!' in '$spec'\n" if ( ! $found_end );
276              
277 38         125 my $options_string = substr($spec, $start_option_index);
278 38         100 my $options = {};
279              
280 38         141 foreach my $option_group (split('!', $options_string)) {
281 36         120 my ($option, $value) = split('=', $option_group);
282 36 100       137 if ( my $normalized_option = $VALID_OPTIONS->{$option} ) {
283 35 50       91 if ( exists $options->{$normalized_option} ) {
284 0         0 die "Already specified option '$option'. Bad option: '$option_group' in '$spec'\n";
285             }
286             else {
287 35         114 $options->{$normalized_option} = $value;
288             }
289             }
290             else {
291 1         10 die "Malformed group spec: Unrecognized option: '$option' in '$spec'\n";
292             }
293             }
294              
295 37         173 $this->{'REGEX'} = $regex;
296 37         116 $this->{'OPTIONS'} = $options;
297             }
298              
299             sub has_option {
300 660     660   1087 my $this = shift;
301 660         1029 my $option = shift;
302              
303 660         2279 return exists $this->{'OPTIONS'}->{$option};
304             }
305              
306             sub option_value {
307 1     1   3 my $this = shift;
308 1         2 my $option = shift;
309              
310 1         3 return $this->{'OPTIONS'}->{$option};
311             }
312              
313             1;