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   65617 use strict;
  4         18  
  4         189  
4 4     4   27 use warnings FATAL => 'all';
  4         9  
  4         173  
5 4     4   451 use parent 'App::NDTools::NDProc::Module';
  4         314  
  4         26  
6              
7 4     4   2380 use Hash::Merge qw();
  4         39293  
  4         113  
8 4     4   1816 use Hash::Merge::Extra 0.04;
  4         7872  
  4         24  
9 4     4   3663 use List::MoreUtils qw(before);
  4         49914  
  4         28  
10 4     4   4124 use Log::Log4Cli;
  4         9  
  4         387  
11 4     4   26 use Storable qw(dclone);
  4         9  
  4         215  
12 4     4   28 use Struct::Path 0.80 qw(implicit_step path);
  4         97  
  4         253  
13 4     4   30 use Struct::Path::PerlStyle 0.80 qw(str2path path2str);
  4         64  
  4         257  
14              
15 4     4   31 use App::NDTools::Slurp qw(s_decode s_encode);
  4         16  
  4         7375  
16              
17             our $VERSION = '0.20';
18              
19 1     1 0 7 sub MODINFO { "Merge structures according provided rules" }
20              
21             sub arg_opts {
22 24     24 0 61 my $self = shift;
23              
24 24         137 my %opts = $self->SUPER::arg_opts();
25 24         82 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   3766 if ($self->{rules} and @{$self->{rules}}) {
  20         100  
32 19         38 push @{$self->{rules}->[-1]->{path}}, { merge => $_[1] };
  19         116  
33             } else {
34 1         3 push @{$self->{OPTS}->{path}}, { merge => $_[1] };
  1         5  
35             }
36             },
37             'source=s' => sub {
38 21     21   16936 push @{$self->{rules}}, { source => $_[1] };
  21         133  
39             },
40             'strict!' => sub {
41 5     5   711 $self->set_path_related_opt($_[0], $_[1]),
42             },
43             'structure=s' => sub {
44 1     1   862 push @{$self->{rules}}, { structure => $_[1] };
  1         7  
45             },
46             'style=s' => sub {
47 5     5   705 $self->set_path_related_opt($_[0], $_[1])
48             },
49             )
50 24         434 }
51              
52             sub configure {
53 70     70 0 124 my $self = shift;
54              
55 70 100       222 $self->{rules} = [] unless ($self->{rules});
56              
57             # resolve rules
58 70         124 for my $rule (@{$self->{rules}}) {
  70         225  
59              
60             # merge with global wide opts
61 22         963 my $globals = dclone($self->{OPTS});
62 2         7 unshift @{$rule->{path}}, @{delete $globals->{path}}
  2         6  
63 22 100 66     124 if ($globals->{path} and @{$globals->{path}});
  22         128  
64 22         49 $rule = { %{$globals}, %{$rule} };
  22         88  
  22         124  
65              
66             # path as simple string if no no specific opts defined
67 21 100 66     89 map { $_ = $_->{merge} if (exists $_->{merge} and keys %{$_} == 1) }
  21         161  
68 22         71 @{$rule->{path}};
  22         90  
69              
70             $rule->{structure} = s_decode($rule->{structure}, 'JSON')
71 22 100       134 if (exists $rule->{structure});
72             }
73             }
74              
75             sub defaults {
76 49     49 0 101 my $self = shift;
77              
78             return {
79 49         97 %{$self->SUPER::defaults()},
  49         167  
80             'strict' => 1,
81             'style' => 'R_OVERRIDE',
82             };
83             }
84              
85             sub get_opts {
86 21     21 0 37 return @{$_[0]->{rules}};
  21         84  
87             }
88              
89             sub map_paths {
90 24     24 0 1922 my ($data, $srcs, $spath) = @_;
91              
92 24     18   114 my @explicit = before { implicit_step($_) } @{$spath};
  18         98  
  24         153  
93 17         52 return path(${$data}, $spath, paths => 1, expand => 1)
94 24 100       294 if (@explicit == @{$spath}); # fully qualified path
  24         77  
95              
96 7         14 my @out;
97 7         17 my @dsts = path(${$data}, $spath, paths => 1);
  7         24  
98              
99 7         9805 $srcs = [ @{$srcs} ];
  7         21  
100 7         16 while (@{$srcs}) {
  14         512  
101 7         15 my ($sp, $sr) = splice @{$srcs}, 0, 2;
  7         21  
102              
103 7 100       23 if (@dsts) { # destination struct may match - use this paths beforehand
104 3         8 push @out, splice @dsts, 0, 2;
105 3         7 next;
106             }
107              
108 4         10 my @e_path = @{$spath};
  4         14  
109 4         19 while (my $step = pop @e_path) {
110 18 100 66     262 if (ref $step eq 'ARRAY' and implicit_step($step)) {
    100 100        
111 5 100       43 if (my @tmp = path(${$data}, \@e_path, deref => 1, paths => 1)) {
  5         21  
112             # expand last existed array, addressed by implicit step
113 4         443 @e_path = ( @{$tmp[0]}, [ scalar @{$tmp[1]} ] );
  4         10  
  4         11  
114 4         13 last;
115             }
116             } elsif (ref $step eq 'HASH' and implicit_step($step)) {
117 1 50       16 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       16 @e_path = @{$sp}[0 .. $#explicit] unless (@e_path);
  0         0  
125 4         11 my @i_path = @{$sp}[@e_path .. $#{$sp}];
  4         14  
  4         24  
126              
127 4 100       11 map { $_ = [0] if (ref $_ eq 'ARRAY') } @i_path; # drop array's indexes in implicit part of path
  2         12  
128 4         9 push @out, path(${$data}, [@e_path, @i_path], paths => 1, expand => 1);
  4         16  
129             }
130              
131 7         27 return @out;
132             }
133              
134             sub check_rule {
135 24     24 0 71 my ($self, $rule) = @_;
136              
137             # merge full source if no paths defined
138 9         28 push @{$rule->{path}}, {}
139 24 100 66     83 unless ($rule->{path} and @{$rule->{path}});
  24         125  
140             # convert to canonical structure
141 27 100       152 map { $_ = { merge => $_ }
142 24         45 unless (ref $_) } @{$rule->{path}};
  24         76  
143              
144 24         87 return $self;
145             }
146              
147             sub process {
148 24     24 0 73 my ($self, $data, $opts, $source) = @_;
149              
150 24 100       103 if (exists $opts->{ignore}) {
151 21         41 for my $path (@{$opts->{ignore}}) {
  21         68  
152 1     0   7 log_debug { "Removing (ignore) from src '$path'" };
  0         0  
153 1         11 path($source, str2path($path), delete => 1);
154             }
155             }
156              
157 24         1231 $self->SUPER::process($data, $opts, $source);
158             }
159              
160             sub process_path {
161 27     27 0 77 my ($self, $data, $path, undef, $opts, $source) = @_;
162              
163             # merge whole source if path omitted
164 27 100       87 $path->{merge} = '' unless (defined $path->{merge});
165              
166 27 100       81 if (exists $opts->{structure}) {
167 1         7 $opts->{source} = s_encode($opts->{structure}, 'JSON', {pretty => 0});
168             $source = $opts->{structure}
169 1         3 }
170              
171 27         54 my $spath = eval { str2path($path->{merge}) };
  27         106  
172 27 50       20590 die_fatal "Failed to parse path ($@)", 4 if ($@);
173              
174 27     0   177 log_debug { "Resolving paths '$path->{merge}'" };
  0         0  
175 27         218 my @srcs = path($source, $spath, paths => 1);
176 27 100       16917 unless (@srcs) {
177             die_fatal "No such path '$path->{merge}' in $opts->{source}", 4
178 3 100       39 if (exists $path->{strict} ? $path->{strict} : $opts->{strict});
    100          
179 1     0   9 log_info { "Ignore path '$path->{merge}' (absent in $opts->{source})" };
  0         0  
180 1         10 return $self;
181             }
182 24         81 my @dsts = map_paths($data, \@srcs, $spath);
183              
184 24   33     882 my $style = $path->{style} || $opts->{style} || $self->{OPTS}->{style};
185 24         74 while (@srcs) {
186 24         72 my ($sp, $sr) = splice @srcs, 0, 2;
187 24         81 my ($dp, $dr) = splice @dsts, 0, 2;
188 0     0   0 log_info { "Merging $opts->{source} ($style, '" .
189 24         143 path2str($sp) . "' => '" . path2str($dp) . "')" };
190 24         236 Hash::Merge::set_behavior($style);
191 24         1400 ${$dr} = Hash::Merge::merge(${$dr}, ${$sr});
  24         3707  
  24         72  
  24         79  
192             }
193             }
194              
195             sub set_path_related_opt {
196 10     10 0 29 my ($self, $name, $val) = @_;
197              
198 10 50 33     38 if ($self->{rules} and @{$self->{rules}}) {
  10         37  
199 10 100 66     51 if (
200             exists $self->{rules}->[-1]->{path} and
201 5         18 @{$self->{rules}->[-1]->{path}}
202             ) {
203 5         17 $self->{rules}->[-1]->{path}->[-1]->{$name} = $val; # per path
204             } else {
205 5         73 $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__