File Coverage

blib/lib/App/NDTools/NDProc/Module/Merge.pm
Criterion Covered Total %
statement 37 157 23.5
branch 1 44 2.2
condition 0 27 0.0
subroutine 12 29 41.3
pod 0 10 0.0
total 50 267 18.7


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