File Coverage

blib/lib/App/NDTools/NDProc.pm
Criterion Covered Total %
statement 217 235 92.3
branch 75 86 87.2
condition 11 12 91.6
subroutine 30 45 66.6
pod 0 16 0.0
total 333 394 84.5


line stmt bran cond sub pod time code
1             package App::NDTools::NDProc;
2              
3 9     9   113770 use strict;
  9         29  
  9         357  
4 9     9   55 use warnings FATAL => 'all';
  9         19  
  9         356  
5 9     9   51 use parent 'App::NDTools::NDTool';
  9         19  
  9         234  
6              
7 9     9   525 use Getopt::Long qw(GetOptionsFromArray :config bundling);
  9         22  
  9         59  
8 9     9   1353 use Log::Log4Cli;
  9         23  
  9         734  
9 9     9   4265 use Module::Find qw(findsubmod);
  9         11953  
  9         606  
10 9     9   76 use App::NDTools::Slurp qw(s_decode s_dump s_encode);
  9         19  
  9         475  
11 9     9   5481 use Storable qw(dclone freeze thaw);
  9         28405  
  9         689  
12 9     9   4391 use Struct::Diff 0.94 qw(diff split_diff);
  9         26297  
  9         648  
13 9     9   72 use Struct::Path 0.80 qw(path);
  9         172  
  9         455  
14 9     9   4317 use Struct::Path::PerlStyle 0.90 qw(str2path);
  9         467505  
  9         28123  
15              
16             our $VERSION = '0.32';
17              
18             sub arg_opts {
19 113     113 0 260 my $self = shift;
20              
21 113         630 Getopt::Long::Configure('pass_through');
22              
23             my %arg_opts = (
24             $self->SUPER::arg_opts(),
25             'builtin-format=s' => \$self->{OPTS}->{'builtin-format'},
26             'builtin-rules=s' => \$self->{OPTS}->{'builtin-rules'},
27             'disable-module=s@' => \$self->{OPTS}->{'disable-module'},
28             'dump-blame=s' => \$self->{OPTS}->{'dump-blame'},
29             'dump-rules=s' => \$self->{OPTS}->{'dump-rules'},
30             'embed-blame=s' => \$self->{OPTS}->{'embed-blame'},
31             'embed-rules=s' => \$self->{OPTS}->{'embed-rules'},
32             'list-modules|l' => \$self->{OPTS}->{'list-modules'},
33             'module|m=s' => \$self->{OPTS}->{module},
34             'rules=s@' => \$self->{OPTS}->{rules},
35 113         4408 );
36 113         645 delete $arg_opts{'help|h'}; # skip in first args parsing -- will be accessable for modules
37 113         374 delete $arg_opts{'version|V'}; # --"--
38              
39 113         950 return %arg_opts;
40             }
41              
42             sub configure {
43 113     113 0 271 my $self = shift;
44              
45 113         384 $self->index_modules();
46              
47 113         318 $self->{rules} = [];
48 8         34 map { push @{$self->{rules}}, @{$self->load_struct($_)} }
  8         24  
  8         43  
49 8         33 @{$self->{OPTS}->{rules}}
50 113 100       485 if ($self->{OPTS}->{rules});
51              
52 113 100 100     467 if ($self->{OPTS}->{module} or @{$self->{rules}}) {
  15         69  
53 105     0   793 log_info { "Explicit rules used: builtin will be ignored" };
  0         0  
54 105         772 $self->{OPTS}->{'builtin-rules'} = undef;
55             }
56              
57             $self->{OPTS}->{'disable-module'} =
58 113         218 { map { $_ => 1 } @{$self->{OPTS}->{'disable-module'}} };
  4         15  
  113         522  
59             }
60              
61             sub defaults {
62             return {
63 113     113 0 727 'blame' => 1, # may be redefined per-rule
64             'builtin-format' => 'RAW',
65             'modpath' => [ 'App::NDTools::NDProc::Module' ],
66             };
67             }
68              
69             sub dump_blame {
70 4     4 0 14 my ($self, $blame) = @_;
71              
72 4     0   33 log_debug { "Dumping blame to $self->{OPTS}->{'dump-blame'}" };
  0         0  
73             s_dump($self->{OPTS}->{'dump-blame'}, $self->{OPTS}->{ofmt},
74 4         39 $self->{OPTS}->{pretty}, $blame);
75             }
76              
77             sub dump_result {
78 73     73 0 246 my ($self, $uri, $data) = @_;
79              
80 73     0   631 log_debug { "Dumping result to $uri" };
  0         0  
81 73         693 s_dump($uri, $self->{OPTS}->{ofmt}, $self->{OPTS}->{pretty}, $data);
82             }
83              
84             sub dump_rules {
85 4     4 0 9 my $self = shift;
86              
87 4         10 for my $rule (@{$self->{rules}}) {
  4         12  
88             # remove undefs - defaults will be used anyway
89 9 100       15 map { defined $rule->{$_} || delete $rule->{$_} } keys %{$rule};
  39         165  
  9         24  
90             }
91 4     0   36 log_debug { "Dumping rules to $self->{OPTS}->{'dump-rules'}" };
  0         0  
92             s_dump($self->{OPTS}->{'dump-rules'}, $self->{OPTS}->{ofmt},
93 4         46 $self->{OPTS}->{pretty}, $self->{rules});
94             }
95              
96             sub embed {
97 4     4 0 18 my ($self, $data, $path, $thing) = @_;
98              
99 4         9 my $spath = eval { str2path($path) };
  4         17  
100 4 50       2732 die_fatal "Unable to parse '$path' ($@)", 4 if ($@);
101 4         10 my $ref = eval { (path($data, $spath, expand => 1))[0]};
  4         21  
102 4 50       627 die_fatal "Unable to lookup '$path' ($@)", 4 if ($@);
103              
104 4         18 ${$ref} = $self->{OPTS}->{'builtin-format'} eq 'RAW'
105             ? $thing
106 4 100       24 : s_encode($thing, $self->{OPTS}->{'builtin-format'}, {pretty => 0});
107             }
108              
109             sub exec {
110 112     112 0 558 my $self = shift;
111              
112 112 100       318 if ($self->{OPTS}->{'list-modules'}) {
113 1         6 map { printf "%-16s %-8s %s\n", @{$_} } $self->list_modules;
  7         17  
  7         160  
114 1         13 die_info undef, 0;
115             }
116              
117 111         203 my $mod;
118 111 100       326 if (defined $self->{OPTS}->{module}) {
119             die_fatal "Unknown module specified '$self->{OPTS}->{module}'", 1
120 98 100       399 unless (exists $self->{MODS}->{$self->{OPTS}->{module}});
121 97         372 $self->init_module($self->{OPTS}->{module});
122              
123 97         686 $mod = $self->{MODS}->{$self->{OPTS}->{module}}->new();
124 97         436 for my $rule ($mod->parse_args($self->{ARGV})->get_opts()) {
125             $rule->{modname} = $self->{OPTS}->{module},
126 91         329 push @{$self->{rules}}, $rule;
  91         328  
127             }
128             }
129              
130             # parse the rest of args (unrecognized by module (if was specified by args))
131             # to be sure there is no unsupported opts remain
132             my @rest_opts = (
133             'help|h' => sub {
134 1     1   258 $self->{OPTS}->{help} = 1;
135 1         4 die "!FINISH";
136             },
137             'version|V' => sub {
138 1     1   262 $self->{OPTS}->{version} = 1;
139 1         5 die "!FINISH";
140             },
141 103         720 );
142              
143 103         436 Getopt::Long::Configure('nopass_through');
144 103 100       3962 unless (GetOptionsFromArray($self->{ARGV}, @rest_opts)) {
145 2 50       568 defined $mod ? $mod->usage : $self->usage;
146 2         26680 die_fatal "Unsupported opts passed", 1;
147             }
148              
149 101 100       18214 if ($self->{OPTS}->{help}) {
150 1         14 $self->usage;
151 1         69380 die_info, 0;
152             }
153              
154 100 100       324 if ($self->{OPTS}->{version}) {
155 1         61 print $self->VERSION . "\n";
156 1         11 die_info, 0;
157             }
158              
159 99 100 100     386 if ($self->{OPTS}->{'dump-rules'} and not @{$self->{ARGV}}) {
  4         22  
160 3         16 $self->dump_rules();
161             } else {
162 96 50       178 $self->check_args(@{$self->{ARGV}}) or die_fatal undef, 1;
  96         570  
163 95         202 $self->process_args(@{$self->{ARGV}});
  95         363  
164             }
165              
166 75         441 die_info "All done", 0;
167             }
168              
169             sub index_modules {
170 113     113 0 216 my $self = shift;
171              
172 113         199 my $required = { map { $_->{modname} => 1 } @{$self->{rules}} };
  0         0  
  113         366  
173 113 100       607 $required->{$self->{OPTS}->{module}} = 1 if ($self->{OPTS}->{module});
174              
175 113         204 for my $path (@{$self->{OPTS}->{modpath}}) {
  113         372  
176 113     1   843 log_trace { "Indexing modules in $path" };
  1         97  
177 113         945 for my $m (findsubmod $path) {
178 791         416333 $self->{MODS}->{(split('::', $m))[-1]} = $m;
179             }
180             }
181              
182 113         339 return $self;
183             }
184              
185             sub init_module {
186 214     214 0 13799 my ($self, $mod) = @_;
187              
188 214 100       710 return if ($self->{_initialized_mods}->{$mod});
189              
190 119     0   731 log_trace { "Inititializing module $mod ($self->{MODS}->{$mod})" };
  0         0  
191 119         9061 eval "require $self->{MODS}->{$mod}";
192 119 50       615 die_fatal "Failed to initialize module '$mod' ($@)", 1 if ($@);
193 119         504 $self->{_initialized_mods}->{$mod} = 1;
194             }
195              
196             sub list_modules {
197 1     1 0 3 my $self = shift;
198              
199             return map {
200 7         24 $self->init_module($_);
201 7         134 [ $_, $self->{MODS}->{$_}->VERSION, $self->{MODS}->{$_}->MODINFO ]
202 1         3 } sort keys %{$self->{MODS}};
  1         9  
203             }
204              
205             sub load_uri {
206 139     139 0 638 shift->load_struct(@_);
207             }
208              
209             *load_source = \&load_uri;
210              
211             sub load_builtin_rules {
212 3     3 0 114 my ($self, $data, $path) = @_;
213              
214 3     0   22 log_debug { "Loading builtin rules from '$path'" };
  0         0  
215 3         19 my $spath = eval { str2path($path) };
  3         16  
216 3 50       2123 die_fatal "Unable to parse path ($@)", 4 if ($@);
217 3         5 my $rules = eval { (path($data, $spath, deref => 1, strict => 1))[0] };
  3         20  
218 3 50       542 die_fatal "Unable to lookup path ($@)", 4 if ($@);
219              
220 3 100       30 return $rules if ($self->{OPTS}->{'builtin-format'} eq 'RAW');
221 1         7 return s_decode($rules, $self->{OPTS}->{'builtin-format'});
222             }
223              
224             sub process_args {
225 95     95 0 174 my $self = shift;
226              
227 95         256 for my $uri (@_) {
228 97     0   625 log_info { "Processing $uri" };
  0         0  
229 97         760 my $data = $self->load_uri($uri, $self->{OPTS}->{ifmt});
230              
231 97 100       356 if ($self->{OPTS}->{'builtin-rules'}) {
232 3         17 $self->{rules} = $self->load_builtin_rules($data, $self->{OPTS}->{'builtin-rules'});
233             # restore original rules - may be changed while processing structure
234             $self->{OPTS}->{'embed-rules'} = $self->{OPTS}->{'builtin-rules'}
235 3 50       17 if (not defined $self->{OPTS}->{'embed-rules'});
236             }
237              
238 97 100       300 if ($self->{OPTS}->{'dump-rules'}) {
239 1         6 $self->dump_rules();
240 1         13 next;
241             }
242              
243 96         374 my @blame = $self->resolve_rules($uri)->process_rules(\$data);
244              
245 73 100       281 if ($self->{OPTS}->{'embed-blame'}) {
246 1     0   9 log_debug { "Embedding blame to '$self->{OPTS}->{'embed-blame'}'" };
  0         0  
247 1         10 $self->embed($data, $self->{OPTS}->{'embed-blame'}, \@blame);
248             }
249              
250 73 100       264 if ($self->{OPTS}->{'embed-rules'}) {
251 3     0   24 log_debug { "Embedding rules to '$self->{OPTS}->{'embed-rules'}'" };
  0         0  
252 3         31 $self->embed($data, $self->{OPTS}->{'embed-rules'}, $self->{rules});
253             }
254              
255 73         415 $self->dump_result($uri, $data);
256 73 100       1108 $self->dump_blame(\@blame) if (defined $self->{OPTS}->{'dump-blame'});
257             }
258             }
259              
260             sub process_rules {
261 93     93 0 267 my ($self, $data) = @_;
262              
263 93         184 my $rnum = 0; # rule number
264 93         166 my @blame;
265              
266             RULE:
267 93         160 for my $rule (@{$self->{resolved_rules}}) {
  93         308  
268             die_fatal "Unknown module specified ($rule->{modname}; rule #$rnum)", 1
269 112 100       446 unless (exists $self->{MODS}->{$rule->{modname}});
270              
271 111 100       352 if (exists $self->{OPTS}->{'disable-module'}->{$rule->{modname}}) {
272 6     0   35 log_debug { "Skip rule #$rnum (disabled module $rule->{modname})" };
  0         0  
273 6         29 next;
274             }
275              
276 105 50       291 if ($rule->{disabled}) {
277 0     0   0 log_debug { "Skip disabled rule #$rnum ($rule->{modname})" };
  0         0  
278 0         0 next;
279             }
280              
281 105 100       378 for my $cond (defined $rule->{cond} ? @{$rule->{cond}} : ()) {
  3         12  
282 4     0   465 log_debug { "Evaluating condition '$cond'" };
  0         0  
283              
284 4         23 my $spath = eval { str2path($cond) };
  4         17  
285 4 50       4368 die_fatal "Unable to parse '$cond' ($@)", 4 if ($@);
286              
287 4 100       9 unless (eval { path($data, $spath) }) {
  4         14  
288 2 50       844 die_fatal "Failed to evaluate cond '$cond' ($@)", 4 if ($@);
289              
290 2     0   15 log_info { "Skip rule #$rnum ($rule->{modname}) cond '$cond'" };
  0         0  
291 2         51 next RULE;
292             }
293             }
294              
295 103     0   1185 log_debug { "Processing rule #$rnum ($rule->{modname})" };
  0         0  
296 103         853 $self->init_module($rule->{modname});
297              
298 103 100       208 my $result = ref ${$data} ? dclone(${$data}) : ${$data};
  103         376  
  102         3064  
  1         3  
299             my $source = exists $rule->{source}
300             ? thaw($self->{sources}->{$rule->{source}})
301 103 100       557 : undef;
302              
303 103         1789 $self->{MODS}->{$rule->{modname}}->new->process($data, $rule, $source);
304              
305 84         593 my $changes = { rule_id => 0 + $rnum };
306 84 100       413 if (defined $rule->{blame} ? $rule->{blame} : $self->{OPTS}->{blame}) {
    100          
307 82         187 $changes->{diff} = diff($result, ${$data}, noU => 1);
  82         455  
308             }
309              
310 84 100       62470 map { $changes->{$_} = $rule->{$_} if (defined $rule->{$_}) }
  252         979  
311             qw(blame comment source); # preserve useful info
312 84         2900 push @blame, dclone($changes);
313             } continue {
314 92         319 $rnum++;
315             }
316              
317 73         280 return @blame;
318             }
319              
320             sub resolve_rules {
321 96     96 0 265 my ($self, $opt_src) = @_;
322              
323 96     0   710 log_debug { "Resolving rules" };
  0         0  
324 96         4558 $self->{resolved_rules} = dclone($self->{rules});
325              
326 96         296 for my $rule (@{$self->{resolved_rules}}) {
  96         344  
327             # single path may be specified as string, convert it to list
328             $rule->{path}->[0] = delete $rule->{path}
329 115 100 100     830 if (exists $rule->{path} and not ref $rule->{path});
330              
331 115 100       414 next unless (exists $rule->{source});
332 43 100 66     239 unless (defined $rule->{source} and $rule->{source} ne '') {
333             # use processing doc as source
334 1         4 $rule->{source} = $opt_src;
335             }
336 43 100       194 next if (exists $self->{sources}->{$rule->{source}});
337             $self->{sources}->{$rule->{source}} =
338 42         169 freeze($self->load_source($rule->{source}, $self->{OPTS}->{ifmt}));
339             }
340              
341 93         2523 return $self;
342             }
343              
344             1; # End of App::NDTools::NDProc