File Coverage

blib/lib/Config/Locale.pm
Criterion Covered Total %
statement 115 123 93.5
branch 11 18 61.1
condition 2 3 66.6
subroutine 28 28 100.0
pod n/a
total 156 172 90.7


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