File Coverage

blib/lib/Config/Processor.pm
Criterion Covered Total %
statement 231 235 98.3
branch 80 90 88.8
condition 11 12 91.6
subroutine 28 30 93.3
pod 2 2 100.0
total 352 369 95.3


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