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.25";
3              
4 72     72   55435 use strict;
  72         126  
  72         1578  
5 72     72   279 use warnings;
  72         116  
  72         77076  
6              
7             sub new {
8 80     80 0 376 my $class = shift;
9 80         123 my @args = @_;
10              
11 80         166 my $this = {
12             KEY_GROUPS => [],
13             };
14              
15 80         133 bless $this, $class;
16              
17 80         259 $this->add_groups($_) foreach @args;
18              
19 80         137 return $this;
20             };
21              
22             sub has_any_group {
23 41     41 0 1028 my $this = shift;
24 41         47 return (scalar @{$this->{'KEY_GROUPS'}}) > 0;
  41         133  
25             }
26              
27             sub add_groups {
28 68     68 0 109 my $this = shift;
29 68         84 my $groups = shift;
30              
31 68         244 foreach my $group_spec (split(',', $groups)) {
32 86         117 my $group;
33 86 100       228 if ( $group_spec =~ m/^!/ ) {
34 27         106 $group = App::RecordStream::KeyGroups::Group->new($group_spec);
35             }
36             else {
37 59         227 $group = App::RecordStream::KeyGroups::KeySpec->new($group_spec);
38             }
39              
40 86         110 push @{$this->{'KEY_GROUPS'}}, $group;
  86         251  
41             }
42             }
43              
44             sub get_keyspecs_for_record {
45 752     752 0 847 my $this = shift;
46 752         761 my $record = shift;
47              
48 752         728 my @specs;
49              
50 752         709 foreach my $group ( @{$this->{'KEY_GROUPS'}} ) {
  752         936  
51 1190         1247 push @specs, @{$group->get_fields($record)};
  1190         1480  
52             }
53              
54 752         1415 return \@specs;
55             }
56              
57             # This is a cached version
58             sub get_keyspecs {
59 38     38 0 55 my $this = shift;
60 38         67 my $record = shift;
61              
62 38 100       78 if ( !$this->{'KEY_SPECS'} ) {
63 25         69 $this->{'KEY_SPECS'} = $this->get_keyspecs_for_record($record);
64             }
65              
66 38         100 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   87 my $class = shift;
110 59         79 my $spec = shift;
111              
112 59         121 my $this = {
113             SPEC => $spec,
114             };
115              
116 59         116 return bless $this, $class;
117             }
118              
119             sub get_fields {
120 1035     1035   1031 my $this = shift;
121 1035         949 my $record = shift;
122              
123 1035 50       1611 if ( $record->has_key_spec($this->{'SPEC'}) ) {
124 1035         1080 return [join('/', @{$record->get_key_list_for_spec($this->{'SPEC'})})];
  1035         1693  
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   136 my $class = shift;
147 28         58 my $group_spec = shift;
148              
149 28         39 my $this = {
150             };
151              
152 28         55 bless $this, $class;
153              
154 28         71 $this->parse_group($group_spec);
155 28         43 return $this;
156             }
157              
158             sub get_fields {
159 163     163   191 my $this = shift;
160 163         154 my $record = shift;
161              
162 163         163 my @specs;
163 163         186 my $regex = $this->{'REGEX'};
164 163         190 foreach my $spec (@{$this->get_specs($record)}) {
  163         214  
165 751 100       2206 if ( $spec =~ m/$regex/ ) {
166 283         488 push @specs, $spec;
167             }
168             }
169              
170             #TODO: deal with sorts
171 163 100       297 if ( $this->has_option('sort') ) {
172 99         183 @specs = sort @specs;
173             }
174 163         390 return \@specs;
175             }
176              
177             sub get_specs {
178 163     163   173 my $this = shift;
179 163         164 my $record = shift;
180              
181 163         163 my $min_depth = 1;
182 163         161 my $max_depth = 1;
183              
184 163 100       246 if ( $this->has_option('full_match') ) {
    100          
185 5         5 $max_depth = -1;
186              
187             }
188             elsif ( $this->has_option('depth') ) {
189 1         4 my $depth = $this->option_value('depth');
190 1         2 $min_depth = $depth;
191 1         2 $max_depth = $depth;
192             }
193              
194 163         226 my $paths = [];
195 163         649 $this->_get_paths({%$record}, 1, $min_depth, $max_depth, [], $paths);
196 163         338 return [map { join('/', @$_) } @$paths];
  751         1256  
197             }
198              
199             sub _get_paths {
200 891     891   846 my $this = shift;
201 891         848 my $data = shift;
202 891         789 my $current_depth = shift;
203 891         774 my $min_depth = shift;
204 891         810 my $max_depth = shift;
205 891         801 my $current_keys = shift;
206 891         800 my $found_paths = shift;
207              
208 891 100       1113 if ( $current_depth >= $min_depth ) {
209 890 100 100     1307 if ( ref($data) eq '' || $this->has_option('return_refs') ) {
210 751         992 push @$found_paths, [@$current_keys];
211             }
212             }
213              
214 891 100       1144 if ( ref($data) eq 'ARRAY' ) {
215 2         4 my $index = -1;
216 2         3 foreach my $value ( @$data ) {
217 3         3 $index++;
218 3 50 33     12 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       1453 if ( ref($data) eq 'HASH') {
229 175         354 foreach my $key (keys %$data) {
230 733 100 100     1018 if ( $current_depth <= $max_depth || $max_depth == -1 ) {
231 728         1195 $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   2971 my $this = shift;
244 41         49 my $spec = shift;
245              
246 41 100       109 if ( '!' ne substr($spec, 0, 1) ) {
247 2         8 die "Malformed group spec: '$spec', does not start with '!'\n";
248             }
249              
250 39 50       76 if ( length($spec) < 2 ) {
251 0         0 die "Malformed group spec: '$spec', does not have enough length\n";
252             }
253              
254 39         71 my $regex = '';
255 39         48 my $last_char = '';
256 39         41 my $found_end = 0;
257 39         50 my $start_option_index = 1;
258              
259 39         85 for (my $index = 1; $index < length($spec); $index++) {
260 148         136 $start_option_index++;
261 148         182 my $current_char = substr($spec, $index, 1);
262              
263 148 100       196 if ( $current_char eq '!' ) {
264 38 50       64 if ( $last_char ne '\\' ) {
265 38         43 $last_char = $current_char;
266 38         47 $found_end = 1;
267 38         52 last;
268             }
269             }
270 110         113 $last_char = $current_char;
271 110         132 $regex .= $current_char;
272 110         160 next;
273             }
274              
275 39 100       68 die "Malformed group spec: Did not find terminating '!' in '$spec'\n" if ( ! $found_end );
276              
277 38         55 my $options_string = substr($spec, $start_option_index);
278 38         51 my $options = {};
279              
280 38         93 foreach my $option_group (split('!', $options_string)) {
281 36         71 my ($option, $value) = split('=', $option_group);
282 36 100       82 if ( my $normalized_option = $VALID_OPTIONS->{$option} ) {
283 35 50       60 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         73 $options->{$normalized_option} = $value;
288             }
289             }
290             else {
291 1         6 die "Malformed group spec: Unrecognized option: '$option' in '$spec'\n";
292             }
293             }
294              
295 37         99 $this->{'REGEX'} = $regex;
296 37         72 $this->{'OPTIONS'} = $options;
297             }
298              
299             sub has_option {
300 660     660   630 my $this = shift;
301 660         623 my $option = shift;
302              
303 660         1317 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         2 return $this->{'OPTIONS'}->{$option};
311             }
312              
313             1;