File Coverage

blib/lib/Data/Transmute.pm
Criterion Covered Total %
statement 177 178 99.4
branch 127 134 94.7
condition 17 25 68.0
subroutine 24 24 100.0
pod 2 2 100.0
total 347 363 95.5


line stmt bran cond sub pod time code
1             package Data::Transmute;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-02-13'; # DATE
5             our $DIST = 'Data-Transmute'; # DIST
6             our $VERSION = '0.037'; # VERSION
7              
8 1     1   66481 use 5.010001;
  1         12  
9 1     1   6 use strict 'subs', 'vars';
  1         2  
  1         37  
10 1     1   6 use warnings;
  1         2  
  1         25  
11 1     1   1613 use Log::ger;
  1         50  
  1         5  
12              
13 1     1   239 use Scalar::Util qw(refaddr);
  1         2  
  1         47  
14              
15 1     1   6 use Exporter qw(import);
  1         2  
  1         2273  
16             our @EXPORT_OK = qw(transmute_data reverse_rules);
17              
18             sub _rule_create_hash_key {
19 51     51   141 my %args = @_;
20              
21 51         75 my $data = $args{data};
22 51 100       136 return unless ref $data eq 'HASH';
23 38         58 my $name = $args{name};
24 38 100       72 die "Rule create_hash_key: Please specify 'name'" unless defined $name;
25              
26 37 100       110 if (exists $data->{$name}) {
27 7 100       21 return if $args{ignore};
28 6 100       32 die "Rule create_hash_key: Key '$name' already exists" unless $args{replace};
29             }
30             die "Rule create_hash_key: Please specify 'value' or 'value_code'"
31 35 100 100     93 unless exists $args{value} || $args{value_code};
32 34 100       164 $data->{$name} = $args{value_code} ? $args{value_code}->($data->{$name}) : $args{value};
33             }
34              
35             sub _rulereverse_create_hash_key {
36 24     24   60 my %args = @_;
37 24 100       63 die "Cannot generate reverse rule create_hash_key with value_code" if $args{value_code};
38 23 100       77 die "Cannot generate reverse rule create_hash_key with ignore=1" if $args{ignore};
39 22 100       51 die "Cannot generate reverse rule create_hash_key with replace=1" if $args{replace};
40 21         97 [delete_hash_key => {name=>$args{name}}];
41             }
42              
43             sub _rule_rename_hash_key {
44 21     21   90 my %args = @_;
45              
46 21         38 my $data = $args{data};
47 21 100       52 return unless ref $data eq 'HASH';
48 19         33 my $from = $args{from};
49 19 100       43 die "Rule rename_hash_key: Please specify 'from'" unless defined $from;
50 18         26 my $to = $args{to};
51 18 100       44 die "Rule rename_hash_key: Please specify 'to'" unless defined $to;
52              
53             # noop
54 17 50       37 return if $from eq $to;
55              
56 17 100       34 if (!exists($data->{$from})) {
57 2 100       16 die "Rule rename_hash_key: Can't rename '$from' -> '$to': Old key '$from' doesn't exist" unless $args{ignore_missing_from};
58 1         4 return;
59             }
60 15 100       33 if (exists $data->{$to}) {
61 3 100       10 return if $args{ignore_existing_target};
62 2 100       18 die "Rule rename_hash_key: Can't rename '$from' -> '$to': Target key '$from' already exists" unless $args{replace};
63             }
64 13         238 $data->{$to} = delete $data->{$from};
65             }
66              
67             sub _rulereverse_rename_hash_key {
68 10     10   29 my %args = @_;
69 10 100       30 die "Cannot generate reverse rule rename_hash_key with ignore_missing_from=1" if $args{ignore_missing_from};
70 9 100       28 die "Cannot generate reverse rule rename_hash_key with ignore_existing_target=1" if $args{ignore_existing_target};
71 8 100       21 die "Cannot generate reverse rule rename_hash_key with replace=1" if $args{replace};
72             [rename_hash_key => {
73             from=>$args{to}, to=>$args{from},
74 7         45 }];
75             }
76              
77             sub _rule_modify_hash_value {
78 9     9   734 require Data::Cmp;
79              
80 9         848 my %args = @_;
81              
82 9         19 my $data = $args{data};
83 9 100       28 return unless ref $data eq 'HASH';
84 7         14 my $name = $args{name};
85 7 100       25 die "Rule modify_hash_value: Please specify 'name' (key)" unless defined $name;
86 6         10 my $from = $args{from};
87 6         12 my $from_exists = exists $args{from};
88 6         10 my $to = $args{to};
89             die "Rule rename_hash_key: Please specify 'to' or 'to_code'"
90 6 50 66     19 unless exists $args{to} || $args{to_code};
91              
92             my $errprefix = "Rule modify_hash_value: Can't modify key '$name'".
93             ($from_exists ? " from '".($from // '') : "").
94 6 100 50     36 ($args{to_code} ? "' using to_code" : "' to '".($to // '')."'");
    100 50        
95              
96 6 100       15 unless (exists $data->{$name}) {
97 1         12 die "$errprefix: key does not exist";
98             }
99              
100 5         9 my $cur = $data->{$name};
101              
102 5 100       15 $to = $args{to_code}->($cur) if $args{to_code};
103              
104 5 100       13 if ($from_exists) {
105             # noop
106 4 50       12 return unless Data::Cmp::cmp_data($from, $to);
107              
108 4 100       86 if (Data::Cmp::cmp_data($cur, $from)) {
109 1   50     31 die "$errprefix: current value is not '".($cur // '')."'";
110             }
111             }
112              
113 4         63 $data->{$name} = $to;
114             }
115              
116             sub _rulereverse_modify_hash_value {
117 4     4   14 my %args = @_;
118 4 100       19 die "Cannot generate reverse rule modify_hash_value without from" unless exists $args{from};
119 3 100       15 die "Cannot generate reverse rule modify_hash_value with to_code" if $args{to_code};
120             [modify_hash_value => {
121             name => $args{name}, from => $args{to}, to => $args{from},
122 2         12 }];
123             }
124              
125             sub _rule_delete_hash_key {
126 33     33   75 my %args = @_;
127              
128 33         54 my $data = $args{data};
129 33 100       72 return unless ref $data eq 'HASH';
130 31         45 my $name = $args{name};
131 31 100       62 die "Rule delete_hash_key: Please specify 'name'" unless defined $name;
132              
133 30         84 delete $data->{$name};
134             }
135              
136             sub _rulereverse_delete_hash_key {
137 3     3   81 die "Can't create reverse rule for delete_hash_key";
138             }
139              
140             sub _rule_transmute_array_elems {
141 15     15   38 my %args = @_;
142              
143 15         25 my $data = $args{data};
144 15 100       47 return unless ref $data eq 'ARRAY';
145              
146             die "Rule transmute_array_elems: Please specify 'rules' or 'rules_module'"
147 13 100 100     47 unless defined($args{rules}) || defined($args{rules_module});
148              
149 12         27 my $idx = -1;
150             ELEM:
151 12         24 for my $el (@$data) {
152 32         42 $idx++;
153 32 100       61 if (defined $args{index_is}) {
154 6 100       24 next ELEM unless $idx == $args{index_is};
155             }
156 28 100       49 if (defined $args{index_in}) {
157 6 100       9 next ELEM unless grep { $idx == $_ } @{ $args{index_in} };
  12         35  
  6         12  
158             }
159 26 100       48 if (defined $args{index_match}) {
160 6 100       37 next ELEM unless $idx =~ $args{index_match};
161             }
162 24 100       44 if (defined $args{index_filter}) {
163 6 100       16 next ELEM unless $args{index_filter}->(index=>$idx, array=>$data, rules=>$args{rules});
164             }
165             $el = transmute_data(
166             data => $el,
167             (rules => $args{rules}) x !!(exists $args{rules}),
168 22         132 (rules_module => $args{rules_module}) x !!(exists $args{rules_module}),
169             );
170             }
171 12         39 $data;
172             }
173              
174             sub _rulereverse_transmute_array_elems {
175 7     7   19 my %args = @_;
176              
177             [transmute_array_elems => {
178             rules => reverse_rules(
179             (rules => $args{rules}) x !!(exists $args{rules}),
180             (rules_module => $args{rules_module}) x !!(exists $args{rules_module}),
181             ),
182             (index_is => $args{index_is}) x !!(exists $args{index_is}),
183             (index_in => $args{index_in}) x !!(exists $args{index_in}),
184             (index_match => $args{index_match}) x !!(exists $args{index_match}),
185 7         31 (index_filter => $args{index_filter}) x !!(exists $args{index_filter}),
186             }];
187             }
188              
189             sub _rule_transmute_hash_values {
190 15     15   39 my %args = @_;
191              
192 15         25 my $data = $args{data};
193 15 100       48 return unless ref $data eq 'HASH';
194              
195             die "Rule transmute_hash_values: Please specify 'rules' or 'rules_module'"
196 13 100 100     45 unless defined($args{rules}) || defined($args{rules_module});
197              
198             KEY:
199 12         31 for my $key (keys %$data) {
200 32 100       69 if (defined $args{key_is}) {
201 6 100       15 next KEY unless $key eq $args{key_is};
202             }
203 28 100       50 if (defined $args{key_in}) {
204 6 100       11 next KEY unless grep { $key eq $_ } @{ $args{key_in} };
  12         31  
  6         11  
205             }
206 26 100       76 if (defined $args{key_match}) {
207 6 100       31 next KEY unless $key =~ $args{key_match};
208             }
209 24 100       46 if (defined $args{key_filter}) {
210 6 100       15 next KEY unless $args{key_filter}->(key=>$key, hash=>$data, rules=>$args{rules});
211             }
212             $data->{$key} = transmute_data(
213             data => $data->{$key},
214             (rules => $args{rules}) x !!(exists $args{rules}),
215 22         102 (rules_module => $args{rules_module}) x !!(exists $args{rules_module}),
216             );
217             }
218 12         33 $data;
219             }
220              
221             sub _rulereverse_transmute_hash_values {
222 7     7   22 my %args = @_;
223              
224             [transmute_hash_values => {
225             rules => reverse_rules(
226             (rules => $args{rules}) x !!(exists $args{rules}),
227             (rules_module => $args{rules_module}) x !!(exists $args{rules_module}),
228             ),
229             (key_is => $args{key_is}) x !!(exists $args{key_is}),
230             (key_in => $args{key_in}) x !!(exists $args{key_in}),
231             (key_match => $args{key_match}) x !!(exists $args{key_match}),
232 7         53 (key_filter => $args{key_filter}) x !!(exists $args{key_filter}),
233             }];
234             }
235              
236             sub _walk {
237 16     16   31 my ($data, $rule_args, $seen) = @_;
238              
239             # transmute the node itself
240             transmute_data(
241             data => $data,
242             (rules => $rule_args->{rules}) x !!(exists $rule_args->{rules}),
243 16         53 (rules_module => $rule_args->{rules_module}) x !!(exists $rule_args->{rules_module}),
244             );
245 16 100       47 my $ref = ref($data) or return;
246 8         20 my $refaddr = refaddr($data);
247 8 50       25 return if $seen->{$refaddr}++;
248              
249 8 100       20 if ($ref eq 'ARRAY') {
    50          
250 4         9 for my $elem (@$data) {
251 3         8 _walk($elem, $rule_args, $seen);
252             }
253             } elsif ($ref eq 'HASH') {
254 4         18 for my $key (sort keys %$data) {
255 12         29 _walk($data->{$key}, $rule_args, $seen);
256             }
257             }
258             }
259              
260             sub _rule_transmute_nodes {
261 1     1   3 my %args = @_;
262              
263 1         3 my $data = $args{data};
264              
265             die "Rule transmute_nodes: Please specify 'rules' or 'rules_module'"
266 1 50 33     5 unless defined($args{rules}) || defined($args{rules_module});
267              
268 1         3 my $seen = {};
269 1         4 _walk($data, \%args, $seen);
270 1         4 $data;
271             }
272              
273             sub _rulereverse_transmute_nodes {
274 1     1   9 die "Rule transmute_nodes is not reversible";
275             }
276              
277             sub _rules_or_rules_module {
278 182     182   260 my $args = shift;
279              
280 182         292 my $rules = $args->{rules};
281 182 100       350 if (!$rules) {
282 7 100       21 if (defined $args->{rules_module}) {
283 6         23 my $mod = "Data::Transmute::Rules::$args->{rules_module}";
284 6         37 (my $mod_pm = "$mod.pm") =~ s!::!/!g;
285 6         582 require $mod_pm;
286 6         12 $rules = \@{"$mod\::RULES"};
  6         29  
287             }
288             }
289 182 100       324 $rules or die "Please specify rules (or rules_module)";
290 181         311 $rules;
291             }
292              
293             sub transmute_data {
294 134     134 1 210360 my %args = @_;
295              
296 134 100       347 exists $args{data} or die "Please specify data";
297 133         251 my $data = $args{data};
298 133         266 my $rules = _rules_or_rules_module(\%args);
299              
300 132         234 my $rulenum = 0;
301 132         241 for my $rule (@$rules) {
302 146         214 $rulenum++;
303 146 50       325 if ($ENV{LOG_DATA_TRANSMUTE_STEP}) {
304 0         0 log_trace "transmute_data #%d/%d: %s",
305             $rulenum, scalar(@$rules), $rule;
306             }
307 146         325 my $funcname = "_rule_$rule->[0]";
308             die "rule #$rulenum: Unknown function '$rule->[0]'"
309 146 100       204 unless defined &{$funcname};
  146         413  
310 145         195 my $func = \&{$funcname};
  145         295  
311             $func->(
312 145   50     218 %{$rule->[1] // {}},
  145         659  
313             data => $data,
314             );
315             }
316 118         321 $data;
317             }
318              
319             sub reverse_rules {
320 49     49 1 73757 my %args = @_;
321              
322 49         113 my $rules = _rules_or_rules_module(\%args);
323              
324 49         62 my @rev_rules;
325 49         95 for my $rule (@$rules) {
326 56         112 my $funcname = "_rulereverse_$rule->[0]";
327 56         79 my $func = \&{$funcname};
  56         150  
328             unshift @rev_rules, $func->(
329 56   50     89 %{$rule->[1] // {}},
  56         189  
330             );
331             }
332 37         164 \@rev_rules;
333             }
334              
335             1;
336             # ABSTRACT: Transmute (transform) data structure using rules data
337              
338             __END__