File Coverage

blib/lib/Config/App.pm
Criterion Covered Total %
statement 196 211 92.8
branch 70 100 70.0
condition 25 38 65.7
subroutine 25 25 100.0
pod 4 4 100.0
total 320 378 84.6


line stmt bran cond sub pod time code
1             package Config::App;
2             # ABSTRACT: Cascading merged application configuration
3              
4 5     5   1038196 use 5.008;
  5         60  
5 5     5   23 use strict;
  5         10  
  5         106  
6 5     5   20 use warnings;
  5         9  
  5         117  
7              
8 5     5   2604 use URI ();
  5         20976  
  5         120  
9 5     5   3424 use LWP::UserAgent ();
  5         182156  
  5         166  
10 5     5   54 use Carp qw( croak carp );
  5         12  
  5         331  
11 5     5   2442 use FindBin ();
  5         4749  
  5         118  
12 5     5   3472 use JSON::XS ();
  5         23236  
  5         164  
13 5     5   1875 use YAML::XS ();
  5         12581  
  5         166  
14 5     5   44 use POSIX ();
  5         11  
  5         10434  
15              
16             our $VERSION = '1.15'; # VERSION
17              
18             $Carp::Internal{ (__PACKAGE__) }++;
19              
20             sub _locations {
21 6     6   27 return grep { length } $ENV{CONFIGAPPINIT}, qw(
  48         72  
22             config/app.yaml
23             etc/config.yaml
24             etc/conf.yaml
25             etc/app.yaml
26             config.yaml
27             conf.yaml
28             app.yaml
29             );
30             }
31              
32             sub _add_to_inc {
33 6     6   22 my ( $root_dir, @libs ) = @_;
34 6         17 for my $lib ( map { $root_dir . '/' . $_ } @libs ) {
  10         49  
35 10 50       73 unshift( @INC, $lib ) unless ( grep { $_ eq $lib } @INC );
  125         194  
36             }
37             }
38              
39             sub import {
40 5     5   63 my $self = shift;
41              
42 5         11 my ( $root_dir, $config_file, $location, @libs );
43 5 100       38 for (
    50          
44             ( @_ > 1 ) ? ( $_[0], $_[-1], _locations() ) :
45             ( @_ == 1 ) ? ( $_[0], _locations() ) : _locations()
46             ) {
47 5         15 ( $root_dir, $config_file ) = _find_root_dir($_);
48              
49 5 50       75 if ( $root_dir eq '/' ) {
    50          
50 0         0 $location = $config_file;
51             }
52             elsif ( -f $config_file ) {
53 5         23 $location = substr( $config_file, length($root_dir) + 1 );
54 5         19 @libs = grep { $location ne $_ } @_;
  1         4  
55 5         14 last;
56             }
57             }
58              
59 5   50     48 _add_to_inc( $root_dir, ( @libs || 'lib' ) );
60              
61 5         15 my $error = do {
62 5         28 local $@;
63 5         10 eval {
64 5         43 $self->new($location);
65             };
66 5         18 $@;
67             };
68              
69 5         13 chomp($error);
70 5 100       19 die $error . "\n" if ($error);
71 4         9023 return;
72             }
73              
74             {
75             my $singleton;
76              
77             sub new {
78 16     16 1 2370 my ( $self, $location, $no_singleton ) = @_;
79 16 100 100     72 return $singleton if ( not $no_singleton and $singleton );
80              
81 15         180 ( my $box = ( POSIX::uname )[1] ) =~ s/\..*$//;
82 15   33     4356 my $user = getpwuid($>) || POSIX::cuserid;
83 15         72 my $env = $ENV{CONFIGAPPENV};
84 15         29 my $conf = {};
85              
86 15 100       60 if ($location) {
87 14         51 _location_fetch( $box, $user, $env, $conf, $location );
88             }
89             else {
90 1         2 my ( $success, @errors );
91              
92 1         3 for my $this_location ( _locations() ) {
93 1         2 my $error = do {
94 1         1 local $@;
95 1         1 eval {
96 1         2 _location_fetch( $box, $user, $env, $conf, $this_location );
97             };
98 1         2 $@;
99             };
100              
101 1         2 chomp($error);
102 1 50       3 if ($error) {
103 0 0       0 die $error . "\n" if ( substr( $error, 0, 15 ) ne 'Failed to find ' );
104 0         0 push( @errors, $error );
105             }
106             else {
107 1         1 $success = 1;
108 1         2 last;
109             }
110             }
111              
112 1 50       3 die join( ' ', @errors ) unless ($success);
113             }
114              
115 14         71 $self = bless( { _conf => $conf }, $self );
116 14 100       36 $singleton = $self unless ($no_singleton);
117              
118 14 100       40 if ( my $libs = $self->get('libs') ) {
119 1 50       6 _add_to_inc(
120             $self->get( qw( config_app root_dir ) ),
121             ( ref $libs eq 'ARRAY' ) ? @$libs : $libs,
122             );
123             }
124              
125 14         72 return $self;
126             }
127             }
128              
129             sub get {
130 28     28 1 59 my $self = shift;
131 28         196 my $data = $self->{_conf};
132              
133 28         80 $data = $data->{$_} for (@_);
134 28         53 return _clone($data);
135             }
136              
137             sub put {
138 1     1 1 2 my $self = shift;
139 1         1 my $new_value = pop;
140 1         2 my $path = [@_];
141 1         2 my $node = pop @{$path};
  1         2  
142 1         1 my $error = do {
143 1         1 local $@;
144 1         2 eval {
145 1         1 my $data = $self->{_conf};
146 1         2 $data = $data->{$_} for ( @{$path} );
  1         2  
147 1         2 $data->{$node} = $new_value;
148             };
149 1         2 $@;
150             };
151              
152 1 50       3 return ($error) ? 0 : 1;
153             }
154              
155             sub conf {
156 2     2 1 31 my $self = shift;
157 2         12 _merge_settings( $self->{_conf}, $_ ) for (@_);
158 2         7 return _clone( $self->{_conf} );
159             }
160              
161             sub _clone {
162 80     80   2649 return YAML::XS::Load( YAML::XS::Dump(@_) );
163             }
164              
165             sub _location_fetch {
166 27     27   90 my ( $box, $user, $env, $conf, $location, @source_path ) = @_;
167              
168 27         79 my ( $raw_config, $root_dir ) = _get_raw_config( $location, @source_path );
169 27 100       110 return unless ($raw_config);
170              
171 26 50 66     142 $conf->{config_app}{root_dir} ||= $root_dir if ($root_dir);
172              
173 26 100       91 my $include = $root_dir . '/' . ( ( ref $location ) ? $$location : $location );
174 26 100       38 unless ( grep { $_ eq $include } @{ $conf->{config_app}{includes} } ) {
  13         37  
  26         81  
175 25         34 push( @{ $conf->{config_app}{includes} }, $include );
  25         50  
176             }
177             else {
178 1 50       15 carp(
179             'Configuration include recursion encountered when trying to include: ' .
180             ( ( ref $location ) ? $$location : $location )
181             );
182 1         869 return;
183             }
184              
185 25         131 my $set = _parse_config( $raw_config, $location, @source_path );
186              
187 24     12   99 my $location_fetch = sub { _location_fetch( $box, $user, $env, $conf, $_[0], $location, @source_path ) };
  12         61  
188             my $fetch_block = sub {
189 48 100   48   131 my $include = ( ( $_[0] ) ? 'pre' : '' ) . 'include';
190 48         81 my $optional = 'optional_' . $include;
191              
192 48 100       92 $location_fetch->( $set->{$include} ) if ( $set->{$include} );
193 48 100       104 $location_fetch->( delete( $conf->{$include} ) ) if ( $conf->{$include} );
194 48 50       105 $location_fetch->( \$set->{$optional} ) if ( $set->{$optional} );
195 48 100       89 $location_fetch->( \ delete( $conf->{$optional} ) ) if ( $conf->{$optional} );
196 24         70 };
197              
198 24         56 $fetch_block->(1);
199              
200 24         240 _merge_settings( $conf, $_ ) for (
201 360         423 grep { defined } (
202             map {
203 360         422 $set->{ join( '|', ( grep { defined } @$_ ) ) }
  840         1159  
204             } (
205             [ 'default' ],
206             [ '+', '+', '+' ], [ '+', '+' ], [ '+' ],
207             [ $box, '+', '+' ], [ $box, '+' ], [ $box ],
208             [ '+', $user, '+' ], [ '+', $user ],
209             [ $box, $user, '+' ], [ $box, $user ],
210             [ '+', '+', $env ],
211             [ '+', $user, $env ],
212             [ $box, '+', $env ],
213             [ $box, $user, $env ],
214             )
215             )
216             );
217              
218 24         109 $fetch_block->();
219              
220 24         156 return;
221             }
222              
223             sub _get_raw_config {
224 27     27   53 my ( $location, @source_path ) = @_;
225              
226 27         39 my $optional = 0;
227 27 100       61 if ( ref $location ) {
228 2         13 $location = $$location;
229 2         3 $optional = 1;
230             }
231              
232 27 50       147 if ( URI->new($location)->scheme ) {
233 0         0 my $ua = LWP::UserAgent->new(
234             agent => 'Config-App',
235             cookie_jar => {},
236             env_proxy => 1,
237             );
238              
239 0         0 my $res = $ua->get($location);
240              
241 0 0       0 if ( $res->is_success ) {
242 0         0 return $res->decoded_content;
243             }
244             else {
245 0 0       0 unless ($optional) {
246             croak 'Failed to get '
247 0         0 . join( ' -> ', map { "\"$_\"" } @source_path, $location )
  0         0  
248             . '; '
249             . $res->status_line;
250             }
251             else {
252 0         0 return '', '';
253             }
254             }
255             }
256             else {
257 27         1652 my ( $root_dir, $config_file ) = _find_root_dir($location);
258              
259 27 100       255 unless ( -f $config_file ) {
260 1 50       4 unless ($optional) {
261 0         0 croak 'Failed to find ' . join( ' -> ', map { "\"$_\"" } @source_path, $location );
  0         0  
262             }
263             else {
264 1         4 return '', '';
265             }
266             }
267             else {
268 26 50       909 open( my $config_fh, '<', $config_file ) or croak "Failed to read $config_file; $!";
269 26         1151 return join( '', <$config_fh> ), $root_dir;
270             }
271             }
272             }
273              
274             sub _find_root_dir {
275 32     32   57 my ($location) = @_;
276 32   33     75 $location ||= location();
277 32 50 33     93 return '/', $location if ( URI->new($location)->scheme or substr( $location, 0, 1 ) eq '/' );
278              
279 32         20633 my ( $root_dir, $config_file );
280 32         149 my @search_path = split( '/', $FindBin::Bin );
281 32         80 while ( @search_path > 1 ) {
282 67         176 $root_dir = join( '/', @search_path );
283 67         129 $config_file = $root_dir . '/' . $location;
284 67 100       950 last if ( -f $config_file );
285 36         128 pop @search_path;
286             }
287              
288 32         163 return $root_dir, $config_file;
289             }
290              
291             {
292             my $json_xs;
293              
294             sub _parse_config {
295 25     25   55 my ( $raw_config, $location, @source_path ) = @_;
296              
297 25         45 my @types = qw( yaml json );
298 25 100 66     167 if ( $location =~ /\.yaml$/ or $location =~ /\.yml$/ ) {
    100 66        
299 19         36 @types = ( 'yaml', grep { $_ ne 'yaml' } @types );
  38         86  
300             }
301             elsif ( $location =~ /\.json$/ or $location =~ /\.js$/ ) {
302 5         15 @types = ( 'json', grep { $_ ne 'json' } @types );
  10         21  
303             }
304              
305 25         49 my ( $config, @errors );
306 25         52 for my $type (@types) {
307 25         26 my $error = do {
308 25         32 local $@;
309 25         36 eval {
310 25 100       49 if ( $type eq 'json' ) {
311 5   66     132 $json_xs ||= JSON::XS->new
312             ->utf8
313             ->relaxed
314             ->allow_nonref
315             ->allow_unknown
316             ->allow_blessed
317             ->allow_tags;
318              
319 5         47 $config = $json_xs->decode($raw_config);
320             }
321             else {
322 20         1036 $config = YAML::XS::Load($raw_config);
323             }
324             };
325 25         88 $@;
326             };
327              
328 25 100       58 if ($error) {
329             my $message =
330             'Failed to parse ' .
331 1         4 join( ' -> ', map { "\"$_\"" } @source_path, $location ) . '; ' .
  1         7  
332             $error;
333 1 50       212 croak($message) if ( not $config );
334 0         0 carp($message);
335             }
336              
337 24 50       55 last if ($config);
338             }
339              
340 24         52 return $config;
341             }
342             }
343              
344             sub _merge_settings {
345 36     36   64 my ( $merge, $source, $is_deep_call ) = @_;
346 36 50       81 return unless $source;
347              
348 36 50 66     183 if ( not $is_deep_call and ref $merge eq 'HASH' and ref $source eq 'HASH' ) {
      66        
349 33 100       126 if ( my $libs = delete $source->{libs} ) {
350 3 100       11 if ( not exists $merge->{libs} ) {
    100          
351 1         4 $merge->{libs} = $libs;
352             }
353             elsif ( ref $merge->{libs} eq 'ARRAY' ) {
354 1 50       3 my %libs = map { $_ => 1 } @{ $merge->{libs} }, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  5         10  
  1         6  
355 1         25 $merge->{libs} = [ sort keys %libs ];
356             }
357             else {
358 1 50       7 my %libs = map { $_ => 1 } $merge->{libs}, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  4         12  
359 1         8 $merge->{libs} = [ sort keys %libs ];
360             }
361             }
362             }
363              
364 36 50       70 if ( ref $merge eq 'HASH' ) {
    0          
365 36         38 for my $key ( keys %{$source} ) {
  36         102  
366 53 100 100     185 if ( exists $merge->{$key} and ref $merge->{$key} eq 'HASH' and ref $source->{$key} eq 'HASH' ) {
      100        
367 3         13 _merge_settings( $merge->{$key}, $source->{$key}, 1 );
368             }
369             else {
370 50         90 $merge->{$key} = _clone( $source->{$key} );
371             }
372             }
373             }
374             elsif ( ref $merge eq 'ARRAY' ) {
375 0         0 push( @$source, @$merge );
376             }
377              
378 36         85 return;
379             }
380              
381             1;
382              
383             __END__