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