File Coverage

lib/XT/Files.pm
Criterion Covered Total %
statement 204 204 100.0
branch 77 78 98.7
condition 16 18 88.8
subroutine 37 37 100.0
pod 15 16 93.7
total 349 353 98.8


line stmt bran cond sub pod time code
1             package XT::Files;
2              
3 32     32   2142813 use 5.006;
  32         343  
4 32     32   165 use strict;
  32         61  
  32         748  
5 32     32   165 use warnings;
  32         53  
  32         1638  
6              
7             our $VERSION = '0.001';
8              
9 32     32   13653 use Class::Tiny 1;
  32         49905  
  32         156  
10              
11 32     32   15258 use Role::Tiny::With ();
  32         154962  
  32         885  
12              
13             Role::Tiny::With::with 'XT::Files::Role::Logger';
14              
15 32     32   201 use Carp ();
  32         67  
  32         429  
16 32     32   149 use File::Basename ();
  32         54  
  32         409  
17 32     32   142 use File::Find ();
  32         60  
  32         401  
18 32     32   14725 use Module::Load ();
  32         33783  
  32         646  
19 32     32   216 use Scalar::Util ();
  32         57  
  32         595  
20 32     32   12393 use version 0.77 ();
  32         58437  
  32         968  
21              
22 32     32   12798 use XT::Files::File;
  32         93  
  32         1878  
23              
24 32     32   220 use constant MODULE_NAME_RX => qr{ ^ [A-Za-z_] [0-9A-Za-z_]* (?: :: [0-9A-Za-z_]+ )* $ }xs; ## no critic (RegularExpressions::RequireLineBoundaryMatching)
  32         61  
  32         72110  
25              
26             #
27             # CLASS METHODS
28             #
29              
30             sub BUILD {
31 48     48 0 39694 my ( $self, $args ) = @_;
32              
33 48         224 $self->{_excludes} = [];
34 48         163 $self->{_file} = {};
35              
36 48 100       173 if ( exists $args->{'-config'} ) {
37 41 100       167 if ( defined $args->{'-config'} ) {
38 11         29 $self->_load_config( $args->{'-config'} );
39             }
40              
41             # -config exists but is not defined, no configuration requested
42             }
43             else {
44             # We did not get "config => undef" and therefore try to load the
45             # default config file
46 7         29 $self->_load_default_config();
47             }
48              
49 40         105 return;
50             }
51              
52             {
53             # The XT::Files singleton
54             my $xtf;
55              
56             sub initialize {
57 5     5 1 97 my $class = shift;
58              
59 5 100       13 Carp::croak( __PACKAGE__ . q{ is already initialized} ) if $class->_is_initialized;
60              
61 4         43 $xtf = $class->new(@_);
62 4         21 return $xtf;
63             }
64              
65             sub instance {
66 4     4 1 11 my ($class) = @_;
67              
68 4 100       13 if ( !$class->_is_initialized ) {
69              
70             # ignore args
71 3         9 $class->initialize;
72             }
73              
74 4         14 return $xtf;
75             }
76              
77             sub _is_initialized {
78 45     45   18745 my ($class) = @_;
79              
80 45 100       437 return 1 if defined $xtf;
81 40         253 return;
82             }
83             }
84              
85             #
86             # OBJECT METHODS
87             #
88              
89             sub plugin {
90 28     28 1 21651 my ( $self, $plugin_name, $plugin_version, $keyvals_ref ) = @_;
91              
92 28         78 my $plugin_pkg = $self->_expand_config_plugin_name($plugin_name);
93              
94 27         115 Module::Load::load($plugin_pkg);
95              
96 26 100       673 if ( defined $plugin_version ) {
97 6 100       20 $self->log_fatal("Not a valid version '$plugin_version'") if !version::is_lax($plugin_version);
98 5 100       192 $self->log_fatal( "$plugin_pkg version $plugin_version required--this is only version " . $plugin_pkg->VERSION ) if version->parse( $plugin_pkg->VERSION ) < version->parse($plugin_version);
99             }
100              
101 24 100       294 $self->log_fatal("$plugin_pkg doesn't have a run method") if !$plugin_pkg->can('run');
102 23 100       145 $self->log_fatal("$plugin_pkg doesn't have a new method") if !$plugin_pkg->can('new');
103              
104 22         92 my $plugin = $plugin_pkg->new( xtf => $self );
105              
106 22         179 $plugin->run($keyvals_ref);
107              
108 22         117 return;
109             }
110              
111             sub files {
112 20     20 1 4860 my ($self) = @_;
113              
114 20         41 my $exclude_regex;
115 20         31 my @excludes = @{ $self->{_excludes} };
  20         70  
116 20 100       66 if (@excludes) {
117 6         17 $exclude_regex = join q{|}, @excludes;
118             }
119              
120 20         38 my @result;
121             RESULT_FILE:
122 20         31 for my $name ( sort keys %{ $self->{_file} } ) {
  20         103  
123              
124             # skip ignored files
125 54 100       110 next RESULT_FILE if !defined $self->{_file}->{$name};
126              
127             # skip excluded files
128 50 100 100     593 next RESULT_FILE if defined $exclude_regex && File::Basename::fileparse($name) =~ $exclude_regex;
129              
130             # skip non-existing files
131 43 100       502 next RESULT_FILE if !-e $name;
132              
133 42         138 push @result, $self->{_file}->{$name};
134             }
135              
136 20         175 return @result;
137             }
138              
139             #
140             # File
141             #
142              
143             sub bin_file {
144 26     26 1 1518 my ( $self, $name ) = @_;
145              
146 26         207 my $file = XT::Files::File->new( name => $name, is_script => 1 );
147 26         167 $self->file( $name, $file );
148 26         53 return;
149             }
150              
151             sub file {
152 93     93 1 395 my ( $self, $name, $file ) = @_;
153              
154 93 100       246 if ( @_ > 2 ) {
155 71 100       150 if ( defined $file ) {
156 67 100 66     577 $self->log_fatal(q{File is not of class 'XT::Files::File'}) if !defined Scalar::Util::blessed($file) || !$file->isa('XT::Files::File');
157             }
158              
159 70         215 $self->{_file}->{$name} = $file;
160             }
161              
162 92         234 return $self->{_file}->{$name};
163             }
164              
165             sub ignore_file {
166 3     3 1 4835 my ( $self, $name ) = @_;
167              
168 3         13 $self->file( $name, undef );
169 3         9 return;
170             }
171              
172             sub module_file {
173 21     21 1 54 my ( $self, $name ) = @_;
174              
175 21         129 my $file = XT::Files::File->new( name => $name, is_module => 1 );
176 21         161 $self->file( $name, $file );
177 21         49 return;
178             }
179              
180             sub pod_file {
181 10     10 1 37 my ( $self, $name ) = @_;
182              
183 10         65 my $file = XT::Files::File->new( name => $name, is_pod => 1 );
184 10         77 $self->file( $name, $file );
185 10         28 return;
186             }
187              
188             sub remove_file {
189 1     1 1 370 my ( $self, $name ) = @_;
190              
191 1         3 delete $self->{_file}->{$name};
192 1         4 return;
193             }
194              
195             sub test_file {
196 8     8 1 34 my ( $self, $name ) = @_;
197              
198 8         65 my $file = XT::Files::File->new( name => $name, is_test => 1, is_script => 1 );
199 8         65 $self->file( $name, $file );
200 8         31 return;
201             }
202              
203             #
204             # Directory
205             #
206              
207             sub bin_dir {
208 14     14 1 58 my ( $self, $name ) = @_;
209              
210 14         42 for my $file ( $self->_find_new_files($name) ) {
211 21         52 $self->bin_file( $file, $name );
212             }
213              
214 14         44 return;
215             }
216              
217             sub module_dir {
218 9     9 1 26 my ( $self, $name ) = @_;
219              
220 9         26 for my $file ( $self->_find_new_files($name) ) {
221 30 100       149 if ( $file =~ m{ [.] pm $ }xsm ) {
    100          
222 17         50 $self->module_file( $file, $name );
223             }
224             elsif ( $file =~ m{ [.] pod $ }xsm ) {
225 6         22 $self->pod_file( $file, $name );
226             }
227             }
228              
229 9         30 return;
230             }
231              
232             sub test_dir {
233 4     4 1 12 my ( $self, $name ) = @_;
234              
235 4         11 for my $file ( $self->_find_new_files($name) ) {
236 6 100       26 if ( $file =~ m{ [.] t $ }xsm ) {
237 3         12 $self->test_file( $file, $name );
238             }
239             }
240              
241 4         14 return;
242             }
243              
244             #
245             # Excludes
246             #
247              
248             sub exclude {
249 21     21 1 5438 my ( $self, $exclude ) = @_;
250              
251 21         33 push @{ $self->{_excludes} }, $exclude;
  21         52  
252 21         41 return;
253             }
254              
255             #
256             # PRIVATE METHODS
257             #
258              
259             sub _expand_config_plugin_name {
260 31     31   153 my ( $self, $plugin_name ) = @_;
261              
262 31         53 my $package_name = $plugin_name;
263 31 100       167 if ( $package_name !~ s{ ^ = }{}xsm ) {
264 17         63 $package_name = "XT::Files::Plugin::$plugin_name";
265             }
266              
267 31 100       209 $self->log_fatal("'$plugin_name' is not a valid plugin name") if $package_name !~ MODULE_NAME_RX;
268              
269 29         139 return $package_name;
270             }
271              
272             sub _find_new_files {
273 32     32   5568 my ( $self, $dir ) = @_;
274              
275 32         49 my @files;
276              
277 32 100       607 if ( !-d $dir ) {
278 10         76 $self->log_debug("Directory $dir does not exist or is not a directory");
279 10         31 return;
280             }
281              
282             File::Find::find(
283             {
284             no_chdir => 1,
285             wanted => sub {
286 97 100 100 97   3794 return if -l $File::Find::name || !-f _;
287 64         809 push @files, $File::Find::name;
288             },
289             },
290 22         1978 $dir,
291             );
292              
293 22         120 @files = grep { !exists $self->{_file}->{$_} } @files;
  64         175  
294              
295 22         73 return @files;
296             }
297              
298             sub _global_keyval { ## no critic (Subroutines::RequireFinalReturn)
299 7     7   6429 my ( $self, $key, $value ) = @_;
300              
301 7 100       18 if ( $key eq ':version' ) {
302 6 100       20 $self->log_fatal("Not a valid version '$value'") if !version::is_lax($value);
303 4 100       259 $self->log_fatal( __PACKAGE__ . " version $value required--this is only version " . __PACKAGE__->VERSION ) if version->parse( __PACKAGE__->VERSION ) < version->parse($value);
304 2         12 return;
305             }
306              
307 1         7 $self->log_fatal("Invalid entry '$key = $value'");
308             }
309              
310             sub _load_config {
311 18     18   1901 my ( $self, $config ) = @_;
312              
313 18         32 my $type_section = 1;
314 18         23 my $type_keyval = 2;
315              
316 18 100   5   474 open my $fh, '<', $config or $self->log_fatal("Cannot read file '$config': $!");
  5         35  
  5         7  
  5         33  
317              
318 16         3405 my $in_global_section = 1;
319 16         32 my @lines;
320 16         28 my $line_counter = 0;
321             LINE:
322 16         131 while ( defined( my $line = <$fh> ) ) {
323 48         75 $line_counter++;
324              
325             # skip empty lines or comment
326 48 100       182 next LINE if $line =~ m{ ^ \s* (?: [#;] | $ ) }xsm;
327              
328             # remove leading whitespace
329 40         188 $line =~ s{ ^ \s* }{}xsm;
330              
331             # remove tailing whitespace
332 40         190 $line =~ s{ \s* $ }{}xsm;
333              
334 40 100       113 if ( $line =~ m{ ^ \[ }xsm ) {
335 12 100       66 $self->log_fatal("Syntax error in config on line $line_counter") if $line !~ m{ ^ \[ ( .+ ) \] $ }xsm;
336 11         36 my $section = $1; ## no critic (RegularExpressions::ProhibitCaptureWithoutTest)
337 11         33 push @lines, [ $type_section, $line_counter, $section ];
338 11         22 $in_global_section = 0;
339 11         45 next LINE;
340             }
341              
342 28         122 my ( $key, $value ) = split /\s*=\s*/xsm, $line, 2;
343 28 100 66     196 $self->log_fatal("Syntax error in config on line $line_counter") if !defined $key || !defined $value || $key eq q{} || $value eq q{};
      100        
      100        
344             #
345 25 100       45 if ($in_global_section) {
346 3         9 $self->_global_keyval( $key, $value );
347 1         4 next LINE;
348             }
349              
350 22         131 push @lines, [ $type_keyval, $line_counter, $key, $value ];
351             }
352 10 50       59 close $fh or $self->logger->log_fatal("Cannot read file '$config': $!");
353              
354 10         32 while (@lines) {
355 11         52 my $section = ${ $lines[0] }[2];
  11         35  
356 11         21 shift @lines;
357              
358 11         25 my @keyvals;
359             my $plugin_version;
360              
361             LINE_KEYVAL:
362 11         34 while (@lines) {
363 24 100       55 last LINE_KEYVAL if $lines[0][0] == $type_section;
364              
365 22 100       45 if ( $lines[0][2] eq ':version' ) {
366 3         8 $plugin_version = $lines[0][3];
367             }
368             else {
369 19         26 push @keyvals, [ @{ $lines[0] }[ 2, 3 ] ];
  19         44  
370             }
371              
372 22         51 shift @lines;
373             }
374              
375 11         49 $self->plugin( $section, $plugin_version, \@keyvals );
376             }
377              
378 10         161 return;
379             }
380              
381             sub _load_default_config {
382 7     7   18 my ($self) = @_;
383              
384 7         12 my $config;
385             FILE:
386 7         18 for my $file ( '.xtfilesrc', 'xtfiles.ini' ) {
387 14 100       296 next FILE if !-e $file;
388 4 100       33 $self->log_fatal("Multiple default config files found: '$config' and '$file'") if defined $config;
389 3         10 $config = $file;
390             }
391              
392 6 100       27 if ( !defined $config ) {
393 4         8 my $default_config = '[Default]';
394 4         9 $config = \$default_config;
395             }
396              
397 6         26 return $self->_load_config($config);
398             }
399              
400             1;
401              
402             __END__