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