File Coverage

blib/lib/Config/App.pm
Criterion Covered Total %
statement 190 225 84.4
branch 69 102 67.6
condition 27 43 62.7
subroutine 28 31 90.3
pod 8 8 100.0
total 322 409 78.7


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