File Coverage

blib/lib/App/NDTools/NDProc.pm
Criterion Covered Total %
statement 206 222 92.7
branch 68 76 89.4
condition 11 12 91.6
subroutine 30 43 69.7
pod 0 16 0.0
total 315 369 85.3


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