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