File Coverage

blib/lib/Config/Locale.pm
Criterion Covered Total %
statement 119 127 93.7
branch 11 18 61.1
condition 2 3 66.6
subroutine 29 29 100.0
pod n/a
total 161 177 90.9


line stmt bran cond sub pod time code
1             package Config::Locale;
2 3     3   677735 use 5.008001;
  3         24  
3 3     3   17 use strict;
  3         7  
  3         82  
4 3     3   16 use warnings;
  3         6  
  3         161  
5             our $VERSION = '0.09';
6              
7             =head1 NAME
8              
9             Config::Locale - Load and merge locale-specific configuration files.
10              
11             =head1 SYNOPSIS
12              
13             use Config::Locale;
14            
15             my $locale = Config::Locale->new(
16             identity => \@values,
17             directory => $config_dir,
18             );
19            
20             my $config = $locale->config();
21              
22             =head1 DESCRIPTION
23              
24             This module takes an identity array, determines the permutations of the identity using
25             L, loads configuration files using L, and finally combines
26             the configurations using L.
27              
28             So, given this setup:
29              
30             Config::Locale->new(
31             identity => ['db', '1', 'qa'],
32             );
33              
34             The following configuration stems will be looked for (listed from least specific to most):
35              
36             default
37             all.all.qa
38             all.1.all
39             all.1.qa
40             db.all.all
41             db.all.qa
42             db.1.all
43             db.1.qa
44             override
45              
46             For each file found the contents will be parsed and then merged together to produce the
47             final configuration hash. The hashes will be merged so that the most specific configuration
48             file will take precedence over the least specific files. So, in the example above,
49             "db.1.qa" values will overwrite values from "db.1.all".
50              
51             The term C comes from L, and means a filename without an extension.
52              
53             =cut
54              
55 3     3   1490 use Config::Any;
  3         34057  
  3         106  
56 3     3   1547 use Hash::Merge;
  3         25854  
  3         165  
57 3     3   1869 use Algorithm::Loops qw( NestedLoops );
  3         7086  
  3         198  
58 3     3   30 use Carp qw( croak );
  3         8  
  3         154  
59 3     3   22 use Path::Tiny;
  3         7  
  3         167  
60 3     3   1775 use Types::Standard -types;
  3         220828  
  3         43  
61 3     3   16110 use Types::Common::String -types;
  3         69929  
  3         40  
62 3     3   6801 use Type::Utils -all;
  3         15079  
  3         31  
63              
64 3     3   11581 use Moo;
  3         25756  
  3         16  
65 3     3   6248 use namespace::clean;
  3         33841  
  3         19  
66              
67             my $path_type = declare as Str;
68              
69             coerce $path_type,
70             from InstanceOf[ 'Path::Tiny' ],
71             via { '' . $_ };
72              
73             coerce $path_type,
74             from InstanceOf[ 'Path::Class::File' ],
75             via { '' . $_ };
76              
77             coerce $path_type,
78             from InstanceOf[ 'Path::Class::Dir' ],
79             via { '' . $_ };
80              
81             =head1 ARGUMENTS
82              
83             =head2 identity
84              
85             The identity that configuration files will be loaded for. In a typical hostname-based
86             configuration setup this will be the be the parts of the hostname that declare the class,
87             number, and cluster that the current host identifies itself as. But, this could be any
88             list of values.
89              
90             =cut
91              
92             has identity => (
93             is => 'ro',
94             isa => ArrayRef[ NonEmptySimpleStr ],
95             requires => 1,
96             );
97              
98             =head2 directory
99              
100             The directory to load configuration files from. Defaults to the current
101             directory.
102              
103             =cut
104              
105             has directory => (
106             is => 'ro',
107             isa => $path_type,
108             coerce => 1,
109             default => '.',
110             );
111              
112             =head2 wildcard
113              
114             The wildcard string to use when constructing the configuration filenames.
115             Defaults to C. This may be explicitly set to undef wich will cause
116             the wildcard string to not be added to the filenames at all.
117              
118             =cut
119              
120             has wildcard => (
121             is => 'ro',
122             isa => NonEmptySimpleStr,
123             default => 'all',
124             );
125              
126             =head2 default_stem
127              
128             A filename stem to load first, before all other stems.
129              
130             Defaults to C. A relative path may be specified which will be assumed
131             to be relative to L. If an absolute path is used then no change
132             will be made.
133              
134             Note that L and L are not applied to this stem.
135              
136             =cut
137              
138             has default_stem => (
139             is => 'ro',
140             isa => $path_type,
141             coerce => 1,
142             default => 'default',
143             );
144              
145             =head2 override_stem
146              
147             A filename stem to load last, after all other stems.
148              
149             Defaults to C. A relative path may be specified which will be assumed
150             to be relative to L. If an absolute path is used then no change
151             will be made.
152              
153             Note that L and L are not applied to this stem.
154              
155             =cut
156              
157             has override_stem => (
158             is => 'ro',
159             isa => $path_type,
160             coerce => 1,
161             default => 'override',
162             );
163              
164             =head2 require_defaults
165              
166             If true, then any key that appears in a non-default configuration file must exist
167             in the default configuration or an error will be thrown. Defaults to C<0>.
168              
169             =cut
170              
171             has require_defaults => (
172             is => 'ro',
173             isa => Bool,
174             default => 0,
175             );
176              
177             =head2 separator
178              
179             The character that will be used to separate the identity keys in the
180             configuration filenames. Defaults to C<.>.
181              
182             =cut
183              
184             has separator => (
185             is => 'ro',
186             isa => (NonEmptySimpleStr) & (StrLength[1,1]),
187             default => '.',
188             );
189              
190             =head2 prefix
191              
192             An optional prefix that will be prepended to the configuration filenames.
193              
194             =cut
195              
196             has prefix => (
197             is => 'ro',
198             isa => SimpleStr,
199             default => '',
200             );
201              
202             =head2 suffix
203              
204             An optional suffix that will be appended to the configuration filenames.
205             While it may seem like the right place, you probably should not be using
206             this to specify the extension of your configuration files. L
207             automatically tries many various forms of extensions without the need
208             to explicitly declare the extension that you are using.
209              
210             =cut
211              
212             has suffix => (
213             is => 'ro',
214             isa => SimpleStr,
215             default => '',
216             );
217              
218             =head2 algorithm
219              
220             Which algorithm used to determine, based on the identity, what configuration
221             files to consider for inclusion.
222              
223             The default, C, keeps the order of the identity. This is most useful
224             for identities that are derived from the name of a resource as resource names
225             (such as hostnames of machines) typically have a defined structure.
226              
227             C finds configuration files that includes any number of the identity
228             values in any order. Due to the high CPU demands of permutation algorithms this does
229             not actually generate every possible permutation - instead it finds all files that
230             match the directory/prefix/separator/suffix and filters those for values in the
231             identity and is very fast.
232              
233             =cut
234              
235             has algorithm => (
236             is => 'ro',
237             isa => Enum['NESTED', 'PERMUTE'],
238             default => 'NESTED',
239             );
240              
241             =head2 merge_behavior
242              
243             Specify a L merge behavior. The default is C.
244              
245             =cut
246              
247             has merge_behavior => (
248             is => 'ro',
249             isa => NonEmptySimpleStr,
250             default => 'LEFT_PRECEDENT',
251             );
252              
253             =head1 ATTRIBUTES
254              
255             =head2 config
256              
257             Contains the final configuration hash as merged from the hashes in L,
258             L, and L.
259              
260             =cut
261              
262             has config => (
263             is => 'lazy',
264             init_arg => undef,
265             );
266             sub _build_config {
267 5     5   2341 my ($self) = @_;
268             return $self->_merge_configs([
269             { default => $self->default_config() },
270 5         148 @{ $self->stem_configs() },
271 5         175 @{ $self->override_configs() },
  5         272  
272             ]);
273             }
274              
275             =head2 default_config
276              
277             A merged hash of all the hashrefs in L. This is computed
278             separately, but then merged with, L so that the L and
279             L can be checked for valid keys if L
280             is set.
281              
282             =cut
283              
284             has default_config => (
285             is => 'lazy',
286             init_arg => undef,
287             );
288             sub _build_default_config {
289 5     5   69 my ($self) = @_;
290 5         130 return $self->_merge_configs( $self->default_configs() );
291             }
292              
293             =head2 default_configs
294              
295             An array of hashrefs, each hashref containing a single key/value pair as returned
296             by L->load_stems() where the key is the filename found, and the value
297             is the parsed configuration hash for any L configuration.
298              
299             =cut
300              
301             has default_configs => (
302             is => 'lazy',
303             init_arg => undef,
304             );
305             sub _build_default_configs {
306 5     5   62 my ($self) = @_;
307 5         115 return $self->_load_configs( [$self->_default_stem_path()] );
308             }
309              
310             =head2 stem_configs
311              
312             Like L, but for any L configurations.
313              
314             =cut
315              
316             has stem_configs => (
317             is => 'lazy',
318             init_arg => undef,
319             );
320             sub _build_stem_configs {
321 5     5   57 my ($self) = @_;
322 5         122 return $self->_load_configs( $self->stems(), $self->default_config() );
323             }
324              
325             =head2 override_configs
326              
327             Like L, but for any L configurations.
328              
329             =cut
330              
331             has override_configs => (
332             is => 'lazy',
333             init_arg => undef,
334             );
335             sub _build_override_configs {
336 5     5   79 my ($self) = @_;
337 5         181 return $self->_load_configs( [$self->_override_stem_path()], $self->default_config() );
338             }
339              
340             sub _merge_configs {
341 10     10   52 my ($self, $configs) = @_;
342              
343 10         389 my $merge = $self->merge_object();
344              
345 10         1186 my $config = {};
346 10         62 foreach my $hash (@$configs) {
347 13         916 foreach my $file (keys %$hash) {
348 13         40 my $this_config = $hash->{$file};
349 13         109 $config = $merge->merge( $this_config, $config );
350             }
351             }
352              
353 10         1015 return $config;
354             }
355              
356             sub _load_configs {
357 15     15   2804 my ($self, $stems, $defaults) = @_;
358              
359 15         204 my $configs = Config::Any->load_stems({
360             stems => $stems,
361             use_ext => 1,
362             });
363              
364 15 50 66     246203 if ($defaults and $self->require_defaults()) {
365 0         0 foreach my $hash (@$configs) {
366 0         0 foreach my $file (keys %$hash) {
367 0         0 my $config = $hash->{$file};
368 0         0 foreach my $key (keys %$config) {
369 0 0       0 next if exists $defaults->{$key};
370 0         0 croak "The $key key is defined in $file but does not have a default set";
371             }
372             }
373             }
374             }
375              
376 15         298 return $configs;
377             }
378              
379             =head2 stems
380              
381             Contains an array of file paths for each value in L.
382              
383             =cut
384              
385             has stems => (
386             is => 'lazy',
387             init_arg => undef,
388             );
389             sub _build_stems {
390 5     5   63 my ($self) = @_;
391              
392 5         96 my $directory = $self->_directory_path();
393 5         64 my $separator = $self->separator();
394 5         30 my $prefix = $self->prefix();
395 5         33 my $suffix = $self->suffix();
396              
397 5         11 my @combinations = @{ $self->combinations() };
  5         107  
398              
399 5         17 my @stems;
400 5         32 foreach my $combination (@combinations) {
401 21         695 my @parts = @$combination;
402 21         99 push @stems, $directory->child( $prefix . join($separator, @parts) . $suffix );
403             }
404              
405 5         320 return \@stems;
406             }
407              
408             =head2 combinations
409              
410             Holds an array of arrays containing all possible permutations of the
411             identity, per the specified L.
412              
413             =cut
414              
415             has combinations => (
416             is => 'lazy',
417             init_arg => undef,
418             );
419             sub _build_combinations {
420 6     6   443 my ($self) = @_;
421              
422 6 100       73 if ($self->algorithm() eq 'NESTED') {
    50          
423 3         23 return $self->_nested_combinations();
424             }
425             elsif ($self->algorithm() eq 'PERMUTE') {
426 3         26 return $self->_permute_combinations();
427             }
428              
429 0         0 die 'Unknown algorithm'; # Shouldn't ever get to this.
430             }
431              
432             sub _nested_combinations {
433 3     3   24 my ($self) = @_;
434              
435 3         33 my $wildcard = $self->wildcard();
436              
437             my $options = [
438 9         43 map { [$wildcard, $_] }
439 3         7 @{ $self->identity() }
  3         17  
440             ];
441              
442             return [
443             # If the wildcard is undef then we will have one empty array that needs removal.
444 24         110 grep { @$_ > 0 }
445              
446             # If the wildcard is undef then we need to strip out the undefs.
447 24         122 map { [ grep { defined($_) } @$_ ] }
  72         127  
448              
449             # Run arbitrarily deep foreach loop.
450             NestedLoops(
451             $options,
452 24     24   1071 sub { [ @_ ] },
453             )
454 3         64 ];
455             }
456              
457             sub _permute_combinations {
458 3     3   8 my ($self) = @_;
459              
460 3         13 my $wildcard = $self->wildcard();
461 3         6 my $prefix = $self->prefix();
462 3         7 my $suffix = $self->suffix();
463 3         6 my $separator = $self->separator();
464              
465             my $id_lookup = {
466 4         33 map { $_ => 1 }
467 3         8 @{ $self->identity() },
  3         10  
468             };
469              
470 3 50       15 $id_lookup->{$wildcard} = 1 if defined $wildcard;
471              
472 3         6 my @combos;
473 3         17 foreach my $file (path( $self->directory() )->children()) {
474 9 50       701 next if -d $file;
475              
476 9 50       189 if ($file->basename() =~ m{^$prefix(.*)$suffix\.}) {
477 9         353 my @parts = split(/[$separator]/, $1);
478 9         17 my $matches = 1;
479 9         17 foreach my $part (@parts) {
480 11 100       29 next if $id_lookup->{$part};
481 4         8 $matches = 0;
482 4         6 last;
483             }
484 9 100       22 if ($matches) { push @combos, \@parts }
  5         16  
485             }
486             }
487              
488             return [
489 3         32 sort { @$a <=> @$b }
  3         21  
490             @combos
491             ];
492              
493 0         0 return \@combos;
494             }
495              
496             =head2 merge_object
497              
498             The L object that will be used to merge the configuration
499             hashes.
500              
501             =cut
502              
503             has merge_object => (
504             is => 'lazy',
505             init_arg => undef,
506             );
507             sub _build_merge_object {
508 5     5   84 my ($self) = @_;
509 5         122 return Hash::Merge->new( $self->merge_behavior() );
510             }
511              
512             has _directory_path => (
513             is => 'lazy',
514             init_arg => undef,
515             );
516             sub _build__directory_path {
517 13     13   742 my ($self) = @_;
518 13         52 return path( $self->directory() )->absolute();
519             }
520              
521             has _default_stem_path => (
522             is => 'lazy',
523             init_arg => undef,
524             );
525             sub _build__default_stem_path {
526 9     9   19724 my ($self) = @_;
527 9         62 return path( $self->default_stem() )->absolute( $self->_directory_path() );
528             }
529              
530             has _override_stem_path => (
531             is => 'lazy',
532             init_arg => undef,
533             );
534             sub _build__override_stem_path {
535 9     9   4188 my ($self) = @_;
536 9         138 return path( $self->override_stem() )->absolute( $self->_directory_path() );
537             }
538              
539             1;
540             __END__