File Coverage

blib/lib/App/RecordStream/KeySpec.pm
Criterion Covered Total %
statement 147 156 94.2
branch 53 66 80.3
condition 11 16 68.7
subroutine 16 17 94.1
pod 0 7 0.0
total 227 262 86.6


line stmt bran cond sub pod time code
1             package App::RecordStream::KeySpec;
2              
3             =head1 NAME
4              
5             App::RecordStream::KeySpec
6              
7             =head1 AUTHOR
8              
9             Benjamin Bernard
10             Keith Amling
11              
12             =head1 DESCRIPTION
13              
14             This class knows out to look up a keyspec in a datastructure
15              
16             =head1 SYNOPSIS
17              
18             use App::RecordStream::KeySpec;
19             my $data_ref = App::RecordStream::KeySpec::find_key($r, 'Foo/Bar');
20              
21             =cut
22              
23             our $VERSION = "4.0.23";
24              
25 72     72   434 use strict;
  72         174  
  72         1822  
26 72     72   340 use warnings;
  72         141  
  72         1613  
27              
28 72     72   473 use App::RecordStream::KeySpec;
  72         166  
  72         1549  
29              
30 72     72   23621 use Data::Dumper;
  72         236256  
  72         86863  
31              
32             my $registry = {};
33              
34             sub find_key {
35 4225     4225 0 8455 my ($data, $spec, $no_vivify, $throw_error) = @_;
36              
37 4225         9421 my $spec_obj = __PACKAGE__->new($spec);
38 4225         9155 return $spec_obj->guess_key($data, $no_vivify, $throw_error);
39             }
40              
41             sub new
42             {
43 8981     8981 0 13720 my $class = shift;
44 8981         13451 my $spec = shift;
45              
46 8981 100       19646 if ( exists $registry->{$spec} ) {
47 8768         18200 return $registry->{$spec};
48             }
49              
50 213         661 my $this = {
51             SPEC => $spec,
52             };
53              
54 213         514 bless $this, $class;
55 213         775 $this->init();
56              
57 213         531 $registry->{$spec} = $this;
58 213         504 return $this;
59             }
60              
61             sub init
62             {
63 213     213 0 404 my $this = shift;
64 213         683 $this->_parse_key_spec();
65             }
66              
67             {
68             my $guessed_keys = {};
69              
70             sub _search_string_to_key {
71 40     40   78 my $key_chain = shift;
72 40         77 my $string = shift;
73              
74 40         188 return $guessed_keys->{join('-', @$key_chain)}->{$string};
75             }
76              
77             sub _add_string_key_mapping {
78 34     34   62 my $key_chain = shift;
79 34         78 my $string = shift;
80 34         71 my $key = shift;
81              
82 34         119 $guessed_keys->{join('-', @$key_chain)}->{$string} = $key;
83             }
84             }
85              
86             sub _guess_key_name_raw {
87 2311     2311   4287 my ($this, $data, $key_chain, $search_string) = @_;
88              
89 2311         3719 my $fuzzy = $this->{'FUZZY'};
90              
91 2311 100       7475 if ( UNIVERSAL::isa($data, 'ARRAY') ) {
92 14 50       94 if ( $search_string =~ m/^#(\d+)$/ ) {
93 14         63 return $1;
94             }
95             else {
96 0         0 die "Cannot select non-numeric index: $search_string (did you forget to prefix with a '#'?) for array: " . Dumper($data);
97             }
98             }
99              
100 2297 100       6537 return $search_string if ( ! $fuzzy );
101              
102 40         83 my $found_key;
103              
104 40 100       109 if ( my $key = _search_string_to_key($key_chain, $search_string) ) {
105 6         19 return $key;
106             }
107              
108             # First check exact match
109 34 100       132 if ( defined $data->{$search_string} ) {
110 9         39 $found_key = $search_string;
111             }
112             else {
113             # Next check prefixes, no interpolation
114 25         160 foreach my $key ( CORE::sort(CORE::keys %$data) ) {
115 69 100       554 if ( $key =~ m/^\Q$search_string\E/i ) {
116 8         21 $found_key = $key;
117             }
118             }
119             }
120              
121 34 100       110 if ( !$found_key ) {
122             # Check for match anywhere in the keys, allow regex interpolation
123 18         59 foreach my $key ( CORE::sort(CORE::keys %$data) ) {
124 44 100       260 if ( $key =~ m/$search_string/i ) {
125 5         13 $found_key = $key;
126             }
127             }
128             }
129              
130 34 100       101 if ( !$found_key ) {
131 14         31 $found_key = $search_string;
132             }
133              
134 34         114 _add_string_key_mapping($key_chain, $search_string, $found_key);
135              
136 34         82 return $found_key
137             }
138              
139             sub has_key_spec {
140 2697     2697 0 4437 my ($this, $data) = @_;
141 2697         4212 eval { $this->guess_key($data, 0, 1) };
  2697         5090  
142              
143 2697 100       7996 if ( $@ =~ m/^NoSuchKey/ ) {
    50          
144 1         7 return 0;
145             }
146             elsif ( $@ ) {
147             #Rethrow if a different error
148 0         0 die $@;
149             }
150              
151 2696         7880 return 1;
152             }
153              
154             sub get_key_list_for_spec {
155 2058     2058 0 3284 my ($this, $data) = @_;
156              
157             return $this->_guess_key_recurse(
158             $data,
159             [],
160             1,
161             0,
162             1,
163 2058         3573 @{$this->{'PARSED_KEYS'}},
  2058         4101  
164             );
165             }
166              
167             sub _parse_key_spec {
168 213     213   491 my ($this) = @_;
169              
170 213         585 my $spec = $this->{'SPEC'};
171 213         421 my $fuzzy = 0;
172 213         420 my $spec_name = $spec;
173              
174 213 100       871 if ( substr($spec, 0, 1) eq '@' ) {
175 37         76 $fuzzy = 1;
176 37         103 $spec = substr($spec, 1);
177             }
178              
179 213         449 my $keys = [];
180 213         470 my $current_key = '';
181 213         412 my $last_char = '';
182              
183 213         760 for (my $index = 0; $index < length($spec); $index++) {
184 820         1713 my $current_char = substr($spec, $index, 1);
185              
186 820 100 100     2479 if ( $current_char eq '/' && $last_char ne '\\' ) {
187 30         92 push @$keys, $current_key;
188 30         64 $current_key = '';
189 30         57 $last_char = '';
190 30         87 next;
191             }
192             else {
193 790 100       1811 if ( $current_char eq '/' ) {
194 1         4 chop $current_key;
195             }
196              
197 790         1398 $current_key .= $current_char;
198 790         1254 $last_char = $current_char;
199 790         2173 next;
200             }
201             }
202              
203 213 50       598 if ( $current_key ne '' ) {
204 213         546 push @$keys, $current_key;
205             }
206              
207 213         502 $this->{'PARSED_KEYS'} = $keys;
208 213         605 $this->{'FUZZY'} = $fuzzy;
209             }
210              
211             {
212             my $keylookup_hash = {};
213              
214             sub guess_key {
215 6923     6923 0 12237 my ($this, $data, $no_vivify, $throw_error) = @_;
216              
217 6923         10080 my @args = @{$this->{'PARSED_KEYS'}};
  6923         15877  
218              
219 6923   50     29109 $no_vivify ||= 0;
220 6923   100     20892 $throw_error ||= 0;
221 6923         14459 my $args_string = join('-', @args, $no_vivify, $throw_error);
222              
223 6923 100       16249 if ( my $code = $keylookup_hash->{$args_string} ) {
224 6699         131333 return $code->($data);
225             }
226              
227 224         811 my $keys = $this->_guess_key_recurse(
228             $data,
229             [],
230             $no_vivify,
231             $throw_error,
232             1,
233             @args,
234             );
235              
236 222         702 my $code = $this->_generate_keylookup_sub($keys, $no_vivify);
237 222         635 $keylookup_hash->{$args_string} = $code;
238              
239 222         4629 return $code->($data);
240             }
241             }
242              
243             # Performance! Oh god, performance. Generate a lookup subroutine that will
244             # lookup the passed keys, for execution later
245             sub _generate_keylookup_sub {
246 222     222   445 my $this = shift;
247 222         384 my $keys = shift;
248 222         398 my $no_vivify = shift;
249 222         381 my $throw_error = shift;
250              
251 222 50       620 if ( scalar @$keys == 0 ) {
252 0         0 return eval 'sub { if ( \$throw_error ) { die "NoSuchKey"; } return ""; }';
253             }
254              
255 222         454 my $code_string = 'sub { my $record = shift;';
256              
257 222         386 my $key_accessor = '$record';
258              
259 222         392 my $action = "return ''";
260 222 50       578 $action = "die 'NoSuchKey'" if ( $throw_error );
261              
262 222         429 my $check_actions = '';
263              
264 222         584 foreach my $key (@$keys) {
265 246 100       713 if ( $key =~ m/^#(\d+)$/ ) {
266 13         42 my $index = $1;
267 13         35 $key_accessor .= "->[$index]";
268             }
269             else {
270 233         1071 my @hex_bytes = unpack('C*', $key);
271 233         489 my $hex_string = '';
272              
273 233         471 foreach my $byte (@hex_bytes) {
274 818         2184 $hex_string .= "\\x" . sprintf ("%lx", $byte);
275             }
276              
277 233         738 $key_accessor .= "->{\"$hex_string\"}";
278             }
279              
280 246         698 $check_actions .= "$action if ( ! exists $key_accessor );";
281             }
282              
283 222 50 33     1109 if ( $no_vivify || $throw_error ) {
284 0         0 $code_string .= $check_actions;
285             }
286              
287 222         555 $code_string .= "return \\($key_accessor)}";
288              
289 222         20523 my $sub_ref = eval $code_string;
290 222 50       789 if ( $@ ) {
291 0         0 warn "Unexpected error in creating key lookup!\n";
292 0         0 die $@;
293             }
294 222         545 return $sub_ref;
295             }
296              
297             sub _guess_key_recurse {
298 2311     2311   4774 my ($this, $data, $key_chain, $no_vivify, $throw_error,
299             $return_key_chain, $search_string, @next_strings) = @_;
300              
301 2311         4098 my $type = ref($data);
302              
303 2311 50 33     10659 if ( $type eq 'SCALAR' || UNIVERSAL::isa(\$data, 'SCALAR') ) {
304 0         0 die "Cannot look for $search_string in scalar: " . Dumper($data);
305             }
306              
307 2311         4979 my $key = $this->_guess_key_name_raw($data, $key_chain, $search_string);
308              
309 2311         3628 my $value;
310              
311 2311 100       4557 if ( $type eq 'ARRAY' ) {
312 14         59 $value = \($data->[$key]);
313 14         44 $key = "#$key";
314             }
315             else {
316 2297 100 100     8156 if ( $no_vivify && (!exists $data->{$key}) ) {
317 3 50       18 return $return_key_chain ? [] : '';
318             }
319              
320 2294         3939 $value = \($data->{$key})
321             }
322              
323 2308 100       5144 if ( scalar @next_strings > 0 ) {
324 31 100       157 if ( ! defined $$value ) {
325 9 100       46 die "NoSuchKey" if ( $throw_error );
326              
327 7 50       21 if ( $no_vivify ) {
328 0 0       0 return $return_key_chain ? [] : '';
329             }
330              
331 7 100       25 if ( substr($next_strings[0], 0, 1) eq '#' ) {
332 3         9 $$value = [];
333             }
334             else {
335 4         10 $$value = {};
336             }
337             }
338              
339 29         173 return $this->_guess_key_recurse(
340             $$value,
341             [@$key_chain, $key],
342             $no_vivify,
343             $throw_error,
344             $return_key_chain,
345             @next_strings,
346             );
347             }
348              
349 2277 50       9719 return $return_key_chain ? [@$key_chain, $key] : $value;
350             }
351              
352             sub keyspec_help {
353 0     0 0   return <
354             KEY SPECS
355             __FORMAT_TEXT__
356             A key spec is short way of specifying a field with prefixes or regular
357             expressions, it may also be nested into hashes and arrays. Use a '/' to nest
358             into a hash and a '#NUM' to index into an array (i.e. #2)
359              
360             An example is in order, take a record like this:
361             __FORMAT_TEXT__
362              
363             {"biz":["a","b","c"],"foo":{"bar 1":1},"zap":"blah1"}
364             {"biz":["a","b","c"],"foo":{"bar 1":2},"zap":"blah2"}
365             {"biz":["a","b","c"],"foo":{"bar 1":3},"zap":"blah3"}
366              
367             __FORMAT_TEXT__
368             In this case a key spec of 'foo/bar 1' would have the values 1,2, and 3
369             in the respective records.
370              
371             Similarly, 'biz/#0' would have the value of 'a' for all 3 records
372              
373             You can also prefix key specs with '\@' to engage the fuzzy matching logic
374             __FORMAT_TEXT__
375              
376             __FORMAT_TEXT__
377             Fuzzy matching works like this in order, first key to match wins
378             __FORMAT_TEXT__
379             1. Exact match ( eq )
380             2. Prefix match ( m/^/ )
381             3. Match anywehre in the key (m//)
382              
383             __FORMAT_TEXT__
384             So, in the above example '\@b/#2', the 'b' portion would expand to 'biz' and 2
385             would be the index into the array, so all records would have the value of 'c'
386              
387             Simiarly, \@f/b would have values 1, 2, and 3
388              
389             You can escape / with a \\. For example, if you have a record:
390             __FORMAT_TEXT__
391             {"foo/bar":2}
392              
393             __FORMAT_TEXT__
394             You can address that key with foo\\/bar
395             __FORMAT_TEXT__
396             KEYSPECS_HELP
397             }
398              
399             1;