File Coverage

blib/lib/Config/Processor.pm
Criterion Covered Total %
statement 222 224 99.1
branch 80 90 88.8
condition 10 12 83.3
subroutine 21 21 100.0
pod 2 2 100.0
total 335 349 95.9


line stmt bran cond sub pod time code
1             package Config::Processor;
2              
3 4     4   57075 use 5.008000;
  4         14  
4 4     4   15 use strict;
  4         5  
  4         64  
5 4     4   11 use warnings;
  4         8  
  4         135  
6              
7             our $VERSION = '0.24';
8              
9 4     4   13 use File::Spec;
  4         6  
  4         76  
10 4     4   1429 use YAML::XS qw( LoadFile );
  4         7774  
  4         165  
11 4     4   2840 use Cpanel::JSON::XS;
  4         16680  
  4         218  
12 4     4   1659 use Hash::Merge;
  4         7129  
  4         156  
13 4     4   19 use Scalar::Util qw( refaddr readonly );
  4         5  
  4         167  
14 4     4   13 use Carp qw( croak );
  4         4  
  4         1074  
15              
16             my %FILE_EXTENSIONS_MAP = (
17             yml => 'yaml',
18             yaml => 'yaml',
19             json => 'json',
20             jsn => 'json',
21             );
22              
23             Hash::Merge::specify_behavior(
24             {
25             SCALAR => {
26             SCALAR => sub { $_[1] },
27             ARRAY => sub { $_[1] },
28             HASH => sub { $_[1] },
29             },
30             ARRAY => {
31             SCALAR => sub { $_[1] },
32             ARRAY => sub { $_[1] },
33             HASH => sub { $_[1] },
34             },
35             HASH => {
36             SCALAR => sub { $_[1] },
37             ARRAY => sub { $_[1] },
38             HASH => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) },
39             },
40             },
41             'CONFIG_PRECEDENT',
42             );
43              
44              
45             sub new {
46 7     7 1 10919 my $class = shift;
47 7         20 my %params = @_;
48              
49 7         12 my $self = bless {}, $class;
50              
51 7   100     39 $self->{dirs} = $params{dirs} || [];
52 7 100       9 unless ( @{ $self->{dirs} } ) {
  7         21  
53 2         3 push( @{ $self->{dirs} }, '.' );
  2         5  
54             }
55              
56             $self->{interpolate_variables} = exists $params{interpolate_variables}
57 7 100       21 ? $params{interpolate_variables} : 1;
58             $self->{process_directives} = exists $params{process_directives}
59 7 100       18 ? $params{process_directives} : 1;
60 7         12 $self->{export_env} = $params{export_env};
61              
62 7         46 $self->{_merger} = Hash::Merge->new('CONFIG_PRECEDENT');
63 7         90 $self->{_config} = undef;
64 7         11 $self->{_vars} = {};
65 7         14 $self->{_seen_nodes} = {};
66              
67 7         18 return $self;
68             }
69              
70             {
71 4     4   39 no strict 'refs';
  4         6  
  4         5339  
72              
73             foreach my $name (
74             qw( interpolate_variables process_directives export_env ) )
75             {
76             *{$name} = sub {
77 16     16   9058 my $self = shift;
78              
79 16 100       31 if (@_) {
80 7         12 $self->{$name} = shift;
81             }
82              
83 16         30 return $self->{$name};
84             }
85             }
86             }
87              
88             sub load {
89 15     15 1 17857 my $self = shift;
90 15         33 my @config_sections = @_;
91              
92 15         28 $self->{_config} = $self->_build_tree(@config_sections);
93 10 100       103 if ( $self->{export_env} ) {
94             $self->{_config} = $self->{_merger}->merge( $self->{_config},
95 1         21 { ENV => {%ENV} } );
96             }
97 10         45 $self->_process_tree( $self->{_config}, [] );
98              
99 9         13 $self->{_vars} = {};
100 9         19 $self->{_seen_nodes} = {};
101              
102 9         48 return $self->{_config};
103             }
104              
105             sub _build_tree {
106 29     29   21 my $self = shift;
107 29         40 my @config_sections = @_;
108              
109 29         25 my $config = {};
110              
111 29         71 foreach my $config_section (@config_sections) {
112 63 50       92 next unless defined $config_section;
113              
114 63 100       94 if ( ref($config_section) eq 'HASH' ) {
115 2         5 $config = $self->{_merger}->merge( $config, $config_section );
116             }
117             else {
118 61         50 my %not_found_idx;
119 61         165 my @file_patterns = split( /\s+/, $config_section );
120              
121 61         46 foreach my $dir ( @{ $self->{dirs} } ) {
  61         97  
122 62         56 foreach my $file_pattern ( @file_patterns ) {
123 64         1725 my @file_pathes = glob( File::Spec->catfile( $dir, $file_pattern ) );
124              
125 64         121 foreach my $file_path (@file_pathes) {
126 68 50       553 next if -d $file_path;
127              
128 68 100       290 unless ( $file_path =~ m/\.([^.]+)$/ ) {
129 1         149 croak "File extension not specified."
130             . " Don't known how parse $file_path";
131             }
132              
133 67         124 my $file_ext = $1;
134 67         83 my $file_type = $FILE_EXTENSIONS_MAP{$file_ext};
135              
136 67 100       112 unless ( defined $file_type ) {
137 1         72 croak "Unknown file extension \".$file_ext\" encountered."
138             . " Don't known how parse $file_path";
139             }
140              
141 66 100       446 unless ( -e $file_path ) {
142 4   100     10 $not_found_idx{$file_pattern} ||= 0;
143 4         3 $not_found_idx{$file_pattern}++;
144              
145 4         8 next;
146             }
147              
148 62         65 my @data = eval {
149 62         68 my $method = "_load_$file_type";
150 62         130 return $self->$method($file_path);
151             };
152 62 100       4458 if ($@) {
153 2         141 croak "Can't parse $file_path\n$@";
154             }
155              
156 60         98 foreach my $data_chunk (@data) {
157 60         171 $config = $self->{_merger}->merge( $config, $data_chunk );
158             }
159             }
160             }
161             }
162              
163             my @not_found = grep {
164 57         1400 $not_found_idx{$_} == scalar @{ $self->{dirs} }
  2         62  
  2         7  
165             } keys %not_found_idx;
166              
167 57 100       156 if ( @not_found ) {
168             croak "Can't locate " . join( ', ', @not_found )
169 1         4 . " in " . join( ', ', @{ $self->{dirs} } );
  1         72  
170             }
171             }
172             }
173              
174 24         84 return $config;
175             }
176              
177             sub _load_yaml {
178 41     41   39 my $self = shift;
179 41         33 my $file_path = shift;
180              
181 41         98 return LoadFile($file_path);
182             }
183              
184             sub _load_json {
185 21     21   26 my $self = shift;
186 21         13 my $file_path = shift;
187              
188 21 50       467 open( my $fh, '<', $file_path ) || die "Can't open $file_path: $!";
189 21         682 my @data = ( decode_json( join( '', <$fh> ) ) );
190 20         134 close($fh);
191              
192 20         73 return @data;
193             }
194              
195             sub _process_tree {
196 923     923   684 my $self = shift;
197 923         581 my $ancs = pop;
198              
199 923 100       1454 return if readonly( $_[0] );
200              
201 917         935 $_[0] = $self->_process_node( $_[0], $ancs );
202              
203 916 100       1624 if ( my $node_addr = refaddr( $_[0] ) ) {
204 264 100       412 return if $self->{_seen_nodes}{$node_addr};
205              
206 245         362 $self->{_seen_nodes}{$node_addr} = 1;
207             }
208              
209 897 100       1370 if ( ref( $_[0] ) eq 'HASH' ) {
    100          
210 170         116 foreach ( values %{ $_[0] } ) {
  170         370  
211 749         519 $self->_process_tree( $_, [ $_[0], @{$ancs} ] );
  749         1170  
212             }
213             }
214             elsif ( ref( $_[0] ) eq 'ARRAY' ) {
215 75         45 foreach ( @{ $_[0] } ) {
  75         117  
216 164         122 $self->_process_tree( $_, [ $_[0], @{$ancs} ] );
  164         283  
217             }
218             }
219              
220 895         1087 return;
221             }
222              
223             sub _process_node {
224 1046     1046   657 my $self = shift;
225 1046         718 my $node = shift;
226 1046         612 my $ancs = shift;
227              
228 1046 50       1239 return unless defined $node;
229              
230 1046 100 66     2444 if ( !ref($node) && $self->{interpolate_variables} ) {
    100 66        
231 655         685 $node =~ s/\$((\$?)\{([^\}]*)\})/
232 85 100 100     124 $2 ? $1 : ( $self->_resolve_var( $3, [ @{$ancs} ] ) || '' )/ge;
233             }
234             elsif ( ref($node) eq 'HASH' && $self->{process_directives} ) {
235 196 100       280 if ( defined $node->{var} ) {
    100          
236 35         29 $node = $self->_resolve_var( $node->{var}, [ @{$ancs} ] );
  35         56  
237             }
238             elsif ( defined $node->{include} ) {
239 14         21 $node = $self->_build_tree( $node->{include} );
240             }
241             else {
242 147 100       170 if ( defined $node->{underlay} ) {
243 18         20 my $layer = delete $node->{underlay};
244 18         22 $layer = $self->_process_layer( $layer, $ancs );
245 18         35 $node = $self->{_merger}->merge( $layer, $node );
246             }
247              
248 147 100       412 if ( defined $node->{overlay} ) {
249 15         13 my $layer = delete $node->{overlay};
250 15         18 $layer = $self->_process_layer( $layer, $ancs );
251 15         29 $node = $self->{_merger}->merge( $node, $layer );
252             }
253             }
254             }
255              
256 1045         1362 return $node;
257             }
258              
259             sub _process_layer {
260 33     33   25 my $self = shift;
261 33         20 my $layer = shift;
262 33         24 my $ancs = shift;
263              
264 33 100       48 if ( ref($layer) eq 'HASH' ) {
    100          
265 18         19 $layer = $self->_process_node( $layer, $ancs );
266             }
267             elsif ( ref($layer) eq 'ARRAY' ) {
268 11         11 my $new_layer = {};
269              
270 11         7 foreach my $node ( @{$layer} ) {
  11         14  
271 28         333 $node = $self->_process_node( $node, $ancs );
272 28         49 $new_layer = $self->{_merger}->merge( $new_layer, $node );
273             }
274              
275 11         119 $layer = $new_layer;
276             }
277              
278 33         28 return $layer;
279             }
280              
281             sub _resolve_var {
282 118     118   82 my $self = shift;
283 118         104 my $name = shift;
284 118         64 my $ancs = shift;
285              
286 118         73 my $value;
287              
288 118 100       141 if ( $name =~ m/^\./ ) {
289 10         6 my $node;
290 10         18 my @tokens = split( /\./, $name, -1 );
291              
292 10         5 while (1) {
293 24         17 my $token = $tokens[0];
294 24         22 $token =~ s/^\s+//;
295 24         10 $token =~ s/\s+$//;
296              
297 24 100       36 last if length($token) > 0;
298              
299 14         10 shift @tokens;
300              
301 14 50       18 last unless @tokens;
302 14 50       11 next unless @{$ancs};
  14         22  
303              
304 14         8 $node = shift @{$ancs};
  14         14  
305             }
306              
307 10         27 $value = eval {
308 10         15 $self->_fetch_value( $node, $ancs, \@tokens );
309             };
310              
311 10 50       16 if ($@) {
312 0         0 chomp $@;
313 0         0 die qq{Can't resolve variable "$name"; $@\n};
314             }
315             }
316             else {
317 108         84 my $vars = $self->{_vars};
318              
319 108 100       152 unless ( defined $vars->{$name} ) {
320 83         128 my @tokens = split( /\./, $name, -1 );
321              
322 83         62 $vars->{$name} = eval {
323 83         129 $self->_fetch_value( $self->{_config}, [], \@tokens );
324             };
325              
326 83 100       152 if ($@) {
327 1         2 chomp $@;
328 1         11 die qq{Can't resolve variable "$name"; $@\n};
329             }
330             }
331              
332 107         92 $value = $vars->{$name};
333             }
334              
335 117         258 return $value;
336             }
337              
338             ####
339             sub _fetch_value {
340 93     93   56 my $self = shift;
341 93         66 my $node = shift;
342 93         56 my $ancs = shift;
343 93         63 my $tokens = shift;
344              
345 93 50       51 return $node unless @{$tokens};
  93         130  
346              
347 93         65 my $value;
348              
349 93         54 while (1) {
350 240         129 my $token = shift @{$tokens};
  240         197  
351 240         261 $token =~ s/^\s+//;
352 240         195 $token =~ s/\s+$//;
353              
354 240 100       254 if ( ref($node) eq 'HASH' ) {
355 205 100       264 last unless defined $node->{$token};
356              
357 198         121 unshift( @{$ancs}, $node );
  198         184  
358              
359 198 100       160 unless ( @{$tokens} ) {
  198         237  
360 55         77 $node->{$token} = $self->_process_node( $node->{$token}, $ancs );
361 55         53 $value = $node->{$token};
362              
363 55         40 last;
364             }
365              
366 143 50       174 last unless ref( $node->{$token} );
367              
368 143         125 $node = $node->{$token};
369             }
370             else { # ARRAY
371 35 100       51 if ( $token =~ m/\D/ ) {
372 1         6 die qq{Argument "$token" isn't numeric in array element.\n};
373             }
374              
375 34 100       53 last unless defined $node->[$token];
376              
377 32         22 unshift( @{$ancs}, $node );
  32         36  
378              
379 32 100       18 unless ( @{$tokens} ) {
  32         42  
380 28         35 $node->[$token] = $self->_process_node( $node->[$token], $ancs );
381 28         27 $value = $node->[$token];
382              
383 28         19 last;
384             }
385              
386 4 50       7 last unless ref( $node->[$token] );
387              
388 4         3 $node = $node->[$token];
389             }
390             }
391              
392 92         156 return $value;
393             }
394              
395             1;
396             __END__