File Coverage

blib/lib/Config/App.pm
Criterion Covered Total %
statement 181 193 93.7
branch 64 88 72.7
condition 28 40 70.0
subroutine 25 25 100.0
pod 4 4 100.0
total 302 350 86.2


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