File Coverage

blib/lib/App/NDTools/NDProc/Module/Merge.pm
Criterion Covered Total %
statement 150 159 94.3
branch 40 44 90.9
condition 17 27 62.9
subroutine 25 29 86.2
pod 0 10 0.0
total 232 269 86.2


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