File Coverage

blib/lib/App/NDTools/NDProc/Module/Merge.pm
Criterion Covered Total %
statement 159 168 94.6
branch 44 48 91.6
condition 17 27 62.9
subroutine 27 31 87.1
pod 0 10 0.0
total 247 284 86.9


line stmt bran cond sub pod time code
1             package App::NDTools::NDProc::Module::Merge;
2              
3 4     4   66301 use strict;
  4         32  
  4         166  
4 4     4   25 use warnings FATAL => 'all';
  4         9  
  4         181  
5 4     4   447 use parent 'App::NDTools::NDProc::Module';
  4         305  
  4         30  
6              
7 4     4   2361 use Hash::Merge qw();
  4         39021  
  4         117  
8 4     4   1810 use Hash::Merge::Extra 0.04;
  4         7919  
  4         25  
9 4     4   3692 use List::MoreUtils qw(before);
  4         50014  
  4         27  
10 4     4   4222 use Log::Log4Cli;
  4         12  
  4         376  
11 4     4   27 use Storable qw(dclone);
  4         9  
  4         237  
12 4     4   29 use Struct::Path 0.80 qw(implicit_step path);
  4         82  
  4         247  
13 4     4   31 use Struct::Path::PerlStyle 0.80 qw(str2path path2str);
  4         64  
  4         214  
14              
15 4     4   28 use App::NDTools::Slurp qw(s_decode s_encode);
  4         18  
  4         7293  
16              
17             our $VERSION = '0.19';
18              
19 1     1 0 6 sub MODINFO { "Merge structures according provided rules" }
20              
21             sub arg_opts {
22 23     23 0 43 my $self = shift;
23              
24 23         81 my %opts = $self->SUPER::arg_opts();
25 23         67 delete $opts{'path=s@'};
26              
27             return (
28             %opts,
29             'ignore=s@' => \$self->{OPTS}->{ignore},
30             'merge|path=s' => sub {
31 20 100 66 20   3759 if ($self->{rules} and @{$self->{rules}}) {
  20         79  
32 19         33 push @{$self->{rules}->[-1]->{path}}, { merge => $_[1] };
  19         94  
33             } else {
34 1         2 push @{$self->{OPTS}->{path}}, { merge => $_[1] };
  1         6  
35             }
36             },
37             'source=s' => sub {
38 21     21   15120 push @{$self->{rules}}, { source => $_[1] };
  21         117  
39             },
40             'strict!' => sub {
41 5     5   727 $self->set_path_related_opt($_[0], $_[1]),
42             },
43             'structure=s' => sub {
44 1     1   836 push @{$self->{rules}}, { structure => $_[1] };
  1         5  
45             },
46             'style=s' => sub {
47 5     5   717 $self->set_path_related_opt($_[0], $_[1])
48             },
49             )
50 23         302 }
51              
52             sub configure {
53 69     69 0 135 my $self = shift;
54              
55 69 100       228 $self->{rules} = [] unless ($self->{rules});
56              
57             # resolve rules
58 69         103 for my $rule (@{$self->{rules}}) {
  69         214  
59              
60             # merge with global wide opts
61 22         916 my $globals = dclone($self->{OPTS});
62 2         4 unshift @{$rule->{path}}, @{delete $globals->{path}}
  2         6  
63 22 100 66     113 if ($globals->{path} and @{$globals->{path}});
  22         104  
64 22         40 $rule = { %{$globals}, %{$rule} };
  22         82  
  22         114  
65              
66             # path as simple string if no no specific opts defined
67 21 100 66     63 map { $_ = $_->{merge} if (exists $_->{merge} and keys %{$_} == 1) }
  21         123  
68 22         80 @{$rule->{path}};
  22         62  
69              
70             $rule->{structure} = s_decode($rule->{structure}, 'JSON')
71 22 100       121 if (exists $rule->{structure});
72             }
73             }
74              
75             sub defaults {
76 48     48 0 90 my $self = shift;
77              
78             return {
79 48         116 %{$self->SUPER::defaults()},
  48         161  
80             'strict' => 1,
81             'style' => 'R_OVERRIDE',
82             };
83             }
84              
85             sub get_opts {
86 21     21 0 44 return @{$_[0]->{rules}};
  21         70  
87             }
88              
89             sub map_paths {
90 24     24 0 53 my ($data, $srcs, $spath) = @_;
91              
92 24     18   91 my @explicit = before { implicit_step($_) } @{$spath};
  18         92  
  24         109  
93 17         50 return path(${$data}, $spath, paths => 1, expand => 1)
94 24 100       204 if (@explicit == @{$spath}); # fully qualified path
  24         78  
95              
96 7         16 my @out;
97 7         15 my @dsts = path(${$data}, $spath, paths => 1);
  7         23  
98              
99 7         9906 $srcs = [ @{$srcs} ];
  7         18  
100 7         16 while (@{$srcs}) {
  14         490  
101 7         14 my ($sp, $sr) = splice @{$srcs}, 0, 2;
  7         21  
102              
103 7 100       21 if (@dsts) { # destination struct may match - use this paths beforehand
104 3         10 push @out, splice @dsts, 0, 2;
105 3         6 next;
106             }
107              
108 4         6 my @e_path = @{$spath};
  4         14  
109 4         19 while (my $step = pop @e_path) {
110 18 100 66     232 if (ref $step eq 'ARRAY' and implicit_step($step)) {
    100 100        
111 5 100       38 if (my @tmp = path(${$data}, \@e_path, deref => 1, paths => 1)) {
  5         15  
112             # expand last existed array, addressed by implicit step
113 4         428 @e_path = ( @{$tmp[0]}, [ scalar @{$tmp[1]} ] );
  4         12  
  4         12  
114 4         10 last;
115             }
116             } elsif (ref $step eq 'HASH' and implicit_step($step)) {
117 1 50       11 if (my @tmp = path(${$data}, [ @e_path, $step ], paths => 1)) {
  1         5  
118 0         0 @e_path = @{$tmp[0]};
  0         0  
119 0         0 last;
120             }
121             }
122             }
123              
124 4 50       14 @e_path = @{$sp}[0 .. $#explicit] unless (@e_path);
  0         0  
125 4         11 my @i_path = @{$sp}[@e_path .. $#{$sp}];
  4         10  
  4         7  
126              
127 4 100       9 map { $_ = [0] if (ref $_ eq 'ARRAY') } @i_path; # drop array's indexes in implicit part of path
  2         11  
128 4         10 push @out, path(${$data}, [@e_path, @i_path], paths => 1, expand => 1);
  4         12  
129             }
130              
131 7         21 return @out;
132             }
133              
134             sub check_rule {
135 24     24 0 52 my ($self, $rule) = @_;
136              
137             # merge full source if no paths defined
138 9         24 push @{$rule->{path}}, {}
139 24 100 66     83 unless ($rule->{path} and @{$rule->{path}});
  24         82  
140             # convert to canonical structure
141 27 100       143 map { $_ = { merge => $_ }
142 24         46 unless (ref $_) } @{$rule->{path}};
  24         62  
143              
144 24         84 return $self;
145             }
146              
147             sub process {
148 24     24 0 70 my ($self, $data, $opts, $source) = @_;
149              
150 24 100       58 if (exists $opts->{ignore}) {
151 21         39 for my $path (@{$opts->{ignore}}) {
  21         59  
152 1     0   7 log_debug { "Removing (ignore) from src '$path'" };
  0         0  
153 1         9 path($source, str2path($path), delete => 1);
154             }
155             }
156              
157 24         1207 $self->SUPER::process($data, $opts, $source);
158             }
159              
160             sub process_path {
161 27     27 0 70 my ($self, $data, $path, undef, $opts, $source) = @_;
162              
163             # merge whole source if path omitted
164 27 100       85 $path->{merge} = '' unless (defined $path->{merge});
165              
166 27 100       58 if (exists $opts->{structure}) {
167 1         7 $opts->{source} = s_encode($opts->{structure}, 'JSON', {pretty => 0});
168             $source = $opts->{structure}
169 1         4 }
170              
171 27         48 my $spath = eval { str2path($path->{merge}) };
  27         98  
172 27 50       20242 die_fatal "Failed to parse path ($@)", 4 if ($@);
173              
174 27     0   179 log_debug { "Resolving paths '$path->{merge}'" };
  0         0  
175 27         191 my @srcs = path($source, $spath, paths => 1);
176 27 100       16763 unless (@srcs) {
177             die_fatal "No such path '$path->{merge}' in $opts->{source}", 4
178 3 100       34 if (exists $path->{strict} ? $path->{strict} : $opts->{strict});
    100          
179 1     0   7 log_info { "Ignore path '$path->{merge}' (absent in $opts->{source})" };
  0         0  
180 1         9 return $self;
181             }
182 24         110 my @dsts = map_paths($data, \@srcs, $spath);
183              
184 24   33     871 my $style = $path->{style} || $opts->{style} || $self->{OPTS}->{style};
185 24         72 while (@srcs) {
186 24         61 my ($sp, $sr) = splice @srcs, 0, 2;
187 24         64 my ($dp, $dr) = splice @dsts, 0, 2;
188 0     0   0 log_info { "Merging $opts->{source} ($style, '" .
189 24         142 path2str($sp) . "' => '" . path2str($dp) . "')" };
190 24         217 Hash::Merge::set_behavior($style);
191 24         1287 ${$dr} = Hash::Merge::merge(${$dr}, ${$sr});
  24         3504  
  24         49  
  24         77  
192             }
193             }
194              
195             sub set_path_related_opt {
196 10     10 0 30 my ($self, $name, $val) = @_;
197              
198 10 50 33     40 if ($self->{rules} and @{$self->{rules}}) {
  10         38  
199 10 100 66     35 if (
200             exists $self->{rules}->[-1]->{path} and
201 5         14 @{$self->{rules}->[-1]->{path}}
202             ) {
203 5         19 $self->{rules}->[-1]->{path}->[-1]->{$name} = $val; # per path
204             } else {
205 5         71 $self->{rules}->[-1]->{$name} = $val; # per rule
206             }
207             } else {
208 0           $self->{OPTS}->{$name} = $val; # global (whole ruleset wide)
209             }
210             }
211              
212             1; # End of App::NDTools::NDProc::Module::Merge
213              
214             __END__