File Coverage

blib/lib/Config/JFDI.pm
Criterion Covered Total %
statement 114 118 96.6
branch 36 44 81.8
condition 13 18 72.2
subroutine 28 29 96.5
pod 8 9 88.8
total 199 218 91.2


line stmt bran cond sub pod time code
1             package Config::JFDI;
2              
3 13     13   1282061 use warnings;
  13         19  
  13         465  
4 13     13   55 use strict;
  13         17  
  13         642  
5              
6             =head1 NAME
7              
8             Config::JFDI - Just * Do it: A Catalyst::Plugin::ConfigLoader-style layer over Config::Any
9              
10             =head1 VERSION
11              
12             Version 0.063_3
13              
14             =cut
15              
16             our $VERSION = '0.063_3';
17              
18             =head1 SYNPOSIS
19              
20             use Config::JFDI;
21              
22             my $config = Config::JFDI->new(name => "my_application", path => "path/to/my/application");
23             my $config_hash = $config->get;
24              
25             This will look for something like (depending on what Config::Any will find):
26              
27             path/to/my/application/my_application_local.{yml,yaml,cnf,conf,jsn,json,...} AND
28              
29             path/to/my/application/my_application.{yml,yaml,cnf,conf,jsn,json,...}
30              
31             ... and load the found configuration information appropiately, with _local taking precedence.
32              
33             You can also specify a file directly:
34              
35             my $config = Config::JFDI->new(file => "/path/to/my/application/my_application.cnf");
36              
37             To later reload your configuration, fresh from disk:
38            
39             $config->reload;
40              
41             =head1 DESCRIPTION
42              
43             Config::JFDI is an implementation of L<Catalyst::Plugin::ConfigLoader> that exists outside of L<Catalyst>.
44              
45             Essentially, Config::JFDI will scan a directory for files matching a certain name. If such a file is found which also matches an extension
46             that Config::Any can read, then the configuration from that file will be loaded.
47              
48             Config::JFDI will also look for special files that end with a "_local" suffix. Files with this special suffix will take
49             precedence over any other existing configuration file, if any. The precedence takes place by merging the local configuration with the
50             "standard" configuration via L<Hash::Merge::Simple>.
51              
52             Finally, you can override/modify the path search from outside your application, by setting the <NAME>_CONFIG variable outside your application (where <NAME>
53             is the uppercase version of what you passed to Config::JFDI->new).
54              
55             =head1 Config::Loader
56              
57             We are currently kicking around ideas for a next-generation configuration loader. The goals are:
58              
59             * A universal platform for configuration slurping and post-processing
60             * Use Config::Any to do configuration loading
61             * A sane API so that developers can roll their own loader according to the needs of their application
62             * A friendly interface so that users can have it just DWIM
63             * Host/application/instance specific configuration via _local and %ENV
64              
65             Find more information and contribute at:
66              
67             Roadmap: L<http://sites.google.com/site/configloader/>
68              
69             Mailing list: L<http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/config-loader>
70              
71             =head1 Behavior change of the 'file' parameter in 0.06
72              
73             In previous versions, Config::JFDI would treat the file parameter as a path parameter, stripping off the extension (ignoring it) and globbing what remained against all the extensions that Config::Any could provide. That is, it would do this:
74              
75             Config::JFDI->new( file => 'xyzzy.cnf' );
76             # Transform 'xyzzy.cnf' into 'xyzzy.pl', 'xyzzy.yaml', 'xyzzy_local.pl', ... (depending on what Config::Any could parse)
77              
78             This is probably not what people intended. Config::JFDI will now squeak a warning if you pass 'file' through, but you can suppress the warning with 'no_06_warning' or 'quiet_deprecation'
79              
80             Config::JFDI->new( file => 'xyzzy.cnf', no_06_warning => 1 );
81             Config::JFDI->new( file => 'xyzzy.cnf', quiet_deprecation => 1 ); # More general
82              
83             If you *do* want the original behavior, simply pass in the file parameter as the path parameter instead:
84              
85             Config::JFDI->new( path => 'xyzzy.cnf' ); # Will work as before
86              
87             =head1 METHODS
88              
89             =cut
90              
91 13     13   6809 use Moose;
  13         4601490  
  13         85  
92              
93 13     13   78192 use Config::JFDI::Source::Loader;
  13         43  
  13         597  
94 13     13   6154 use Config::JFDI::Carp;
  13         32  
  13         48  
95              
96 13     13   5583 use Path::Class;
  13         328814  
  13         882  
97 13     13   106 use Config::Any;
  13         35  
  13         305  
98 13     13   6660 use Hash::Merge::Simple;
  13         5501  
  13         576  
99 13     13   72 use Sub::Install;
  13         17  
  13         127  
100 13     13   6788 use Data::Visitor::Callback;
  13         835395  
  13         531  
101 13     13   6940 use Clone qw//;
  13         29712  
  13         15051  
102              
103             has package => qw/is ro isa Str/;
104              
105             has source => qw/is ro/, handles => [qw/ driver local_suffix no_env env_lookup path found /];
106              
107             #has driver => qw/is ro lazy_build 1/;
108             #sub _build_driver {
109             # return {};
110             #}
111              
112             #has local_suffix => qw/is ro required 1 lazy 1 default local/;
113              
114             #has no_env => qw/is ro required 1/, default => 0;
115              
116             #has env_lookup => qw/is ro/, default => sub { [] };
117              
118             has load_once => qw/is ro required 1/, default => 1;
119              
120             has loaded => qw/is ro required 1/, default => 0;
121              
122             has substitution => qw/reader _substitution lazy_build 1 isa HashRef/;
123             sub _build_substitution {
124 19     19   813 return {};
125             }
126              
127             has default => qw/is ro lazy_build 1 isa HashRef/;
128             sub _build_default {
129 21     21   569 return {};
130             }
131              
132             has path_to => qw/reader _path_to lazy_build 1 isa Str/;
133             sub _build_path_to {
134 5     5   8 my $self = shift;
135 5 100       29 return $self->config->{home} if $self->config->{home};
136 2 50       48 return $self->source->path unless $self->source->path_is_file;
137 0         0 return '.';
138             }
139              
140             has _config => qw/is rw isa HashRef/;
141              
142             =head2 $config = Config::JFDI->new(...)
143              
144             You can configure the $config object by passing the following to new:
145              
146             name The name specifying the prefix of the configuration file to look for and
147             the ENV variable to read. This can be a package name. In any case,
148             :: will be substituted with _ in <name> and the result will be lowercased.
149              
150             To prevent modification of <name>, pass it in as a scalar reference.
151              
152             path The directory to search in
153              
154             file Directly read the configuration from this file. Config::Any must recognize
155             the extension. Setting this will override path
156              
157             no_local Disable lookup of a local configuration. The 'local_suffix' option will be ignored. Off by default
158              
159             local_suffix The suffix to match when looking for a local configuration. "local" By default
160             ("config_local_suffix" will also work so as to be drop-in compatible with C::P::CL)
161              
162             no_env Set this to 1 to disregard anything in the ENV. The 'env_lookup' option will be ignored. Off by default
163              
164             env_lookup Additional ENV to check if $ENV{<NAME>...} is not found
165              
166             driver A hash consisting of Config:: driver information. This is passed directly through
167             to Config::Any
168              
169             install_accessor Set this to 1 to install a Catalyst-style accessor as <name>::config
170             You can also specify the package name directly by setting install_accessor to it
171             (e.g. install_accessor => "My::Application")
172              
173             substitute A hash consisting of subroutines called during the substitution phase of configuration
174             preparation. ("substitutions" will also work so as to be drop-in compatible with C::P::CL)
175             A substitution subroutine has the following signature: ($config, [ $argument1, $argument2, ... ])
176              
177             path_to The path to dir to use for the __path_to(...)__ substitution. If nothing is given, then the 'home'
178             config value will be used ($config->get->{home}). Failing that, the current directory will be used.
179              
180             default A hash filled with default keys/values
181              
182             Returns a new Config::JFDI object
183              
184             =cut
185              
186             sub BUILD {
187 23     23 0 59965 my $self = shift;
188 23         51 my $given = shift;
189              
190 23 50 66     289 $self->{package} = $given->{name} if defined $given->{name} && ! defined $self->{package} && ! ref $given->{name};
      66        
191              
192 23         38 my ($source, %source);
193 23 100       146 if ($given->{file}) {
194             carp "The behavior of the 'file' option has changed, pass in 'quiet_deprecation' or 'no_06_warning' to disable this warning"
195 7 50 66     41 unless $given->{quiet_deprecation} || $given->{no_06_warning};
196 7 50       25 carp "Warning, overriding path setting with file (\"$given->{file}\" instead of \"$given->{path}\")" if $given->{path};
197 7         19 $given->{path} = $given->{file};
198 7         25 $source{path_is_file} = 1;
199             }
200              
201             {
202 23         36 for (qw/
  23         67  
203             name
204             path
205             driver
206              
207             no_local
208             local_suffix
209              
210             no_env
211             env_lookup
212              
213             /) {
214 161 100       350 $source{$_} = $given->{$_} if exists $given->{$_};
215             }
216              
217 23 50       82 carp "Warning, 'local_suffix' will be ignored if 'file' is given, use 'path' instead" if exists $source{local_suffix};
218              
219 23 50       72 $source{local_suffix} = $given->{config_local_suffix} if $given->{config_local_suffix};
220              
221 23         293 $source = Config::JFDI::Source::Loader->new( %source );
222             }
223              
224 23         224 $self->{source} = $source;
225              
226 23         70 for (qw/substitute substitutes substitutions substitution/) {
227 89 100       225 if ($given->{$_}) {
228 1         3 $self->{substitution} = $given->{$_};
229 1         2 last;
230             }
231             }
232              
233 23 100       129 if (my $package = $given->{install_accessor}) {
234 2 100       40 $package = $self->package if $package eq 1;
235             Sub::Install::install_sub({
236             code => sub {
237 4     4   775 return $self->config;
238             },
239 2         29 into => $package,
240             as => "config"
241             });
242              
243             }
244             }
245              
246             =head2 $config_hash = Config::JFDI->open( ... )
247              
248             As an alternative way to load a config, ->open will pass given arguments to ->new( ... ), then attempt to do ->load
249              
250             Unlike ->get or ->load, if no configuration files are found, ->open will return undef (or the empty list)
251              
252             This is so you can do something like:
253              
254             my $config_hash = Config::JFDI->open( "/path/to/application.cnf" ) or croak "Couldn't find config file!"
255              
256             In scalar context, ->open will return the config hash, NOT the config object. If you want the config object, call ->open in list context:
257              
258             my ($config_hash, $config) = Config::JFDI->open( ... )
259              
260             You can pass any arguments to ->open that you would to ->new
261              
262             =head2 $config->get
263              
264             =head2 $config->config
265              
266             =head2 $config->load
267              
268             Load a config as specified by ->new( ... ) and ENV and return a hash
269              
270             These will only load the configuration once, so it's safe to call them multiple times without incurring any loading-time penalty
271              
272             =head2 $config->found
273              
274             Returns a list of files found
275              
276             If the list is empty, then no files were loaded/read
277              
278             =cut
279              
280             sub open {
281 9 100   9 1 4812 if ( ! ref $_[0] ) {
282 4         10 my $class = shift;
283 4 100       42 return $class->new( no_06_warning => 1, 1 == @_ ? (file => $_[0]) : @_ )->open;
284             }
285 5         11 my $self = shift;
286 5 100       31 carp "You called ->open on an instantiated object with arguments" if @_;
287 5 100       293 return unless $self->found;
288 4 100       44 return wantarray ? ($self->get, $self) : $self->get;
289             }
290              
291             sub get {
292 66     66 1 3312 my $self = shift;
293              
294 66         175 my $config = $self->config;
295 66         538 return $config;
296             # TODO Expand to allow dotted key access (?)
297             }
298              
299             sub config {
300 124     124 1 194 my $self = shift;
301              
302 124 100       4236 return $self->_config if $self->loaded;
303 22         91 return $self->load;
304             }
305              
306             sub load {
307 23     23 1 50 my $self = shift;
308              
309 23 50 33     631 if ($self->loaded && $self->load_once) {
310 0         0 return $self->get;
311             }
312              
313 23         700 $self->_config($self->default);
314              
315             {
316 23         27 my @read = $self->source->read;
  23         718  
317              
318 23         220 $self->_load($_) for @read;
319             }
320              
321 23         1351 $self->{loaded} = 1;
322              
323             {
324 23         39 my $visitor = Data::Visitor::Callback->new(
325             plain_value => sub {
326 250 50   250   90345 return unless defined $_;
327 250         615 $self->substitute($_);
328             }
329 23         1681 );
330 23         10223 $visitor->visit( $self->config );
331              
332             }
333              
334 23         4966 return $self->config;
335             }
336              
337             =head2 $config->clone
338              
339             Return a clone of the configuration hash using L<Clone>
340              
341             This will load the configuration first, if it hasn't already
342              
343             =cut
344              
345             sub clone {
346 0     0 1 0 my $self = shift;
347 0         0 return Clone::clone($self->config);
348             }
349              
350             =head2 $config->reload
351              
352             Reload the configuration, examining ENV and scanning the path anew
353              
354             Returns a hash of the configuration
355              
356             =cut
357              
358             sub reload {
359 1     1 1 2 my $self = shift;
360 1         2 $self->{loaded} = 0;
361 1         4 return $self->load;
362             }
363              
364             =head2 $config->substitute( <value>, <value>, ... )
365              
366             For each given <value>, if <value> looks like a substitution specification, then run
367             the substitution macro on <value> and store the result.
368              
369             There are three default substitutions (the same as L<Catalyst::Plugin::ConfigLoader>)
370              
371             =over 4
372              
373             =item * C<__HOME__> - replaced with C<$c-E<gt>path_to('')>
374              
375             =item * C<__path_to(foo/bar)__> - replaced with C<$c-E<gt>path_to('foo/bar')>
376              
377             =item * C<__literal(__FOO__)__> - leaves __FOO__ alone (allows you to use
378             C<__DATA__> as a config value, for example)
379              
380             =back
381              
382             The parameter list is split on comma (C<,>).
383              
384             You can define your own substitutions by supplying the substitute option to ->new
385              
386             =cut
387              
388             sub substitute {
389 250     250 1 264 my $self = shift;
390              
391 250         7964 my $substitution = $self->_substitution;
392 250   100 2   745 $substitution->{ HOME } ||= sub { shift->path_to( '' ); };
  2         9  
393 250   100 5   932 $substitution->{ path_to } ||= sub { shift->path_to( @_ ); };
  5         25  
394 250   100 8   526 $substitution->{ literal } ||= sub { return $_[ 1 ]; };
  8         53  
395 250         713 my $matcher = join( '|', keys %$substitution );
396              
397 250         429 for ( @_ ) {
398 250 100       2273 s{__($matcher)(?:\((.+?)\))?__}{ $substitution->{ $1 }->( $self, $2 ? split( /,/, $2 ) : () ) }eg;
  18         202  
399             }
400             }
401              
402             sub path_to {
403 7     7 1 13 my $self = shift;
404 7         18 my @path = @_;
405              
406 7         276 my $path_to = $self->_path_to;
407              
408 7         115 my $path = Path::Class::Dir->new( $path_to, @path );
409 7 100       915 if ( -d $path ) {
410 1         39 return $path;
411             }
412             else {
413 6         404 return Path::Class::File->new( $path_to, @path );
414             }
415             }
416              
417             sub _load {
418 26     26   469 my $self = shift;
419 26         49 my $cfg = shift;
420              
421 26         82 my ($file, $hash) = %$cfg;
422              
423 26         1014 $self->{_config} = Hash::Merge::Simple->merge($self->_config, $hash);
424             }
425              
426             =head1 AUTHOR
427              
428             Robert Krimen, C<< <rkrimen at cpan.org> >>
429              
430             =head1 SEE ALSO
431              
432             L<Catalyst::Plugin::ConfigLoader>, L<Config::Any>, L<Catalyst>
433              
434             =head1 SOURCE
435              
436             You can contribute or fork this project via GitHub:
437              
438             L<http://github.com/robertkrimen/config-jfdi/tree/master>
439              
440             git clone git://github.com/robertkrimen/config-jfdi.git PACKAGE
441              
442             =head1 BUGS
443              
444             Please report any bugs or feature requests to C<bug-config-jfdi at rt.cpan.org>, or through
445             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Config-JFDI>. I will be notified, and then you'll
446             automatically be notified of progress on your bug as I make changes.
447              
448              
449              
450              
451             =head1 SUPPORT
452              
453             You can find documentation for this module with the perldoc command.
454              
455             perldoc Config::JFDI
456              
457              
458             You can also look for information at:
459              
460             =over 4
461              
462             =item * RT: CPAN's request tracker
463              
464             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Config-JFDI>
465              
466             =item * AnnoCPAN: Annotated CPAN documentation
467              
468             L<http://annocpan.org/dist/Config-JFDI>
469              
470             =item * CPAN Ratings
471              
472             L<http://cpanratings.perl.org/d/Config-JFDI>
473              
474             =item * Search CPAN
475              
476             L<http://search.cpan.org/dist/Config-JFDI>
477              
478             =back
479              
480              
481             =head1 ACKNOWLEDGEMENTS
482              
483              
484             =head1 COPYRIGHT & LICENSE
485              
486             Copyright 2008 Robert Krimen, all rights reserved.
487              
488             This program is free software; you can redistribute it and/or modify it
489             under the same terms as Perl itself.
490              
491              
492             =cut
493              
494             1; # End of Config::JFDI