File Coverage

blib/lib/Config/YAML/Modern.pm
Criterion Covered Total %
statement 128 134 95.5
branch 25 26 96.1
condition 12 18 66.6
subroutine 19 24 79.1
pod 14 14 100.0
total 198 216 91.6


line stmt bran cond sub pod time code
1             package Config::YAML::Modern;
2              
3 5     5   185507 use 5.008;
  5         23  
  5         274  
4 5     5   37 use strict;
  5         12  
  5         371  
5 5     5   32 use warnings;
  5         14  
  5         821  
6              
7             =head1 NAME
8              
9             Config::YAML::Modern - Modern YAML-based config loader from file or directory.
10              
11             =head1 VERSION
12              
13             Version 0.36
14              
15             =cut
16              
17             our $VERSION = '0.36';
18             $VERSION = eval $VERSION;
19              
20             # develop mode only
21             # use Smart::Comments;
22              
23             # die beautiful
24 5     5   34 use Carp qw/croak/;
  5         10  
  5         457  
25              
26             # too match for directory-based loader
27 5     5   41 use File::Basename qw/dirname fileparse/;
  5         183  
  5         356  
28 5     5   88 use File::Spec;
  5         10  
  5         136  
29 5     5   28 use File::Glob qw/:glob/;
  5         17  
  5         1787  
30              
31             # srsly who care about your YAML lib :) I`nt!
32 5     5   9920 use YAML::Any qw/LoadFile/;
  5         6720  
  5         31  
33              
34             # its for correct hash creation + for data mining
35 5     5   90813 use Data::Diver qw/DiveVal DiveDie Dive/;
  5         5970  
  5         421  
36              
37             # so, its smartest way for Merge hash
38 5     5   10939 use Hash::Merge;
  5         13978  
  5         10104  
39              
40             =head1 SYNOPSIS
41              
42             Config::YAML::Modern created to get dial with yaml-based configuration.
43              
44             Its possible to load single file, or all files in one directory (without recursion search).
45              
46             Data from many files was be merged properly (almost), also filename was be converted
47             to top-level hash keys.
48              
49             Filename like 'file.data.yaml' was be converted to { file => { data => $file_content } }.
50              
51             Also module provide perfect dive() interface form Data::Diver.
52              
53             It may be used like
54              
55             my $file_content = $conf_object->dive(qw/file data/);
56            
57              
58             Simply usage for file load
59              
60             use Config::YAML::Modern;
61              
62             my $config = Config::YAML::Modern->new();
63            
64             my $filename = 'test.yaml';
65            
66             $config->load_file($filename);
67            
68             my $data = $config->config();
69              
70              
71             More complicated for directory-based loading
72              
73             my $config2 = Config::YAML::Modern->new( key_conversion => 'ucfirst' );
74            
75             my $directory = '/etc/my_app/';
76            
77             # slurp all data to hashref
78             my $data2 = $config2->load_dir($directory)->config();
79            
80             # but exist more sophisticated path
81             my @list_of_key = (qw/Model Message 0 author/);
82             my $data3 = $config2->dive(@list_of_key);
83            
84             # $data3 == $data2->{Model}{Message}[0]{author}
85              
86              
87             =cut
88              
89             # our error text for sprintf
90             my $err_text = [
91             qq( filename is required ),
92             qq( file |%s| is not exists ),
93             qq( dont know |%s| conversion ),
94             qq( error on parsing file |%s| with message: %s ),
95             qq( directory name is required ),
96             qq( directory |%s| is not exists ),
97             qq( suffix is required, or you must set 'i_dont_use_suffix property' ),
98             qq( no one file matched with |%s| pattern at |%s| directory ),
99             qq( call with empty args deprecated ),
100             qq( only hashref are allowed ),
101              
102             ];
103              
104             # its our private subs
105             my ( $key_conversion, $get_files_list );
106              
107             =head1 SUBROUTINES/METHODS
108              
109             =cut
110              
111             =head2 new
112              
113             new( [ list of args ] ) - create Config::YAML::Modern object and return it.
114              
115             my $config = Config::YAML::Modern->new();
116              
117             The options currently supported are:
118              
119             =over 4
120              
121             =item * C
122             behavior on merge data, see L docs.
123              
124             Available values are [LEFT_PRECEDENT, RIGHT_PRECEDENT, STORAGE_PRECEDENT, RETAINMENT_PRECEDENT],
125             'LEFT_PRECEDENT' by default.
126              
127             =item * C
128             File suffix, used in search files in directory for matching. '.yaml' by default.
129              
130             =item * C
131             Rule for conversion parts of filename to hash keys.
132              
133             Available values are [undef, uc, ucfirst, lc, lcfirst]. No conversion - 'undef' by default.
134              
135             =item * C
136             Set to true if you not use suffix on config files. Suffix is used by default - 'undef'.
137              
138             =item * C<__force_return_data>
139             If setted to true, methods: load_file(), load_dir(), add_hash(), add_file() and add_dir()
140             returns dataset instead of $self, returned by default - 'undef'.
141              
142             !!! important - in this case loaded or added data are NOT BE STORED in object, use it well
143              
144             =item * C
145             If setted to true method:
146              
147             - load_file() will return or assign to object empty flat hash without created keys by file name - just {}.
148              
149             - load_dir() will ignore empty files and not been add keys by names of empty files at all
150              
151             - add_file() and add_dir() will ignore empty files and not use it in merge process
152              
153             By default empty files NOT ignored, value by default - 'undef'.
154              
155             =back
156              
157             =cut
158              
159             sub new {
160              
161 14     14 1 4306 my $class = shift;
162 14         147 my $arg = {
163             __config => {},
164             merge_behavior => 'LEFT_PRECEDENT',
165             file_suffix => '.yaml',
166             key_conversion => undef,
167             i_dont_use_suffix => undef,
168             __force_return_data => undef,
169             ignore_empty_file => undef,
170             @_
171             };
172              
173 14   33     120 my $self = bless( $arg, ref $class || $class );
174              
175 14         50 return $self;
176             }
177              
178             =head2 load_file
179              
180             load_file($filename) - load data from yaml-contained file
181              
182             $config->load_file($filename);
183              
184             =cut
185              
186             sub load_file {
187 33     33 1 3949 my $self = shift;
188 33         85 my $filename = shift;
189              
190 33 100       131 unless ( defined $filename ) {
191 1         181 croak sprintf $err_text->[0];
192             }
193              
194 32 100       1008 unless ( -e $filename ) {
195 1         187 croak sprintf $err_text->[1], $filename;
196             }
197              
198             # this block for filename to hash key resolving
199             # et my.config.yaml -> { my => { config => { $data_here } } }
200 31         1706 my ( $filename_for_hash, undef, $suffix ) =
201             fileparse( $filename, qr/\.[^.]*/ );
202 31         237 my @file_part = split m/\./, $filename_for_hash;
203              
204             # I care about all of you, but it bad practice!!!
205 31 100       151 if ( defined $self->{'i_dont_use_suffix'} ) {
206 26         102 $suffix =~ s/^\.//;
207              
208             # fix empty key addition
209 26 100       127 push @file_part, $suffix if ( $suffix ne '' );
210             }
211              
212             # if we are need key conversation
213 31         112 my $key_conv = $self->{key_conversion};
214 31 50       155 @file_part = $key_conversion->( $key_conv, @file_part )
215             if ( defined $key_conv );
216              
217             # now we are go to load file
218 31         79 my $config_value = {};
219 31         45 my $temp_val;
220              
221 31         48 eval { $temp_val = LoadFile($filename) };
  31         277  
222              
223 31         257906 croak sprintf $err_text->[3], $filename, $@ while ($@);
224              
225 31         266 DiveVal( $config_value, @file_part ) = $temp_val;
226              
227             # return empty hash if file empty to suppress vanish data by empty file
228 31 100 100     4469 if ( !defined $temp_val && defined $self->{'ignore_empty_file'} ) {
229              
230 6         13 $config_value = {};
231             }
232              
233             # for dir_load, or you are may use it, if you want
234 31         249 return $config_value while ( defined $self->{__force_return_data} );
235              
236             # or get classical $self for chaining
237 11         30 $self->{'__config'} = $config_value;
238 11         101 return $self;
239              
240             }
241              
242             =head2 load_dir
243              
244             load_dir($directory) - get files from directory (non-recursive), load data and merge it together
245              
246             $config2->load_dir($directory);
247              
248             =cut
249              
250             sub load_dir {
251 8     8 1 1973 my $self = shift;
252 8         18 my $dir = shift;
253              
254 8 100       29 unless ( defined $dir ) {
255 1         450 croak sprintf $err_text->[4];
256             }
257              
258 7 100       201 unless ( -d $dir ) {
259 1         102 croak sprintf $err_text->[5], $dir;
260             }
261              
262 6         25 my @file_list = $get_files_list->( $self, $dir );
263              
264             # its hack, but I`m not shined
265 5         16 my $return_data_flag = $self->{'__force_return_data'};
266 5         11 $self->{'__force_return_data'} = 1;
267              
268             #ok, little-by-little take our config
269 5         9 my %result;
270              
271             # LEFT_PRECEDENT is almost right way
272 5         53 my $merger = Hash::Merge->new( $self->{'merge_behavior'} );
273              
274 5         99 foreach my $full_filename (@file_list) {
275              
276 17         1125 my $temp_val = $self->load_file($full_filename);
277              
278             # just ignore empty files
279             next
280 17 100 66     112 if (!scalar keys %$temp_val
281             && defined $self->{'ignore_empty_file'} );
282              
283             # make smart deep merge
284 13         25 %result = %{ $merger->merge( \%result, $temp_val ) };
  13         84  
285              
286             }
287              
288             # change it back
289 5         471 $self->{'__force_return_data'} = $return_data_flag;
290              
291             # you are may use it, if you want
292 5         30 return \%result while ( defined $self->{__force_return_data} );
293              
294             # or get classical $self for chaining
295 3         8 $self->{'__config'} = \%result;
296 3         61 return $self;
297             }
298              
299             =head2 add_hash
300              
301             add_hash($hash_ref, $behavior? ) - add data to object from hash with $behavior resolution, or use default behavior.
302              
303             my $data3 = $config2->add_hash( $hash_ref, 'RIGHT_PRECEDENT' );
304              
305             Just wrapper ontop of L
306              
307             =cut
308              
309             sub add_hash {
310 8     8 1 4570 my $self = shift;
311 8         18 my $hash_ref = shift;
312 8         169 my $behavior = shift;
313              
314 8 100       2232 croak sprintf $err_text->[8] unless ( defined $hash_ref );
315              
316 7 100       149 croak sprintf $err_text->[9] unless ( ref $hash_ref eq 'HASH' );
317              
318             # LEFT_PRECEDENT is almost right way
319 6   66     69 my $merger = Hash::Merge->new( $behavior || $self->{'merge_behavior'} );
320              
321             # make smart deep merge
322 6         98 my %result = %{ $merger->merge( $self->{'__config'}, $hash_ref ) };
  6         26  
323              
324             # you are may use it, if you want
325 6         1069 return \%result while ( defined $self->{__force_return_data} );
326              
327             # or get classical $self for chaining
328 4         10 $self->{'__config'} = \%result;
329 4         23 return $self;
330              
331             }
332              
333             =head2 add_file
334              
335             add_file($filename, $behavior? ) - add data to object from file with $behavior resolution, or use default behavior.
336              
337             my $data3 = $config2->add_file( $filename3, 'RIGHT_PRECEDENT' );
338              
339             =cut
340              
341             sub add_file {
342 2     2 1 12 my $self = shift;
343 2         4 my $filename = shift;
344 2         5 my $behavior = shift;
345              
346             # its hack, but I`m not shined
347 2         8 my $return_data_flag = $self->{'__force_return_data'};
348 2         6 $self->{'__force_return_data'} = 1;
349              
350 2         6 my $result;
351 2         9 my $temp_val = $self->load_file($filename);
352              
353             # just ignore empty files
354 2 100 66     23 if ( !scalar keys %$temp_val && defined $self->{'ignore_empty_file'} ) {
355 1         4 $result = $self->{'__config'};
356             }
357             else {
358 1         8 $result = $self->add_hash( $temp_val, $behavior );
359             }
360              
361             # change it back
362 2         6 $self->{'__force_return_data'} = $return_data_flag;
363              
364             # you are may use it, if you want
365 2         15 return $result while ( defined $self->{__force_return_data} );
366              
367             # or get classical $self for chaining
368 2         6 $self->{'__config'} = $result;
369 2         15 return $self;
370              
371             }
372              
373             =head2 add_dir
374              
375             add_dir($dir_name, $behavior? ) - add data to object from directory with $behavior resolution, or use default behavior.
376              
377             my $data3 = $config2->add_dir( $dir_name2, 'RETAINMENT_PRECEDENT' );
378              
379             =cut
380              
381             sub add_dir {
382 2     2 1 4 my $self = shift;
383 2         5 my $dir_name = shift;
384 2         5 my $behavior = shift;
385              
386             # its hack, but I`m not shined
387 2         6 my $return_data_flag = $self->{'__force_return_data'};
388 2         5 $self->{'__force_return_data'} = 1;
389              
390 2         5 my $result;
391 2         9 my $temp_val = $self->load_dir($dir_name);
392              
393             # just ignore empty files
394 2 100 66     17 if ( !scalar keys %$temp_val && defined $self->{'ignore_empty_file'} ) {
395              
396 1         4 $result = $self->{'__config'};
397             }
398             else {
399 1         5 $result = $self->add_hash( $temp_val, $behavior );
400             }
401              
402             # change it back
403 2         7 $self->{'__force_return_data'} = $return_data_flag;
404              
405             # you are may use it, if you want
406 2         9 return $result while ( defined $self->{__force_return_data} );
407              
408             # or get classical $self for chaining
409 2         6 $self->{'__config'} = $result;
410 2         12 return $self;
411              
412             }
413              
414             =head2 dive
415              
416             dive(@list_of_key) - return data from object by @list_of_key patch resolution, return "undef" if path resolution wrong.
417              
418             my $data3 = $config2->dive(@list_of_key);
419              
420             Just wrapper ontop of L
421              
422             =cut
423              
424             sub dive {
425 11     11 1 2434 my $self = shift;
426 11         30 my @list_of_key = @_;
427              
428 11         272 croak sprintf $err_text->[8] while ( $#list_of_key < 0 );
429              
430 10         49 my $value = Dive( $self->{'__config'}, @list_of_key );
431              
432 10         518 return $value;
433             }
434              
435             =head2 dive_die
436              
437             dive_die(@list_of_key) - return data from object by @list_of_key patch resolution, and do "die" if path resolution wrong.
438              
439             my $data3 = $config2->dive_die(@list_of_key);
440              
441             Just wrapper ontop of L
442              
443             =cut
444              
445             sub dive_die {
446 1     1 1 2 my $self = shift;
447 1         4 my @list_of_key = @_;
448              
449 1         7 croak sprintf $err_text->[8] while ( $#list_of_key < 0 );
450              
451 1         6 my $value = DiveDie( $self->{'__config'}, @list_of_key );
452              
453 0         0 return $value;
454             }
455              
456             =head2 config
457              
458             config() - return all config data from object
459              
460             my $data = $config->config();
461              
462             =cut
463              
464             sub config {
465 13     13 1 1074 my $self = shift;
466 13         80 return $self->{'__config'};
467             }
468              
469             #=======
470             # internal functions
471             #=======
472              
473             =begin comment key_conversion
474              
475             subroutine for convert filepart
476              
477             =end comment
478              
479             =cut
480              
481             $key_conversion = sub {
482              
483             my $key_conv = shift;
484             my @part_in = @_;
485             my @part_out;
486              
487             # yes! it`s noisy and ugly, get 5.14 and it will by pretty
488             if ( $key_conv eq 'uc' ) {
489             @part_out = map { uc $_ } @part_in;
490             }
491             elsif ( $key_conv eq 'ucfirst' ) {
492             @part_out = map { ucfirst $_ } @part_in;
493             }
494             elsif ( $key_conv eq 'lc' ) {
495             @part_out = map { lc $_ } @part_in;
496             }
497             elsif ( $key_conv eq 'lcfirst' ) { # ok, but why???
498             @part_out = map { lcfirst $_ } @part_in;
499             }
500             else { # add another one by yourself or get error
501             croak sprintf $err_text->[2], $key_conv;
502             }
503              
504             return @part_out;
505             };
506              
507             =begin comment get_files_list
508              
509             subroutine for get all files from directory
510              
511             =end comment
512              
513             =cut
514              
515             $get_files_list = sub {
516             my $self = shift;
517             my $dir = shift;
518              
519             my $glob = '*.';
520              
521             if ( !defined $self->{'i_dont_use_suffix'} ) {
522              
523             croak sprintf $err_text->[6] unless ( defined $self->{'file_suffix'} );
524              
525             # just throw out the dot
526             my ($suffix) = $self->{'file_suffix'} =~ /([^.]+)$/;
527             $glob .= $suffix;
528              
529             }
530             else {
531             $glob = '*'; # yes, just '*' pattern
532             }
533              
534             my $full_pattern = File::Spec->catfile( $dir, $glob );
535              
536             # get all files in our dir
537             # REMEMBER!! no recursive search and e.t.c. - just plain dir scan!!!
538             my @file_list = bsd_glob( $full_pattern, GLOB_MARK );
539              
540             # so, we are must filter directory in this case
541             if ( defined $self->{'i_dont_use_suffix'} ) {
542             @file_list = grep { !m'/$' } @file_list;
543             }
544              
545             croak sprintf $err_text->[7], $glob, $dir while ( $#file_list < 0 );
546              
547             return @file_list;
548              
549             };
550              
551             =head1 DEPRECATED METHODS
552              
553             The old module interface is still available, but its use is discouraged. It will eventually be removed from the module.
554              
555             =cut
556              
557             =head2 file_load
558              
559             file_load($filename) - load data from yaml-contained file
560              
561             $config->file_load($filename);
562              
563             =cut
564              
565             sub file_load {
566 0     0 1   goto &load_file;
567             }
568              
569             =head2 dir_load
570              
571             dir_load($directory) - get files from directory, load data and merge it together
572              
573             $config2->dir_load($directory);
574              
575             =cut
576              
577             sub dir_load {
578 0     0 1   goto &load_dir;
579             }
580              
581             =head2 hash_add
582              
583             hash_add($hash_ref, $behavior? ) - add data to object from hash with $behavior resolution, or use default behavior.
584              
585             my $data3 = $config2->hash_add( $hash_ref, 'RIGHT_PRECEDENT' );
586              
587             Just wrapper ontop of L
588              
589             =cut
590              
591             sub hash_add {
592 0     0 1   goto &add_hash;
593             }
594              
595             =head2 file_add
596              
597             file_add($filename, $behavior? ) - add data to object from file with $behavior resolution, or use default behavior.
598              
599             my $data3 = $config2->file_add( $filename3, 'RIGHT_PRECEDENT' );
600              
601             =cut
602              
603             sub file_add {
604 0     0 1   goto &add_file;
605             }
606              
607             =head2 dir_add
608              
609             file_add($dir_name, $behavior? ) - add data to object from directory with $behavior resolution, or use default behavior.
610              
611             my $data3 = $config2->dir_add( $dir_name2, 'RETAINMENT_PRECEDENT' );
612              
613             =cut
614              
615             sub dir_add {
616 0     0 1   goto &add_dir;
617             }
618              
619             =head1 EXPORT
620              
621             Nothing by default.
622              
623             =head1 AUTHOR
624              
625             Meettya, C<< >>
626              
627             =head1 BUGS
628              
629             Please report any bugs or feature requests to C, or through
630             the web interface at L. I will be notified, and then you'll
631             automatically be notified of progress on your bug as I make changes.
632              
633             =head1 DEVELOPMENT
634              
635             =head2 Repository
636              
637             https://github.com/Meettya/Config-YAML-Modern
638              
639             =head1 SUPPORT
640              
641             You can find documentation for this module with the perldoc command.
642              
643             perldoc Config::YAML::Modern
644              
645             You can also look for information at:
646              
647             =over 4
648              
649             =item * RT: CPAN's request tracker (report bugs here)
650              
651             L
652              
653             =item * AnnoCPAN: Annotated CPAN documentation
654              
655             L
656              
657             =item * CPAN Ratings
658              
659             L
660              
661             =item * Search CPAN
662              
663             L
664              
665             =back
666              
667              
668             =head1 ACKNOWLEDGEMENTS
669              
670              
671             =head1 LICENSE AND COPYRIGHT
672              
673             Copyright 2011 Meettya.
674              
675             This program is free software; you can redistribute it and/or modify it
676             under the terms of either: the GNU General Public License as published
677             by the Free Software Foundation; or the Artistic License.
678              
679             See http://dev.perl.org/licenses/ for more information.
680              
681              
682             =cut
683              
684             1; # End of Config::YAML::Modern