File Coverage

blib/lib/App/NDTools/NDProc.pm
Criterion Covered Total %
statement 194 217 89.4
branch 67 76 88.1
condition 11 12 91.6
subroutine 27 43 62.7
pod 0 16 0.0
total 299 364 82.1


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