File Coverage

lib/XT/Files.pm
Criterion Covered Total %
statement 207 207 100.0
branch 81 82 98.7
condition 16 18 88.8
subroutine 37 37 100.0
pod 15 16 93.7
total 356 360 98.8


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