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