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