File Coverage

blib/lib/Config/App.pm
Criterion Covered Total %
statement 191 226 84.5
branch 70 104 67.3
condition 27 43 62.7
subroutine 28 31 90.3
pod 8 8 100.0
total 324 412 78.6


line stmt bran cond sub pod time code
1             package Config::App;
2             # ABSTRACT: Cascading merged application configuration
3              
4 6     6   1195802 use 5.010;
  6         29  
5 6     6   38 use strict;
  6         31  
  6         205  
6 6     6   51 use warnings;
  6         9  
  6         408  
7              
8 6     6   38 use Carp qw( croak carp );
  6         15  
  6         510  
9 6     6   51 use Cwd 'getcwd';
  6         23  
  6         282  
10 6     6   3245 use FindBin ();
  6         7981  
  6         164  
11 6     6   4247 use JSON::XS ();
  6         38329  
  6         210  
12 6     6   4571 use LWP::UserAgent ();
  6         392392  
  6         218  
13 6     6   3846 use POSIX ();
  6         45774  
  6         195  
14 6     6   48 use URI ();
  6         11  
  6         96  
15 6     6   2650 use YAML::XS ();
  6         19837  
  6         5853  
16              
17             our $VERSION = '1.19'; # VERSION
18              
19             $Carp::Internal{ (__PACKAGE__) }++;
20              
21             sub _locate_root_config {
22 33     33   74 my ($config_location) = @_;
23 33 100       98 if ($config_location) {
24 27 50       261 return undef, $config_location if ( URI->new($config_location)->scheme );
25 27 0       35985 return ( -f $config_location ) ? ( '/', $config_location ) : ( undef, undef )
    50          
26             if ( substr( $config_location, 0, 1 ) eq '/' );
27             }
28              
29             my $locate = sub {
30 33     33   86 my ( $abs_paths, $rel_config_locations ) = @_;
31              
32 33         89 for my $abs_paths (@$abs_paths) {
33 34         69 for my $test_location (@$rel_config_locations) {
34 34         170 my @search_path = split( '/', $abs_paths );
35 34         93 while (@search_path) {
36 75         231 my $test_path = join( '/', @search_path );
37 75 100 50     1724 return $test_path || '/', $test_location if ( -f $test_path . '/' . $test_location );
38 43         137 pop @search_path;
39             }
40             }
41             }
42 33         279 };
43              
44             my ( $root_dir, $config_file ) = $locate->(
45             [ $FindBin::Bin, getcwd() ],
46             [ ($config_location) ? $config_location : (
47 33 100       528 ( $ENV{CONFIGAPPINIT} ) ? $ENV{CONFIGAPPINIT} : (),
    100          
48             qw(
49             config/app.yaml
50             etc/config.yaml
51             etc/conf.yaml
52             etc/app.yaml
53             config.yaml
54             conf.yaml
55             app.yaml
56             )
57             ) ],
58             );
59              
60 33 100 66     448 return ( length $root_dir and length $config_file ) ? ( $root_dir, $config_file ) : undef;
61             }
62              
63             sub _add_to_inc {
64 6     6   20 my ( $root_dir, @libs ) = @_;
65              
66 6         14 for my $lib ( map { $root_dir . '/' . $_ } @libs ) {
  10         31  
67 10 50       19 unshift( @INC, $lib ) unless ( grep { $_ eq $lib } @INC );
  95         140  
68             }
69              
70 6         16 return;
71             }
72              
73             sub import {
74 5     5   98 my $self = shift;
75              
76 5         15 my ( $root_dir, $config_file, @libs );
77 5         30 for ( @_, undef ) {
78 6         21 my @locate_root_config = _locate_root_config($_);
79              
80 6 50 66     30 unless ( $locate_root_config[0] ) {
81 0         0 push( @libs, $_ );
82             }
83             elsif ( not $root_dir ) {
84             ( $root_dir, $config_file ) = @locate_root_config;
85             }
86             }
87              
88 5 50       20 die "Config::App unable to locate configuration file\n" unless ($config_file);
89              
90 5 50 50     45 _add_to_inc( $root_dir, ( @libs || 'lib' ) ) if $root_dir;
91              
92 5         7 my $error = do {
93 5         8 local $@;
94 5         13 eval { $self->new($config_file) };
  5         25  
95 5         17 $@;
96             };
97 5         14 chomp($error);
98 5 100       19 die $error . "\n" if $error;
99              
100 4         10388 return;
101             }
102              
103             {
104             my $singleton;
105              
106             sub new {
107 16     16 1 991553 my ( $self, $location, $no_singleton ) = @_;
108 16 100 100     90 return $singleton if ( not $no_singleton and $singleton );
109              
110 15         200 ( my $box = ( POSIX::uname )[1] ) =~ s/\..*$//;
111 15         42 my $conf = {};
112              
113             _process_location({
114             box => $box,
115             user => getpwuid($>) || POSIX::cuserid,
116             env => $ENV{CONFIGAPPENV},
117 15   33     2600 conf => $conf,
118             optional => 0,
119             location => $location,
120             });
121              
122 14         125 $self = bless( { _conf => $conf }, $self );
123 14 100       49 $singleton = $self unless $no_singleton;
124              
125 14 100       46 if ( my $libs = $self->get('libs') ) {
126 1 50       5 _add_to_inc(
127             $self->root_dir,
128             ( ref $libs eq 'ARRAY' ) ? @$libs : $libs,
129             );
130             }
131              
132 14         93 return $self;
133             }
134              
135             sub deimport {
136 0     0 1 0 my $self = shift;
137              
138 0 0       0 delete $self->{_conf} if ( __PACKAGE__ eq ref $self );
139 0         0 $singleton = undef;
140              
141             {
142 6     6   51 no strict 'refs';
  6         13  
  6         14834  
  0         0  
143 0         0 @{ __PACKAGE__ . '::ISA' } = ();
  0         0  
144 0         0 my $symbol_table = __PACKAGE__ . '::';
145 0         0 for my $symbol ( keys %$symbol_table ) {
146 0 0       0 next if ( $symbol =~ /\A[^:]+::\z/ );
147 0         0 delete $symbol_table->{$symbol};
148             }
149             }
150              
151 0         0 delete $INC{ join( '/', split /(?:'|::)/, __PACKAGE__ ) . '.pm' };
152              
153 0         0 return;
154             }
155             }
156              
157             sub find {
158 0     0 1 0 my $class = shift;
159              
160 0         0 my $self;
161 0         0 local $@;
162 0         0 eval {
163 0         0 $self = $class->new(@_);
164             };
165 0 0       0 if ($@) {
166 0         0 return;
167             }
168              
169 0         0 return $self;
170             }
171              
172             sub root_dir {
173 1     1 1 4 my ($self) = @_;
174 1         3 return $self->get( qw( config_app root_dir ) );
175             }
176              
177             sub includes {
178 0     0 1 0 my ($self) = @_;
179 0         0 return $self->get( qw( config_app includes ) );
180             }
181              
182             sub get {
183 28     28 1 70 my $self = shift;
184 28         108 my $data = $self->{_conf};
185              
186 28         100 $data = $data->{$_} for (@_);
187 28         91 return _clone($data);
188             }
189              
190             sub put {
191 1     1 1 4 my $self = shift;
192 1         3 my $new_value = pop;
193 1         4 my $path = [@_];
194 1         2 my $node = pop @{$path};
  1         3  
195 1         2 my $error = do {
196 1         2 local $@;
197 1         2 eval {
198 1         3 my $data = $self->{_conf};
199 1         2 $data = $data->{$_} for ( @{$path} );
  1         3  
200 1         4 $data->{$node} = $new_value;
201             };
202 1         4 $@;
203             };
204              
205 1 50       4 return ($error) ? 0 : 1;
206             }
207              
208             sub conf {
209 2     2 1 33 my $self = shift;
210 2         12 _merge_settings( $self->{_conf}, $_ ) for (@_);
211 2         10 return _clone( $self->{_conf} );
212             }
213              
214             sub _process_location {
215 27     27   62 my ($input) = @_;
216 27         147 my ( $root_dir, $config_file ) = _locate_root_config( $input->{location} );
217              
218 27 100       88 my $include = join( '/', grep { defined and $_ ne '/' } $root_dir, $config_file );
  54         296  
219 27 100       52 my $sources = [ grep { defined } @{ $input->{sources} || [] }, $input->{location} ];
  41         106  
  27         138  
220              
221             my $raw_config = _get_raw_config({
222             include => $include,
223             location => $input->{location},
224             optional => $input->{optional},
225 27         181 sources => $sources,
226             });
227 27 100       150 return unless $raw_config;
228              
229             $input->{conf}->{config_app}{root_dir} = $root_dir
230 26 100 66     206 if ( defined $root_dir and not exists $input->{conf}->{config_app}{root_dir} );
231              
232 26 100       43 unless ( grep { $_ eq $include } @{ $input->{conf}->{config_app}{includes} } ) {
  13         45  
  26         103  
233 25         42 push( @{ $input->{conf}->{config_app}{includes} }, $include );
  25         72  
234             }
235             else {
236 1         22 carp "Configuration include recursion encountered when trying to include: $include";
237 1         1484 return;
238             }
239              
240 25         125 my $set = _parse_config({
241             raw_config => $raw_config,
242             include => $include,
243             sources => $sources,
244             });
245              
246             my $sub_process_location = sub { _process_location({
247             box => $input->{box},
248             user => $input->{user},
249             env => $input->{env},
250             conf => $input->{conf},
251 12     12   140 location => $_[0],
252             optional => $_[1],
253             sources => $sources,
254 24         155 }) };
255              
256             my $fetch_block = sub {
257 48     48   109 my ($include_type) = @_;
258 48         87 my $optional = 'optional_' . $include_type;
259              
260 48 100       135 $sub_process_location->( $set->{$include_type}, 0 ) if ( $set->{$include_type} );
261             $sub_process_location->( delete( $input->{conf}->{$include_type} ), 0 )
262 48 100       138 if ( $input->{conf}->{$include_type} );
263 48 50       124 $sub_process_location->( $set->{$optional}, 1 ) if ( $set->{$optional} );
264             $sub_process_location->( delete( $input->{conf}->{$optional} ), 1 )
265 48 100       147 if ( $input->{conf}->{$optional} );
266 24         95 };
267              
268 24         66 $fetch_block->('preinclude');
269              
270 24         86 my ( $box, $user, $env ) = @$input{ qw( box user env ) };
271 24         393 _merge_settings( $input->{conf}, $_ ) for (
272 360         684 grep { defined } (
273             map {
274 360         671 $set->{ join( '|', ( grep { defined } @$_ ) ) }
  840         1678  
275             } (
276             [ 'default' ],
277             [ '+', '+', '+' ], [ '+', '+' ], [ '+' ],
278             [ $box, '+', '+' ], [ $box, '+' ], [ $box ],
279             [ '+', $user, '+' ], [ '+', $user ],
280             [ $box, $user, '+' ], [ $box, $user ],
281             [ '+', '+', $env ],
282             [ '+', $user, $env ],
283             [ $box, '+', $env ],
284             [ $box, $user, $env ],
285             )
286             )
287             );
288              
289 24         185 $fetch_block->('include');
290              
291 24         314 return;
292             }
293              
294             {
295             my $ua;
296              
297             sub _get_raw_config {
298 27     27   62 my ($input) = @_;
299              
300 27 50       167 if ( URI->new( $input->{include} )->scheme ) {
301 0   0     0 $ua ||= LWP::UserAgent->new(
302             agent => 'Config-App',
303             cookie_jar => {},
304             env_proxy => 1,
305             );
306              
307 0         0 my $res = $ua->get( $input->{include} );
308              
309 0 0       0 if ( $res->is_success ) {
310 0         0 return $res->decoded_content;
311             }
312             else {
313             croak 'Failed to get '
314 0         0 . join( ' -> ', map { "\"$_\"" } @{ $input->{sources} } )
  0         0  
315             . '; '
316             . $res->status_line
317 0 0       0 unless $input->{optional};
318 0         0 return;
319             }
320             }
321             else {
322 27 100       1914 unless ( $input->{include} ) {
323             croak 'Failed to find ' .
324 0         0 join( ' -> ', map { "\"$_\"" } @{ $input->{sources} } )
  0         0  
325 1 50       5 unless $input->{optional};
326 1         4 return;
327             }
328             else {
329             open( my $include_fh, '<', $input->{include} )
330 26 50       1286 or croak "Failed to read $input->{include}; $!";
331 26         1428 return join( '', <$include_fh> );
332             }
333             }
334             }
335             }
336              
337             {
338             my $json_xs;
339              
340             sub _parse_config {
341 25     25   55 my ($input) = @_;
342              
343 25         71 my @types = qw( yaml json );
344 25 100 66     229 if ( $input->{include} =~ /\.yaml$/i or $input->{include} =~ /\.yml$/i ) {
    50 33        
345 19         50 @types = ( 'yaml', grep { $_ ne 'yaml' } @types );
  38         138  
346             }
347             elsif ( $input->{include} =~ /\.json$/i or $input->{include} =~ /\.js$/i ) {
348 6         19 @types = ( 'json', grep { $_ ne 'json' } @types );
  12         32  
349             }
350              
351 25         47 my ( $config, @errors );
352 25         60 for my $type (@types) {
353 25         39 my $error = do {
354 25         40 local $@;
355 25         54 eval {
356 25 100       79 if ( $type eq 'json' ) {
357 6   66     199 $json_xs ||= JSON::XS->new
358             ->utf8
359             ->relaxed
360             ->allow_nonref
361             ->allow_unknown
362             ->allow_blessed
363             ->allow_tags;
364              
365 6         67 $config = $json_xs->decode( $input->{raw_config} );
366             }
367             else {
368 19         1580 $config = YAML::XS::Load( $input->{raw_config} );
369             }
370             };
371 25         88 $@;
372             };
373              
374 25 100       88 if ($error) {
375             my $message =
376             'Failed to parse ' .
377 1         3 join( ' -> ', map { "\"$_\"" } @{ $input->{sources} } ) . '; ' .
  1         5  
  1         1  
378             $error;
379 1 50       272 croak($message) if ( not $config );
380 0         0 carp($message);
381             }
382              
383 24 50       77 last if $config;
384             }
385              
386 24         63 return $config;
387             }
388             }
389              
390             sub _merge_settings {
391 36     36   122 my ( $merge, $source, $is_deep_call ) = @_;
392 36 50       95 return unless $source;
393              
394 36 50 66     287 if ( not $is_deep_call and ref $merge eq 'HASH' and ref $source eq 'HASH' ) {
      66        
395 33 100       105 if ( my $libs = delete $source->{libs} ) {
396 3 100       22 if ( not exists $merge->{libs} ) {
    100          
397 1         4 $merge->{libs} = $libs;
398             }
399             elsif ( ref $merge->{libs} eq 'ARRAY' ) {
400 1 50       5 my %libs = map { $_ => 1 } @{ $merge->{libs} }, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  5         13  
  1         41  
401 1         9 $merge->{libs} = [ sort keys %libs ];
402             }
403             else {
404 1 50       8 my %libs = map { $_ => 1 } $merge->{libs}, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  4         15  
405 1         12 $merge->{libs} = [ sort keys %libs ];
406             }
407             }
408             }
409              
410 36 50       119 if ( ref $merge eq 'HASH' ) {
    0          
411 36         60 for my $key ( keys %{$source} ) {
  36         112  
412 53 100 100     242 if ( exists $merge->{$key} and ref $merge->{$key} eq 'HASH' and ref $source->{$key} eq 'HASH' ) {
      100        
413 3         16 _merge_settings( $merge->{$key}, $source->{$key}, 1 );
414             }
415             else {
416 50         141 $merge->{$key} = _clone( $source->{$key} );
417             }
418             }
419             }
420             elsif ( ref $merge eq 'ARRAY' ) {
421 0         0 push( @$source, @$merge );
422             }
423              
424 36         124 return;
425             }
426              
427             sub _clone {
428 80     80   4057 return YAML::XS::Load( YAML::XS::Dump(@_) );
429             }
430              
431             1;
432              
433             __END__