File Coverage

blib/lib/App/NDTools/NDProc.pm
Criterion Covered Total %
statement 63 217 29.0
branch 5 76 6.5
condition 1 12 8.3
subroutine 16 43 37.2
pod 0 16 0.0
total 85 364 23.3


line stmt bran cond sub pod time code
1             package App::NDTools::NDProc;
2              
3 1     1   110335 use strict;
  1         8  
  1         31  
4 1     1   5 use warnings FATAL => 'all';
  1         2  
  1         31  
5 1     1   5 use parent 'App::NDTools::NDTool';
  1         2  
  1         5  
6              
7 1     1   52 use Getopt::Long qw(:config bundling pass_through);
  1         2  
  1         5  
8 1     1   175 use Log::Log4Cli;
  1         2  
  1         67  
9 1     1   483 use Module::Find qw(findsubmod);
  1         1389  
  1         58  
10 1     1   7 use App::NDTools::Slurp qw(s_decode s_dump s_encode);
  1         1  
  1         47  
11 1     1   682 use Storable qw(dclone freeze thaw);
  1         3114  
  1         72  
12 1     1   524 use Struct::Diff 0.94 qw(diff split_diff);
  1         2930  
  1         67  
13 1     1   7 use Struct::Path 0.80 qw(path);
  1         14  
  1         46  
14 1     1   510 use Struct::Path::PerlStyle 0.80 qw(str2path);
  1         167911  
  1         2367  
15              
16             our $VERSION = '0.24';
17              
18             sub arg_opts {
19 1     1 0 2 my $self = shift;
20              
21             my %arg_opts = (
22             $self->SUPER::arg_opts(),
23             'builtin-format=s' => \$self->{OPTS}->{'builtin-format'},
24             'builtin-rules=s' => \$self->{OPTS}->{'builtin-rules'},
25             'disable-module=s@' => \$self->{OPTS}->{'disable-module'},
26             'dump-blame=s' => \$self->{OPTS}->{'dump-blame'},
27             'dump-rules=s' => \$self->{OPTS}->{'dump-rules'},
28             'embed-blame=s' => \$self->{OPTS}->{'embed-blame'},
29             'embed-rules=s' => \$self->{OPTS}->{'embed-rules'},
30             'list-modules|l' => \$self->{OPTS}->{'list-modules'},
31             'module|m=s' => \$self->{OPTS}->{module},
32             'rules=s@' => \$self->{OPTS}->{rules},
33 1         5 );
34 1         7 delete $arg_opts{'help|h'}; # skip in first args parsing -- will be accessable for modules
35 1         3 delete $arg_opts{'version|V'}; # --"--
36              
37 1         10 return %arg_opts;
38             }
39              
40             sub configure {
41 1     1 0 2 my $self = shift;
42              
43 1         8 $self->index_modules();
44              
45 1         3 $self->{rules} = [];
46 0         0 map { push @{$self->{rules}}, @{$self->load_struct($_)} }
  0         0  
  0         0  
47 0         0 @{$self->{OPTS}->{rules}}
48 1 50       9 if ($self->{OPTS}->{rules});
49              
50 1 50 33     4 if ($self->{OPTS}->{module} or @{$self->{rules}}) {
  1         6  
51 0     0   0 log_info { "Explicit rules used: builtin will be ignored" };
  0         0  
52 0         0 $self->{OPTS}->{'builtin-rules'} = undef;
53             }
54              
55             $self->{OPTS}->{'disable-module'} =
56 1         3 { map { $_ => 1 } @{$self->{OPTS}->{'disable-module'}} };
  0         0  
  1         4  
57             }
58              
59             sub defaults {
60             return {
61 1     1 0 8 'blame' => 1, # may be redefined per-rule
62             'builtin-format' => "", # raw
63             'modpath' => [ 'App::NDTools::NDProc::Module' ],
64             };
65             }
66              
67             sub dump_arg {
68 0     0 0 0 my ($self, $uri, $arg) = @_;
69              
70 0     0   0 log_debug { "Dumping result to $uri" };
  0         0  
71 0         0 s_dump($uri, $self->{OPTS}->{ofmt}, $self->{OPTS}->{pretty}, $arg);
72             }
73              
74             sub dump_blame {
75 0     0 0 0 my ($self, $blame) = @_;
76              
77 0 0       0 return unless (defined $self->{OPTS}->{'dump-blame'});
78              
79 0     0   0 log_debug { "Dumping blame to $self->{OPTS}->{'dump-blame'}" };
  0         0  
80             s_dump($self->{OPTS}->{'dump-blame'}, $self->{OPTS}->{ofmt},
81 0         0 $self->{OPTS}->{pretty}, $blame);
82             }
83              
84             sub dump_rules {
85 0     0 0 0 my $self = shift;
86              
87 0         0 for my $rule (@{$self->{rules}}) {
  0         0  
88             # remove undefs - defaults will be used anyway
89 0 0       0 map { defined $rule->{$_} || delete $rule->{$_} } keys %{$rule};
  0         0  
  0         0  
90             }
91 0     0   0 log_debug { "Dumping rules to $self->{OPTS}->{'dump-rules'}" };
  0         0  
92             s_dump($self->{OPTS}->{'dump-rules'}, $self->{OPTS}->{ofmt},
93 0         0 $self->{OPTS}->{pretty}, $self->{rules});
94             }
95              
96             sub embed {
97 0     0 0 0 my ($self, $data, $path, $thing) = @_;
98              
99 0         0 my $spath = eval { str2path($path) };
  0         0  
100 0 0       0 die_fatal "Unable to parse '$path' ($@)", 4 if ($@);
101 0         0 my $ref = eval { (path($data, $spath, expand => 1))[0]};
  0         0  
102 0 0       0 die_fatal "Unable to lookup '$path' ($@)", 4 if ($@);
103              
104 0         0 ${$ref} = $self->{OPTS}->{'builtin-format'} ?
105 0 0       0 s_encode($thing, $self->{OPTS}->{'builtin-format'}) :
106             $thing;
107             }
108              
109             sub exec {
110 0     0 0 0 my $self = shift;
111              
112 0 0       0 if ($self->{OPTS}->{'list-modules'}) {
113 0         0 map { printf "%-10s %-8s %s\n", @{$_} } $self->list_modules;
  0         0  
  0         0  
114 0         0 die_info undef, 0;
115             }
116              
117 0 0       0 if (defined $self->{OPTS}->{module}) {
118             die_fatal "Unknown module specified '$self->{OPTS}->{module}'", 1
119 0 0       0 unless (exists $self->{MODS}->{$self->{OPTS}->{module}});
120 0         0 $self->init_module($self->{OPTS}->{module});
121              
122 0         0 my $mod = $self->{MODS}->{$self->{OPTS}->{module}}->new();
123 0         0 for my $rule ($mod->parse_args($self->{ARGV})->get_opts()) {
124             $rule->{modname} = $self->{OPTS}->{module},
125 0         0 push @{$self->{rules}}, $rule;
  0         0  
126             }
127             }
128              
129             # parse the rest of args (unrecognized by module (if was specified by args))
130             # to be sure there is no unsupported opts remain
131             my @rest_opts = (
132 0     0   0 'help|h' => sub { $self->usage; exit 0 },
  0         0  
133 0     0   0 'version|V' => sub { print $self->VERSION . "\n"; exit 0 },
  0         0  
134 0         0 );
135              
136 0         0 my $p = Getopt::Long::Parser->new();
137 0         0 $p->configure('nopass_through'); # just to be sure
138 0 0       0 unless ($p->getoptionsfromarray($self->{ARGV}, @rest_opts)) {
139              
140 0         0 $self->usage;
141 0         0 die_fatal "Unsupported opts passed", 1;
142             }
143              
144 0 0 0     0 if ($self->{OPTS}->{'dump-rules'} and not @{$self->{ARGV}}) {
  0         0  
145 0         0 $self->dump_rules();
146             } else {
147 0 0       0 $self->check_args(@{$self->{ARGV}}) or die_fatal undef, 1;
  0         0  
148 0         0 $self->process_args(@{$self->{ARGV}});
  0         0  
149             }
150              
151 0         0 die_info "All done", 0;
152             }
153              
154             sub index_modules {
155 1     1 0 2 my $self = shift;
156              
157 1         2 my $required = { map { $_->{modname} => 1 } @{$self->{rules}} };
  0         0  
  1         4  
158 1 50       3 $required->{$self->{OPTS}->{module}} = 1 if ($self->{OPTS}->{module});
159              
160 1         2 for my $path (@{$self->{OPTS}->{modpath}}) {
  1         3  
161 1     0   7 log_trace { "Indexing modules in $path" };
  0         0  
162 1         10 for my $m (findsubmod $path) {
163 4         2923 $self->{MODS}->{(split('::', $m))[-1]} = $m;
164             }
165             }
166              
167 1         3 return $self;
168             }
169              
170             sub init_module {
171 4     4 0 68449 my ($self, $mod) = @_;
172              
173 4 50       13 return if ($self->{_initialized_mods}->{$mod});
174              
175 4     0   27 log_trace { "Inititializing module $mod ($self->{MODS}->{$mod})" };
  0         0  
176 4         284 eval "require $self->{MODS}->{$mod}";
177 4 50       27 die_fatal "Failed to initialize module '$mod' ($@)", 1 if ($@);
178 4         35 $self->{_initialized_mods}->{$mod} = 1;
179             }
180              
181             sub list_modules {
182 0     0 0   my $self = shift;
183              
184             return map {
185 0           $self->init_module($_);
186 0           [ $_, $self->{MODS}->{$_}->VERSION, $self->{MODS}->{$_}->MODINFO ]
187 0           } sort keys %{$self->{MODS}};
  0            
188             }
189              
190             sub load_arg {
191 0     0 0   shift->load_struct(@_);
192             }
193              
194             *load_source = \&load_arg;
195              
196             sub load_builtin_rules {
197 0     0 0   my ($self, $data, $path) = @_;
198              
199 0     0     log_debug { "Loading builtin rules from '$path'" };
  0            
200 0           my $spath = eval { str2path($path) };
  0            
201 0 0         die_fatal "Unable to parse path ($@)", 4 if ($@);
202 0           my $rules = eval { (path($data, $spath, deref => 1, strict => 1))[0] };
  0            
203 0 0         die_fatal "Unable to lookup path ($@)", 4 if ($@);
204              
205             return $self->{OPTS}->{'builtin-format'} ?
206 0 0         s_decode($rules, $self->{OPTS}->{'builtin-format'}) :
207             $rules;
208             }
209              
210             sub process_args {
211 0     0 0   my $self = shift;
212              
213 0           for my $arg (@_) {
214 0     0     log_info { "Processing $arg" };
  0            
215 0           my $data = $self->load_arg($arg, $self->{OPTS}->{ifmt});
216              
217 0 0         if ($self->{OPTS}->{'builtin-rules'}) {
218 0           $self->{rules} = $self->load_builtin_rules($data, $self->{OPTS}->{'builtin-rules'});
219             # restore original rules - may be changed while processing structure
220             $self->{OPTS}->{'embed-rules'} = $self->{OPTS}->{'builtin-rules'}
221 0 0         if (not defined $self->{OPTS}->{'embed-rules'});
222             }
223              
224 0 0         if ($self->{OPTS}->{'dump-rules'}) {
225 0           $self->dump_rules();
226 0           next;
227             }
228              
229 0           my @blame = $self->resolve_rules($arg)->process_rules(\$data);
230              
231 0 0         if ($self->{OPTS}->{'embed-blame'}) {
232 0     0     log_debug { "Embedding blame to '$self->{OPTS}->{'embed-blame'}'" };
  0            
233 0           $self->embed($data, $self->{OPTS}->{'embed-blame'}, \@blame);
234             }
235              
236 0 0         if ($self->{OPTS}->{'embed-rules'}) {
237 0     0     log_debug { "Embedding rules to '$self->{OPTS}->{'embed-rules'}'" };
  0            
238 0           $self->embed($data, $self->{OPTS}->{'embed-rules'}, $self->{rules});
239             }
240              
241 0           $self->dump_arg($arg, $data);
242 0           $self->dump_blame(\@blame);
243             }
244             }
245              
246             sub process_rules {
247 0     0 0   my ($self, $data) = @_;
248              
249 0           my $rcnt = 0; # rules counter
250 0           my @blame;
251              
252 0           for my $rule (@{$self->{resolved_rules}}) {
  0            
253 0 0         if (exists $self->{OPTS}->{'disable-module'}->{$rule->{modname}}) {
254 0     0     log_debug { "Skip rule #$rcnt (module $rule->{modname} is disabled by args)" };
  0            
255 0           next;
256             }
257 0 0         if ($rule->{disabled}) {
258 0     0     log_debug { "Rule #$rcnt ($rule->{modname}) is disabled, skip it" };
  0            
259 0           next;
260             }
261             die_fatal "Unknown module specified ($rule->{modname}; rule #$rcnt)", 1
262 0 0         unless (exists $self->{MODS}->{$rule->{modname}});
263              
264 0     0     log_debug { "Processing rule #$rcnt ($rule->{modname})" };
  0            
265 0           $self->init_module($rule->{modname});
266              
267 0 0         my $result = ref ${$data} ? dclone(${$data}) : ${$data};
  0            
  0            
  0            
268 0 0         my $source = exists $rule->{source} ? thaw($self->{sources}->{$rule->{source}}) : undef;
269 0           $self->{MODS}->{$rule->{modname}}->new->process($data, $rule, $source);
270              
271 0           my $changes = { rule_id => 0 + $rcnt };
272 0 0         if (defined $rule->{blame} ? $rule->{blame} : $self->{OPTS}->{blame}) {
    0          
273 0           my $diff = split_diff(diff($result, ${$data}, noO => 1, noU => 1));
  0            
274 0 0         $changes->{R} = delete $diff->{a} if (exists $diff->{a}); # more obvious
275 0 0         $changes->{A} = delete $diff->{b} if (exists $diff->{b}); # --"--
276             }
277 0 0         map { $changes->{$_} = $rule->{$_} if (defined $rule->{$_}) }
  0            
278             qw(blame comment source); # preserve useful info
279 0           push @blame, dclone($changes);
280             } continue {
281 0           $rcnt++;
282             }
283              
284 0           return @blame;
285             }
286              
287             sub resolve_rules {
288 0     0 0   my ($self, $opt_src) = @_;
289              
290 0     0     log_debug { "Resolving rules" };
  0            
291 0           $self->{resolved_rules} = dclone($self->{rules});
292              
293 0           for my $rule (@{$self->{resolved_rules}}) {
  0            
294             # single path may be specified as string, convert it to list
295             $rule->{path}->[0] = delete $rule->{path}
296 0 0 0       if (exists $rule->{path} and not ref $rule->{path});
297              
298 0 0         next unless (exists $rule->{source});
299 0 0 0       unless (defined $rule->{source} and $rule->{source} ne '') {
300             # use processing doc as source
301 0           $rule->{source} = $opt_src;
302             }
303 0 0         next if (exists $self->{sources}->{$rule->{source}});
304             $self->{sources}->{$rule->{source}} =
305 0           freeze($self->load_source($rule->{source}, $self->{OPTS}->{ifmt}));
306             }
307              
308 0           return $self;
309             }
310              
311             1; # End of App::NDTools::NDProc