File Coverage

blib/lib/Data/Transmute.pm
Criterion Covered Total %
statement 180 181 99.4
branch 143 150 95.3
condition 28 37 75.6
subroutine 25 25 100.0
pod 2 2 100.0
total 378 395 95.7


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