File Coverage

blib/lib/App/RecordStream/Operation/flatten.pm
Criterion Covered Total %
statement 64 75 85.3
branch 8 12 66.6
condition 6 6 100.0
subroutine 10 11 90.9
pod 0 6 0.0
total 88 110 80.0


line stmt bran cond sub pod time code
1             package App::RecordStream::Operation::flatten;
2              
3             our $VERSION = "4.0.25";
4              
5 2     2   896 use strict;
  2         4  
  2         45  
6              
7 2     2   8 use base qw(App::RecordStream::Operation);
  2         4  
  2         1338  
8              
9             my $INVALID_REF_TYPES = [qw(
10             SCALAR
11             ARRAY
12             CODE
13             REF
14             GLOB
15             LVALUE
16             FORMAT
17             IO
18             VSTRING
19             Regexp
20             )];
21              
22             sub init {
23 3     3 0 4 my $this = shift;
24 3         4 my $args = shift;
25              
26              
27 3         4 my @fields;
28 3         3 my $default_depth = 1;
29 3         4 my $separator = '-';
30              
31              
32             my $add_field = sub {
33 3     3   6 my ($depth, $field_names) = @_;
34              
35 3         13 my $key_groups = App::RecordStream::KeyGroups->new();
36 3         9 $key_groups->add_groups($field_names);
37              
38 3         10 push @fields, [$depth, $key_groups];
39 3         10 };
40              
41             my $spec = {
42 27         65 (map { ($_ . "=s") => $add_field } (1..9)),
43             "depth=i" => \$default_depth,
44 2     2   2008 "key|k|field|f=s" => sub { $add_field->($default_depth, $_[1]); },
45 1     1   1067 "deep=s" => sub { $add_field->(-1, $_[1]); },
46 3         6 "separator=s" => \$separator,
47             };
48              
49 3         15 $this->parse_options($args, $spec);
50              
51 3         6 $this->{'FIELDS'} = \@fields;
52 3         5 $this->{'SEPARATOR'} = $separator;
53 3         29 $this->{'DEFAULT_DEPTH'} = $default_depth;
54             }
55              
56             sub accept_record {
57 9     9 0 10 my $this = shift;
58 9         9 my $record = shift;
59              
60 9         13 my $fields = $this->{'FIELDS'};
61 9         11 my $separator = $this->{'SEPARATOR'};
62              
63 9         14 foreach my $pair (@$fields) {
64 9         12 my ($depth, $key_groups) = @$pair;
65 9         11 foreach my $spec (@{$key_groups->get_keyspecs($record)}) {
  9         16  
66 9         12 eval {
67 9         14 my $value = $this->remove_spec($record, $spec);
68 9         17 $this->split_field($record, $spec, $depth, $value);
69             };
70              
71 9 50       27 if ( $@ =~ m/Cannot flatten into/ ) {
    50          
72 0         0 warn $@;
73 0         0 undef $@;
74 0         0 next;
75             }
76             elsif ( $@ ) {
77 0         0 die $@;
78             }
79             }
80             }
81              
82 9         50 $this->push_record($record);
83              
84 9         27 return 1;
85             }
86              
87             sub remove_spec {
88 9     9 0 15 my ($this, $record, $spec) = @_;
89 9         15 my $key_list = $record->get_key_list_for_spec($spec);
90              
91 9         13 my $last_key = pop @$key_list;
92 9         14 my $new_spec = join('/', @$key_list);
93              
94 9         10 my $data = $record;
95 9 50       16 if ($new_spec) {
96 0         0 $data = ${$record->guess_key_from_spec($new_spec, 1)};
  0         0  
97             }
98              
99 9         13 my $ref_type = ref($data);
100 9 50       13 if ( ! grep { $_ eq $ref_type } @$INVALID_REF_TYPES ) {
  90         109  
101 9         19 return delete $data->{$last_key};
102             }
103             else {
104 0         0 die "Cannot flatten into ref type: '$ref_type', must be a hash! skipping spec $spec!\n";
105             }
106             }
107              
108             sub split_field {
109 24     24 0 35 my ($this, $record, $name, $depth, $value) = @_;
110              
111 24         27 my $separator = $this->{'SEPARATOR'};
112              
113 24 100 100     55 if($depth != 0 && ref($value) eq "ARRAY") {
114 3         7 for(my $i = 0; $i < @$value; ++$i) {
115 6         16 $this->split_field($record, $name . $separator . $i, $depth - 1, $value->[$i]);
116             }
117 3         5 return;
118             }
119              
120 21 100 100     41 if($depth != 0 && ref($value) eq "HASH") {
121 9         18 for my $key (keys(%$value)) {
122 9         31 $this->split_field($record, $name . $separator . $key, $depth - 1, $value->{$key});
123             }
124 9         22 return;
125             }
126              
127             # either depth is 0 or it wasn't expandable anyway
128 12         13 ${$record->guess_key_from_spec($name)} = $value;
  12         20  
129             }
130              
131             sub add_help_types {
132 3     3 0 4 my $this = shift;
133 3         10 $this->use_help_type('keyspecs');
134 3         7 $this->use_help_type('keygroups');
135 3         5 $this->use_help_type('keys');
136             }
137              
138             sub usage {
139 0     0 0   my $this = shift;
140              
141 0           my $options = [
142             [ ' ', 'For this comma-separated list of fields flatten to depth n (1-9).'],
143             [ 'depth ', 'Change the default depth, negative being arbitrary depth (defaults to 1).'],
144             [ 'key ', 'For this comma-separated list of fields flatten to the default depth (may NOT be a a key spec).'],
145             [ 'deep ', 'For this comma-separated list of fields flatten to arbitrary depth.'],
146             [ 'separator ', 'Use this string to separate joined field names (defaults to "-").'],
147             ];
148              
149 0           my $args_string = $this->options_string($options);
150              
151 0           return <
152             Usage: recs-flatten []
153             __FORMAT_TEXT__
154             Flatten nested structures in records.
155              
156             NOTE: This script implements a strategy for dealing with nested structures
157             that is almost always better handled by using keyspecs or keygroups. It
158             should, in general, be as easy or easier to use those concepts with the data
159             manipulations you actually want to accomplish.
160             __FORMAT_TEXT__
161              
162             Arguments:
163             $args_string
164              
165             __FORMAT_TEXT__
166             All field values may be keyspecs or keygroups, value of keyspec must not be
167             an array element
168             __FORMAT_TEXT__
169              
170             Examples:
171             Under
172             recs-flatten -1 field
173             We see
174             {"field" => "value"} becomes {"field" => "value"}
175             {"field" => {"subfield" => "value"}} becomes {"field-subfield" => "value"}
176             {"field" => ["value1", "value2"]} becomes {"field-0" => "value1", "field-1" => "value2"}
177             {"field" => {"subfield" => [0, 1]}} becomes {"field-subfield" => [0, 1]}}
178             Under
179             recs-flatten --deep x
180             We see
181             {"x" => {"y" => [{"z" = "v"}]}} becomes {"x-y-0-z" => "v"}
182             USAGE
183             }
184              
185             1;