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   966292 use 5.008;
  5         33  
5 5     5   22 use strict;
  5         6  
  5         81  
6 5     5   17 use warnings;
  5         7  
  5         102  
7              
8 5     5   2298 use URI ();
  5         19528  
  5         97  
9 5     5   2900 use LWP::UserAgent ();
  5         160508  
  5         130  
10 5     5   32 use Carp qw( croak carp );
  5         9  
  5         246  
11 5     5   1881 use FindBin ();
  5         4474  
  5         117  
12 5     5   2853 use JSON::XS ();
  5         21689  
  5         107  
13 5     5   1639 use YAML::XS ();
  5         11689  
  5         92  
14 5     5   28 use POSIX ();
  5         8  
  5         10357  
15              
16             our $VERSION = '1.14'; # VERSION
17              
18             $Carp::Internal{ (__PACKAGE__) }++;
19              
20             sub _locations {
21 6     6   22 return grep { length } $ENV{CONFIGAPPINIT}, qw(
  48         67  
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   15 my ( $root_dir, @libs ) = @_;
34 6         13 for my $lib ( map { $root_dir . '/' . $_ } @libs ) {
  10         39  
35 10 50       48 unshift( @INC, $lib ) unless ( grep { $_ eq $lib } @INC );
  125         190  
36             }
37             }
38              
39             sub import {
40 5     5   48 my $self = shift;
41              
42 5         10 my ( $root_dir, $config_file, $location, @libs );
43 5 100       27 for (
    50          
44             ( @_ > 1 ) ? ( $_[0], $_[-1], _locations() ) :
45             ( @_ == 1 ) ? ( $_[0], _locations() ) : _locations()
46             ) {
47 5         11 ( $root_dir, $config_file ) = _find_root_dir($_);
48              
49 5 50       68 if ( $root_dir eq '/' ) {
    50          
50 0         0 $location = $config_file;
51             }
52             elsif ( -f $config_file ) {
53 5         16 $location = substr( $config_file, length($root_dir) + 1 );
54 5         11 @libs = grep { $location ne $_ } @_;
  1         3  
55 5         10 last;
56             }
57             }
58              
59 5   50     32 _add_to_inc( $root_dir, ( @libs || 'lib' ) );
60              
61 5         12 my $error = do {
62 5         19 local $@;
63 5         11 eval {
64 5         42 $self->new($location);
65             };
66 5         14 $@;
67             };
68              
69 5         8 chomp($error);
70 5 100       16 die $error . "\n" if ($error);
71 4         7053 return;
72             }
73              
74             {
75             my $singleton;
76              
77             sub new {
78 16     16 1 2556 my ( $self, $location, $no_singleton ) = @_;
79 16 100 100     78 return $singleton if ( not $no_singleton and $singleton );
80              
81 15         176 ( my $box = ( POSIX::uname )[1] ) =~ s/\..*$//;
82 15   33     3918 my $user = getpwuid($>) || POSIX::cuserid;
83 15         54 my $env = $ENV{CONFIGAPPENV};
84 15         29 my $conf = {};
85              
86 15 100       38 if ($location) {
87 14         40 _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         2 eval {
96 1         3 _location_fetch( $box, $user, $env, $conf, $this_location );
97             };
98 1         2 $@;
99             };
100              
101 1         3 chomp($error);
102 1 50       2 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         2 $success = 1;
108 1         2 last;
109             }
110             }
111              
112 1 50       4 die join( ' ', @errors ) unless ($success);
113             }
114              
115 14         55 $self = bless( { _conf => $conf }, $self );
116 14 100       34 $singleton = $self unless ($no_singleton);
117              
118 14 100       30 if ( my $libs = $self->get('libs') ) {
119 1 50       5 _add_to_inc(
120             $self->get( qw( config_app root_dir ) ),
121             ( ref $libs eq 'ARRAY' ) ? @$libs : $libs,
122             );
123             }
124              
125 14         63 return $self;
126             }
127             }
128              
129             sub get {
130 28     28 1 50 my $self = shift;
131 28         63 my $data = $self->{_conf};
132              
133 28         104 $data = $data->{$_} for (@_);
134 28         45 return _clone($data);
135             }
136              
137             sub put {
138 1     1 1 1 my $self = shift;
139 1         2 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         2 local $@;
144 1         2 eval {
145 1         2 my $data = $self->{_conf};
146 1         1 $data = $data->{$_} for ( @{$path} );
  1         2  
147 1         2 $data->{$node} = $new_value;
148             };
149 1         3 $@;
150             };
151              
152 1 50       5 return ($error) ? 0 : 1;
153             }
154              
155             sub conf {
156 2     2 1 25 my $self = shift;
157 2         7 _merge_settings( $self->{_conf}, $_ ) for (@_);
158 2         7 return _clone( $self->{_conf} );
159             }
160              
161             sub _clone {
162 80     80   2602 return YAML::XS::Load( YAML::XS::Dump(@_) );
163             }
164              
165             sub _location_fetch {
166 27     27   67 my ( $box, $user, $env, $conf, $location, @source_path ) = @_;
167              
168 27         60 my ( $raw_config, $root_dir ) = _get_raw_config( $location, @source_path );
169 27 100       119 return unless ($raw_config);
170              
171 26 50 66     133 $conf->{config_app}{root_dir} ||= $root_dir if ($root_dir);
172              
173 26 100       88 my $include = $root_dir . '/' . ( ( ref $location ) ? $$location : $location );
174 26 100       35 unless ( grep { $_ eq $include } @{ $conf->{config_app}{includes} } ) {
  13         39  
  26         80  
175 25         37 push( @{ $conf->{config_app}{includes} }, $include );
  25         46  
176             }
177             else {
178 1 50       17 carp(
179             'Configuration include recursion encountered when trying to include: ' .
180             ( ( ref $location ) ? $$location : $location )
181             );
182 1         970 return;
183             }
184              
185 25         62 my $set = _parse_config( $raw_config, $location, @source_path );
186              
187 24     12   94 my $location_fetch = sub { _location_fetch( $box, $user, $env, $conf, $_[0], $location, @source_path ) };
  12         43  
188             my $fetch_block = sub {
189 48 100   48   141 my $include = ( ( $_[0] ) ? 'pre' : '' ) . 'include';
190 48         76 my $optional = 'optional_' . $include;
191              
192 48 100       102 $location_fetch->( $set->{$include} ) if ( $set->{$include} );
193 48 100       98 $location_fetch->( delete( $conf->{$include} ) ) if ( $conf->{$include} );
194 48 50       78 $location_fetch->( \$set->{$optional} ) if ( $set->{$optional} );
195 48 100       86 $location_fetch->( \ delete( $conf->{$optional} ) ) if ( $conf->{$optional} );
196 24         75 };
197              
198 24         53 $fetch_block->(1);
199              
200 24         212 _merge_settings( $conf, $_ ) for (
201 360         777 grep { defined } (
202             map {
203 360         470 $set->{ join( '|', ( grep { defined } @$_ ) ) }
  840         1237  
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         106 $fetch_block->();
219              
220 24         157 return;
221             }
222              
223             sub _get_raw_config {
224 27     27   42 my ( $location, @source_path ) = @_;
225              
226 27         34 my $optional = 0;
227 27 100       50 if ( ref $location ) {
228 2         15 $location = $$location;
229 2         3 $optional = 1;
230             }
231              
232 27 50       129 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         1487 my ( $root_dir, $config_file ) = _find_root_dir($location);
258              
259 27 100       283 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         5 return '', '';
265             }
266             }
267             else {
268 26 50       858 open( my $config_fh, '<', $config_file ) or croak "Failed to read $config_file; $!";
269 26         1217 return join( '', <$config_fh> ), $root_dir;
270             }
271             }
272             }
273              
274             sub _find_root_dir {
275 32     32   53 my ($location) = @_;
276 32   33     66 $location ||= location();
277 32 50 33     76 return '/', $location if ( URI->new($location)->scheme or substr( $location, 0, 1 ) eq '/' );
278              
279 32         19443 my ( $root_dir, $config_file );
280 32         120 my @search_path = split( '/', $FindBin::Bin );
281 32         86 while ( @search_path > 1 ) {
282 67         177 $root_dir = join( '/', @search_path );
283 67         124 $config_file = $root_dir . '/' . $location;
284 67 100       920 last if ( -f $config_file );
285 36         129 pop @search_path;
286             }
287              
288 32         172 return $root_dir, $config_file;
289             }
290              
291             {
292             my $json_xs;
293              
294             sub _parse_config {
295 25     25   45 my ( $raw_config, $location, @source_path ) = @_;
296              
297 25         43 my @types = qw( yaml json );
298 25 100 66     153 if ( $location =~ /\.yaml$/ or $location =~ /\.yml$/ ) {
    100 66        
299 19         31 @types = ( 'yaml', grep { $_ ne 'yaml' } @types );
  38         94  
300             }
301             elsif ( $location =~ /\.json$/ or $location =~ /\.js$/ ) {
302 5         9 @types = ( 'json', grep { $_ ne 'json' } @types );
  10         32  
303             }
304              
305 25         50 my ( $config, @errors );
306 25         43 for my $type (@types) {
307 25         31 my $error = do {
308 25         29 local $@;
309 25         35 eval {
310 25 100       55 if ( $type eq 'json' ) {
311 5   66     77 $json_xs ||= JSON::XS->new
312             ->utf8
313             ->relaxed
314             ->allow_nonref
315             ->allow_unknown
316             ->allow_blessed
317             ->allow_tags;
318              
319 5         38 $config = $json_xs->decode($raw_config);
320             }
321             else {
322 20         1023 $config = YAML::XS::Load($raw_config);
323             }
324             };
325 25         90 $@;
326             };
327              
328 25 100       56 if ($error) {
329             my $message =
330             'Failed to parse ' .
331 1         2 join( ' -> ', map { "\"$_\"" } @source_path, $location ) . '; ' .
  1         8  
332             $error;
333 1 50       223 croak($message) if ( not $config );
334 0         0 carp($message);
335             }
336              
337 24 50       52 last if ($config);
338             }
339              
340 24         50 return $config;
341             }
342             }
343              
344             sub _merge_settings {
345 36     36   60 my ( $merge, $source, $is_deep_call ) = @_;
346 36 50       66 return unless $source;
347              
348 36 50 66     170 if ( not $is_deep_call and ref $merge eq 'HASH' and ref $source eq 'HASH' ) {
      66        
349 33 100       73 if ( my $libs = delete $source->{libs} ) {
350 3 100       11 if ( not exists $merge->{libs} ) {
    100          
351 1         3 $merge->{libs} = $libs;
352             }
353             elsif ( ref $merge->{libs} eq 'ARRAY' ) {
354 1 50       2 my %libs = map { $_ => 1 } @{ $merge->{libs} }, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  5         10  
  1         5  
355 1         19 $merge->{libs} = [ sort keys %libs ];
356             }
357             else {
358 1 50       6 my %libs = map { $_ => 1 } $merge->{libs}, ( ref $libs eq 'ARRAY' ) ? @$libs : $libs;
  4         10  
359 1         7 $merge->{libs} = [ sort keys %libs ];
360             }
361             }
362             }
363              
364 36 50       68 if ( ref $merge eq 'HASH' ) {
    0          
365 36         46 for my $key ( keys %{$source} ) {
  36         97  
366 53 100 100     184 if ( exists $merge->{$key} and ref $merge->{$key} eq 'HASH' and ref $source->{$key} eq 'HASH' ) {
      100        
367 3         27 _merge_settings( $merge->{$key}, $source->{$key}, 1 );
368             }
369             else {
370 50         82 $merge->{$key} = _clone( $source->{$key} );
371             }
372             }
373             }
374             elsif ( ref $merge eq 'ARRAY' ) {
375 0         0 push( @$source, @$merge );
376             }
377              
378 36         92 return;
379             }
380              
381             1;
382              
383             __END__