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 6     6   91532 use strict;
  6         22  
  6         198  
4 6     6   29 use warnings FATAL => 'all';
  6         10  
  6         201  
5 6     6   30 use parent 'App::NDTools::NDTool';
  6         9  
  6         134  
6              
7 6     6   289 use Getopt::Long qw(GetOptionsFromArray :config bundling);
  6         13  
  6         27  
8 6     6   749 use Log::Log4Cli;
  6         13  
  6         400  
9 6     6   2469 use Module::Find qw(findsubmod);
  6         6872  
  6         344  
10 6     6   40 use App::NDTools::Slurp qw(s_decode s_dump s_encode);
  6         10  
  6         278  
11 6     6   3114 use Storable qw(dclone freeze thaw);
  6         16584  
  6         393  
12 6     6   2581 use Struct::Diff 0.94 qw(diff split_diff);
  6         14521  
  6         372  
13 6     6   39 use Struct::Path 0.80 qw(path);
  6         84  
  6         304  
14 6     6   2658 use Struct::Path::PerlStyle 0.80 qw(str2path);
  6         266234  
  6         14954  
15              
16             our $VERSION = '0.29';
17              
18             sub arg_opts {
19 84     84 0 151 my $self = shift;
20              
21 84         357 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 84         2808 );
36 84         421 delete $arg_opts{'help|h'}; # skip in first args parsing -- will be accessable for modules
37 84         219 delete $arg_opts{'version|V'}; # --"--
38              
39 84         594 return %arg_opts;
40             }
41              
42             sub configure {
43 84     84 0 168 my $self = shift;
44              
45 84         266 $self->index_modules();
46              
47 84         216 $self->{rules} = [];
48 8         12 map { push @{$self->{rules}}, @{$self->load_struct($_)} }
  8         17  
  8         30  
49 8         21 @{$self->{OPTS}->{rules}}
50 84 100       270 if ($self->{OPTS}->{rules});
51              
52 84 100 100     284 if ($self->{OPTS}->{module} or @{$self->{rules}}) {
  15         66  
53 76     0   513 log_info { "Explicit rules used: builtin will be ignored" };
  0         0  
54 76         397 $self->{OPTS}->{'builtin-rules'} = undef;
55             }
56              
57             $self->{OPTS}->{'disable-module'} =
58 84         134 { map { $_ => 1 } @{$self->{OPTS}->{'disable-module'}} };
  4         15  
  84         302  
59             }
60              
61             sub defaults {
62             return {
63 84     84 0 450 '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 57     57 0 137 my ($self, $uri, $arg) = @_;
71              
72 57     0   370 log_debug { "Dumping result to $uri" };
  0         0  
73 57         423 s_dump($uri, $self->{OPTS}->{ofmt}, $self->{OPTS}->{pretty}, $arg);
74             }
75              
76             sub dump_blame {
77 57     57 0 152 my ($self, $blame) = @_;
78              
79 57 100       600 return unless (defined $self->{OPTS}->{'dump-blame'});
80              
81 4     0   24 log_debug { "Dumping blame to $self->{OPTS}->{'dump-blame'}" };
  0         0  
82             s_dump($self->{OPTS}->{'dump-blame'}, $self->{OPTS}->{ofmt},
83 4         28 $self->{OPTS}->{pretty}, $blame);
84             }
85              
86             sub dump_rules {
87 4     4 0 7 my $self = shift;
88              
89 4         7 for my $rule (@{$self->{rules}}) {
  4         8  
90             # remove undefs - defaults will be used anyway
91 9 100       12 map { defined $rule->{$_} || delete $rule->{$_} } keys %{$rule};
  36         81  
  9         19  
92             }
93 4     0   23 log_debug { "Dumping rules to $self->{OPTS}->{'dump-rules'}" };
  0         0  
94             s_dump($self->{OPTS}->{'dump-rules'}, $self->{OPTS}->{ofmt},
95 4         33 $self->{OPTS}->{pretty}, $self->{rules});
96             }
97              
98             sub embed {
99 4     4 0 11 my ($self, $data, $path, $thing) = @_;
100              
101 4         5 my $spath = eval { str2path($path) };
  4         14  
102 4 50       2322 die_fatal "Unable to parse '$path' ($@)", 4 if ($@);
103 4         8 my $ref = eval { (path($data, $spath, expand => 1))[0]};
  4         12  
104 4 50       476 die_fatal "Unable to lookup '$path' ($@)", 4 if ($@);
105              
106 4         26 ${$ref} = $self->{OPTS}->{'builtin-format'} eq 'RAW'
107             ? $thing
108 4 100       18 : s_encode($thing, $self->{OPTS}->{'builtin-format'});
109             }
110              
111             sub exec {
112 83     83 0 144 my $self = shift;
113              
114 83 100       195 if ($self->{OPTS}->{'list-modules'}) {
115 1         6 map { printf "%-10s %-8s %s\n", @{$_} } $self->list_modules;
  4         8  
  4         80  
116 1         8 die_info undef, 0;
117             }
118              
119 82 100       205 if (defined $self->{OPTS}->{module}) {
120             die_fatal "Unknown module specified '$self->{OPTS}->{module}'", 1
121 69 100       226 unless (exists $self->{MODS}->{$self->{OPTS}->{module}});
122 68         201 $self->init_module($self->{OPTS}->{module});
123              
124 68         382 my $mod = $self->{MODS}->{$self->{OPTS}->{module}}->new();
125 68         214 for my $rule ($mod->parse_args($self->{ARGV})->get_opts()) {
126             $rule->{modname} = $self->{OPTS}->{module},
127 63         195 push @{$self->{rules}}, $rule;
  63         178  
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   213 $self->{OPTS}->{help} = 1;
136 1         4 die "!FINISH";
137             },
138             'version|V' => sub {
139 1     1   209 $self->{OPTS}->{version} = 1;
140 1         4 die "!FINISH";
141             },
142 75         549 );
143              
144 75         248 Getopt::Long::Configure('nopass_through');
145 75 100       2351 unless (GetOptionsFromArray($self->{ARGV}, @rest_opts)) {
146 1         214 $self->usage;
147 1         5406 die_fatal "Unsupported opts passed", 1;
148             }
149              
150 74 100       11128 if ($self->{OPTS}->{help}) {
151 1         10 $self->usage;
152 1         25175 die_info, 0;
153             }
154              
155 73 100       188 if ($self->{OPTS}->{version}) {
156 1         40 print $self->VERSION . "\n";
157 1         8 die_info, 0;
158             }
159              
160 72 100 100     205 if ($self->{OPTS}->{'dump-rules'} and not @{$self->{ARGV}}) {
  4         15  
161 3         11 $self->dump_rules();
162             } else {
163 69 50       91 $self->check_args(@{$self->{ARGV}}) or die_fatal undef, 1;
  69         330  
164 68         104 $self->process_args(@{$self->{ARGV}});
  68         210  
165             }
166              
167 59         260 die_info "All done", 0;
168             }
169              
170             sub index_modules {
171 84     84 0 169 my $self = shift;
172              
173 84         120 my $required = { map { $_->{modname} => 1 } @{$self->{rules}} };
  0         0  
  84         242  
174 84 100       337 $required->{$self->{OPTS}->{module}} = 1 if ($self->{OPTS}->{module});
175              
176 84         111 for my $path (@{$self->{OPTS}->{modpath}}) {
  84         196  
177 84     1   529 log_trace { "Indexing modules in $path" };
  1         82  
178 84         571 for my $m (findsubmod $path) {
179 336         183238 $self->{MODS}->{(split('::', $m))[-1]} = $m;
180             }
181             }
182              
183 84         192 return $self;
184             }
185              
186             sub init_module {
187 157     157 0 5437 my ($self, $mod) = @_;
188              
189 157 100       457 return if ($self->{_initialized_mods}->{$mod});
190              
191 84     0   479 log_trace { "Inititializing module $mod ($self->{MODS}->{$mod})" };
  0         0  
192 84         4976 eval "require $self->{MODS}->{$mod}";
193 84 50       347 die_fatal "Failed to initialize module '$mod' ($@)", 1 if ($@);
194 84         279 $self->{_initialized_mods}->{$mod} = 1;
195             }
196              
197             sub list_modules {
198 1     1 0 3 my $self = shift;
199              
200             return map {
201 4         13 $self->init_module($_);
202 4         54 [ $_, $self->{MODS}->{$_}->VERSION, $self->{MODS}->{$_}->MODINFO ]
203 1         3 } sort keys %{$self->{MODS}};
  1         5  
204             }
205              
206             sub load_arg {
207 92     92 0 283 shift->load_struct(@_);
208             }
209              
210             *load_source = \&load_arg;
211              
212             sub load_builtin_rules {
213 3     3 0 7 my ($self, $data, $path) = @_;
214              
215 3     0   18 log_debug { "Loading builtin rules from '$path'" };
  0         0  
216 3         15 my $spath = eval { str2path($path) };
  3         10  
217 3 50       1703 die_fatal "Unable to parse path ($@)", 4 if ($@);
218 3         6 my $rules = eval { (path($data, $spath, deref => 1, strict => 1))[0] };
  3         13  
219 3 50       401 die_fatal "Unable to lookup path ($@)", 4 if ($@);
220              
221 3 100       13 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 68     68 0 108 my $self = shift;
227              
228 68         143 for my $arg (@_) {
229 70     0   403 log_info { "Processing $arg" };
  0         0  
230 70         446 my $data = $self->load_arg($arg, $self->{OPTS}->{ifmt});
231              
232 70 100       205 if ($self->{OPTS}->{'builtin-rules'}) {
233 3         12 $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       22 if (not defined $self->{OPTS}->{'embed-rules'});
237             }
238              
239 70 100       164 if ($self->{OPTS}->{'dump-rules'}) {
240 1         5 $self->dump_rules();
241 1         11 next;
242             }
243              
244 69         259 my @blame = $self->resolve_rules($arg)->process_rules(\$data);
245              
246 57 100       177 if ($self->{OPTS}->{'embed-blame'}) {
247 1     0   6 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 57 100       145 if ($self->{OPTS}->{'embed-rules'}) {
252 3     0   17 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 57         221 $self->dump_arg($arg, $data);
257 57         319 $self->dump_blame(\@blame);
258             }
259             }
260              
261             sub process_rules {
262 69     69 0 153 my ($self, $data) = @_;
263              
264 69         151 my $rcnt = 0; # rules counter
265 69         95 my @blame;
266              
267 69         127 for my $rule (@{$self->{resolved_rules}}) {
  69         165  
268 88 100       243 if (exists $self->{OPTS}->{'disable-module'}->{$rule->{modname}}) {
269 6     0   27 log_debug { "Skip rule #$rcnt (module $rule->{modname} is disabled by args)" };
  0         0  
270 6         26 next;
271             }
272 82 50       169 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 82 100       246 unless (exists $self->{MODS}->{$rule->{modname}});
278              
279 81     0   477 log_debug { "Processing rule #$rcnt ($rule->{modname})" };
  0         0  
280 81         500 $self->init_module($rule->{modname});
281              
282 81 100       100 my $result = ref ${$data} ? dclone(${$data}) : ${$data};
  81         215  
  80         1612  
  1         2  
283 81 100       283 my $source = exists $rule->{source} ? thaw($self->{sources}->{$rule->{source}}) : undef;
284 81         1006 $self->{MODS}->{$rule->{modname}}->new->process($data, $rule, $source);
285              
286 70         375 my $changes = { rule_id => 0 + $rcnt };
287 70 100       336 if (defined $rule->{blame} ? $rule->{blame} : $self->{OPTS}->{blame}) {
    100          
288 68         102 $changes->{diff} = diff($result, ${$data}, noU => 1);
  68         292  
289             }
290 70 100       38082 map { $changes->{$_} = $rule->{$_} if (defined $rule->{$_}) }
  210         595  
291             qw(blame comment source); # preserve useful info
292 70         1807 push @blame, dclone($changes);
293             } continue {
294 76         208 $rcnt++;
295             }
296              
297 57         184 return @blame;
298             }
299              
300             sub resolve_rules {
301 69     69 0 155 my ($self, $opt_src) = @_;
302              
303 69     0   393 log_debug { "Resolving rules" };
  0         0  
304 69         2617 $self->{resolved_rules} = dclone($self->{rules});
305              
306 69         168 for my $rule (@{$self->{resolved_rules}}) {
  69         202  
307             # single path may be specified as string, convert it to list
308             $rule->{path}->[0] = delete $rule->{path}
309 88 100 100     494 if (exists $rule->{path} and not ref $rule->{path});
310              
311 88 100       214 next unless (exists $rule->{source});
312 23 100 66     79 unless (defined $rule->{source} and $rule->{source} ne '') {
313             # use processing doc as source
314 1         2 $rule->{source} = $opt_src;
315             }
316 23 100       76 next if (exists $self->{sources}->{$rule->{source}});
317             $self->{sources}->{$rule->{source}} =
318 22         57 freeze($self->load_source($rule->{source}, $self->{OPTS}->{ifmt}));
319             }
320              
321 69         943 return $self;
322             }
323              
324             1; # End of App::NDTools::NDProc