File Coverage

blib/lib/Config/Path.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Config::Path;
2 6     6   308617 use Moose;
  0            
  0            
3              
4             our $VERSION = '0.12';
5              
6             use Config::Any;
7             use Hash::Merge;
8              
9              
10              
11             has '_config' => (
12             is => 'ro',
13             isa => 'HashRef',
14             lazy_build => 1,
15             clearer => 'reload'
16             );
17              
18              
19             has 'config_options' => (
20             is => 'ro',
21             isa => 'HashRef',
22             default => sub { {
23             flatten_to_hash => 1,
24             use_ext => 1
25             } }
26             );
27              
28              
29             has 'directory' => (
30             is => 'ro',
31             isa => 'Str',
32             predicate => 'has_directory'
33             );
34              
35              
36             has 'files' => (
37             traits => [ qw(Array) ],
38             is => 'ro',
39             isa => 'ArrayRef',
40             predicate => 'has_files',
41             handles => {
42             add_file => 'push'
43             }
44             );
45              
46             has '_mask' => (
47             is => 'rw',
48             isa => 'HashRef',
49             predicate => 'has_mask',
50             clearer => 'clear_mask'
51             );
52              
53              
54             has 'convert_empty_to_undef' => (
55             is => 'ro',
56             isa => 'Bool',
57             default => 1
58             );
59              
60             sub BUILD {
61             my ($self) = @_;
62              
63             if($self->has_directory && $self->has_files) {
64             die "directory and files are mutually exclusive, choose one"
65             }
66              
67             unless($self->has_directory || $self->has_files) {
68             die "One of directory or files must be specified"
69             }
70             }
71              
72             sub _build__config {
73             my ($self) = @_;
74              
75             # This might be undef, but that's ok. We'll check later.
76             my $files = $self->files;
77              
78             # Check for a directory
79             if($self->has_directory) {
80             my $dir = $self->directory;
81              
82             unless(-d $dir) {
83             die "Can't open directory: $dir";
84             }
85              
86             opendir(my $dh, $dir);
87             my @files = sort(map("$dir/$_", grep { $_ !~ /^\./ && -f "$dir/$_" } readdir($dh)));
88             closedir($dh);
89              
90             $files = \@files;
91             }
92              
93             if(!defined($files) || scalar(@{ $files }) < 1) {
94             warn "No files found.";
95             }
96              
97             my $anyconf = Config::Any->load_files({ %{ $self->config_options }, files => $files });
98              
99             my $config = ();
100             my $merge = Hash::Merge->new('RIGHT_PRECEDENT');
101             foreach my $file (@{ $files }) {
102             # Double check that it exists, as Config::Any might not have loaded it
103             next unless exists $anyconf->{$file};
104             next unless defined $anyconf->{$file};
105             $config = $merge->merge($config, $anyconf->{$file});
106             }
107             if(defined($config)) {
108             return $config;
109             }
110              
111             return {};
112             }
113              
114              
115             sub fetch {
116             my ($self, $path) = @_;
117              
118             # Check the mask first to see if the path we've been given has been
119             # overriden.
120             if($self->has_mask) {
121             # Use exists just in case they set the value to undef.
122             return $self->_mask->{$path} if exists($self->_mask->{$path});
123             }
124              
125             my $conf = $self->_config;
126              
127             # you should be able to pass nothing and get a hashref back
128             if ( defined $path ) {
129              
130             $path =~ s/^\///g; # Remove leading slashes, as they don't do anything
131             # and there's no reason to break over it.
132              
133             foreach my $piece (split(/\//, $path)) {
134             if(ref($conf) eq 'HASH') {
135             $conf = $conf->{$piece};
136             } elsif(ref($conf) eq 'ARRAY' && $piece =~ /\d+/) {
137             $conf = $conf->[$piece];
138             } else {
139             # Not sure what they asked for, but it's not gonna work. Maybe a
140             # string member of an array?
141             $conf = undef;
142             }
143             return undef unless defined($conf);
144             }
145              
146             }
147              
148             if ( $self->convert_empty_to_undef ) {
149             if ( ref $conf eq 'HASH' and not keys %$conf ) {
150             $conf = undef;
151             }
152             }
153              
154             return $conf;
155             }
156              
157              
158             sub mask {
159             my ($self, $path, $value) = @_;
160              
161             # Set the mask if there isn't one.
162             $self->_mask({}) unless $self->has_mask;
163              
164             # No reason to create a hierarchical setup here, just use the path as
165             # the key.
166             $self->_mask->{$path} = $value;
167             }
168              
169              
170             after 'reload' => sub {
171             my $self = shift;
172             $self->clear_mask;
173             };
174              
175              
176             1;
177              
178             __END__
179             =pod
180              
181             =head1 NAME
182              
183             Config::Path
184              
185             =head1 VERSION
186              
187             version 0.13
188              
189             =head1 SYNOPSIS
190              
191             use Config::Path;
192              
193             my $conf = Config::Path->new(
194             files => [ 't/conf/configA.yml', 't/conf/configB.yml' ]
195             );
196              
197             # Or, if you want to load all files in a directory
198              
199             my $dconf = Config::Path->new(
200             directory => 'myapp/conf'
201             );
202              
203             # If you *DON'T* want to convert empty hashes and arrays to undef
204             # (XML parsing will return <foo></foo> as {})
205             my $conf2 = Config::Path->new(
206             convert_empty_to_undef => 0
207             );
208              
209             =head1 DESCRIPTION
210              
211             Config::Path is a Yet Another Config module with a few twists that were desired
212             for an internal project:
213              
214             =over 4
215              
216             =item Multiple files merged into a single, flat hash
217              
218             =item Path-based configuration value retrieval
219              
220             =item Support for loading all config files in a directory
221              
222             =item Sane precedence for key collisions
223              
224             =item Clean, simple implementation
225              
226             =back
227              
228             =head2 Multiple-File Merging
229              
230             If any of your config files contain the same keys, the "right" file wins, using
231             L<Hash::Merge>'s RIGHT_PRECEDENT setting. In other words, later file's keys
232             will have precedence over those loaded earlier.
233              
234             Note that when a full directory of files are loaded the files are sorted via
235             Perl's C<sort> before merging so as to remove any amigiuity about the order
236             in which they will be loaded.
237              
238             =head2 Directory Slurping
239              
240             If you specify a value for the C<directory> attribute, rather than the C<files>
241             attribute then Config::Path will attempt to load all the files in the supplied
242             directory via Config::Any. B<The files will be merged in alphabetical order
243             so that there is no ambiguity in the event of a key collision. Files later
244             in the alphabet will override keys of their predecessors.>
245              
246             =head2 Arrays
247              
248             Arrays can be accessed with paths like C<foo/0/bar>. Just use the array index
249             to descend into that element. If you attempt to treat a hash like an array
250             or an array like hash you will simply get C<undef> back.
251              
252             =head1 NAME
253              
254             Config::Path - Path-like config API with multiple file support, directory
255             loading and arbitrary backends from Config::Any.
256              
257             =head1 ATTRIBUTES
258              
259             =head2 config_options
260              
261             HashRef of options passed to Config::Any.
262              
263             =head2 directory
264              
265             A directory in which files should be searched for. Note that this option is
266             mutually-exclusive to the C<files> attribute. Only set one of them.
267              
268             =head2 files
269              
270             The list of files that will be parsed for this configuration. Note that this
271             option is mutually-exclusive to the C<files> attribute. Only set one of them.
272              
273             =head2 convert_empty_to_undef
274              
275             Defaults to true, if this option is set to false then entities
276             fetched that are {} or [] will be kept in tact.
277              
278             Otherwise Config::Path converts these to undef.
279              
280             =head1 METHODS
281              
282             =head2 add_file ($file)
283              
284             Adds the supplied filename to the list of files that will be loaded. Note
285             that adding a file after you've already loaded a config will not change
286             anything. You'll need to call C<reload> if you want to reread the
287             configuration and include the new file.
288              
289             =head2 clear_mask
290              
291             Clear all values covered by C<mask>.
292              
293             =head2 fetch ($path)
294              
295             Get a value from the config file. As per the name of this module, fetch takes
296             a path argument in the form of C<foo/bar/baz>. This is effectively a
297             shorthand way of expressing a series of hash keys. Whatever value is on
298             the end of the keys will be returned. As such, fetch might return undef,
299             scalar, arrayref, hashref or whatever you've stored in the config file.
300              
301             my $foo = $config->fetch('baz/bar/foo');
302              
303             Note that leading slashes will be automatically stripped, just in case you
304             prefer the idea of using them. They are effectively useless though.
305              
306             =head2 mask ('path/to/value', 'newvalue')
307              
308             Override the specified key to the specified value. Note that this only changes
309             the path's value in this instance. It does not change the config file. This is
310             useful for tests. Note that C<exists> is used so setting a path to undef
311             will not clear the mask. If you want to clear masks use C<clear_mask>.
312              
313             =head2 reload
314              
315             Rereads the config files specified in C<files>. Well, actually it just blows
316             away the internal state of the config so that the next call will reload the
317             configuration. Note that this also clears any C<mask>ing you've done.
318              
319             =head1 AUTHOR
320              
321             Cory G Watson, C<< <gphat at cpan.org> >>
322              
323             =head1 ACKNOWLEDGEMENTS
324              
325             Jay Shirley
326             Mike Eldridge
327              
328             =head1 COPYRIGHT & LICENSE
329              
330             Copyright 2010 Magazines.com
331              
332             This program is free software; you can redistribute it and/or modify it
333             under the terms of either: the GNU General Public License as published
334             by the Free Software Foundation; or the Artistic License.
335              
336             See http://dev.perl.org/licenses/ for more information.
337              
338             =head1 AUTHOR
339              
340             Cory G Watson <gphat@cpan.org>
341              
342             =head1 COPYRIGHT AND LICENSE
343              
344             This software is copyright (c) 2012 by Cold Hard Code, LLC.
345              
346             This is free software; you can redistribute it and/or modify it under
347             the same terms as the Perl 5 programming language system itself.
348              
349             =cut
350