File Coverage

lib/Config/Neat.pm
Criterion Covered Total %
statement 173 182 95.0
branch 96 110 87.2
condition 26 39 66.6
subroutine 12 12 100.0
pod 0 8 0.0
total 307 351 87.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Config::Neat - Parse/render human-readable configuration files with inheritance and schema validation
4              
5             =head1 SYNOPSIS
6              
7             use Config::Neat;
8              
9             my $cfg = Config::Neat->new();
10             my $data = $cfg->parse_file('server.nconf');
11              
12             =head1 DESCRIPTION
13              
14             This module provides parsing capabilites for the Config::Neat configuration file
15             format (see the example below). This is a highly readable and clean format inspired
16             by [nginx configuration files](http://wiki.nginx.org/FullExample).
17             See L
18             for the detailed file syntax specification.
19              
20             Sample configuration file (let's call it 'server.nconf'):
21              
22             # Server configuration
23              
24             host localhost
25             port 8080
26             use_ssl YES
27             supported_mime_types text/html text/css text/xml text/plain
28             image/gif image/jpeg image/png image/x-icon
29             application/x-javascript
30              
31             handler test1 {
32             url /test1
33             class MyApp::Test
34             }
35              
36             handler test2 {
37             url /test2
38             class MyApp::AnotherTest
39             }
40              
41             Sample usage:
42              
43             use Config::Neat;
44              
45             my $cfg = Config::Neat->new();
46             my $data = $cfg->parse_file('server.nconf');
47              
48             # now $data contains a parsed hash tree which you can examine
49              
50             # consider the sample configuration file above
51              
52             my $list = $data->{'server'}->{'supported_mime_types'};
53             #
54             # $list now is an array reference:
55             # ['text/html', 'text/css', ..., 'application/x-javascript']
56              
57             my $handlers = $data->{'handler'};
58             map {
59             print $_->{url}->as_string, ' maps to ', $_->{class}->as_string
60             } @$handlers;
61              
62             =head1 COPYRIGHT
63              
64             Copyright (C) 2012-2015 Igor Afanasyev
65              
66             =head1 SEE ALSO
67              
68             L
69              
70             =cut
71              
72             package Config::Neat;
73              
74             our $VERSION = '1.401';
75              
76 4     4   2242 use strict;
  4         7  
  4         118  
77              
78 4     4   1671 use Config::Neat::Array;
  4         9  
  4         137  
79 4     4   28 use Config::Neat::Util qw(is_neat_array new_ixhash get_next_auto_key read_file);
  4         8  
  4         264  
80 4     4   25 use Tie::IxHash;
  4         7  
  4         6576  
81              
82             my $LINE_START = 0;
83             my $KEY = 1;
84             my $WHITESPACE = 2;
85             my $VALUE = 3;
86             my $LINE_COMMENT = 4;
87             my $BLOCK_COMMENT = 5;
88              
89             #
90             # Initialize object
91             #
92             sub new {
93 5     5 0 866 my ($class) = @_;
94              
95 5         14 my $self = {
96             cfg => {}
97             };
98              
99 5         12 bless $self, $class;
100 5         14 return $self;
101             }
102              
103             # Given a string representation of the config, returns a parsed tree
104             sub parse {
105 98     98 0 501 my ($self, $nconf) = @_;
106              
107 98         253 my $o = {
108             context => [new_ixhash],
109             context_data => [{}],
110             c => undef,
111              
112             pos => 0,
113              
114             key => '',
115             values => Config::Neat::Array->new(),
116             value => undef,
117             mode => $LINE_START,
118             previous_mode => $LINE_START,
119             was_backslash => undef,
120             was_slash => undef,
121             was_asterisk => undef,
122             first_value_pos => 0,
123             };
124              
125 98         187 my $in_raw_mode = undef;
126 98         140 my $line = 1;
127              
128             sub end_of_param {
129 915     915 0 1379 my ($o, $no_default_param) = @_;
130              
131 915 100       1789 if ($o->{key} ne '') {
132 463 100 100     978 push @{$o->{values}}, 'YES' if !$no_default_param && scalar(@{$o->{values}}) == 0;
  11         26  
  278         727  
133 463         702 my $current_ctx = $o->{context}->[$#{$o->{context}}];
  463         913  
134 463         700 my $data = $o->{context_data}->[$#{$o->{context_data}}];
  463         738  
135 463 100       1842 if (exists $current_ctx->{$o->{key}}) {
136 18 100       117 $data->{is_array} = {} unless exists $data->{is_array};
137 18 100       54 if (!$data->{is_array}->{$o->{key}}) {
138 13         61 $current_ctx->{$o->{key}} = Config::Neat::Array->new([$current_ctx->{$o->{key}}]);
139 13         182 $data->{is_array}->{$o->{key}} = 1;
140             }
141 18         59 $current_ctx->{$o->{key}}->push($o->{values});
142             } else {
143 445         2850 $current_ctx->{$o->{key}} = $o->{values};
144             }
145 463         7200 $o->{values} = Config::Neat::Array->new();
146 463         871 $o->{key} = '';
147             }
148             }
149              
150             sub append_text {
151 4780     4780 0 7213 my ($o, $text) = @_;
152              
153 4780 100       9447 if ($o->{mode} == $LINE_START) {
    100          
154 454 100 100     1135 if (($o->{first_value_pos} > 0) and ($o->{pos} >= $o->{first_value_pos})) {
155 5         10 $o->{mode} = $VALUE;
156             } else {
157 449         958 end_of_param($o);
158 449         668 $o->{mode} = $KEY;
159 449         665 $o->{first_value_pos} = 0;
160             }
161             } elsif ($o->{mode} == $WHITESPACE) {
162 387         499 $o->{mode} = $VALUE;
163 387 100       686 if ($o->{first_value_pos} == 0) {
164 285         426 $o->{first_value_pos} = $o->{pos} - 1; # -1 to allow for non-hanging backtick before the first value
165             }
166             }
167              
168 4780 100       7710 if ($o->{mode} == $KEY) {
    50          
169 2555         3931 $o->{key} .= $text;
170             } elsif ($o->{mode} == $VALUE) {
171 2225         3601 $o->{value} .= $text;
172             } else {
173 0         0 die "Unexpected mode $o->{mode}";
174             }
175             }
176              
177             sub process_pending_chars {
178 6192     6192 0 7288 my $o = shift;
179              
180 6192 100       9831 if ($o->{was_slash}) {
181 78         156 append_text($o, '/');
182 78         146 $o->{was_slash} = undef;
183             }
184              
185 6192 100       10325 if ($o->{was_backslash}) {
186 14         27 append_text($o, '\\');
187 14         19 $o->{was_backslash} = undef;
188             }
189             }
190              
191             sub process_char {
192 4688     4688 0 5796 my $o = shift;
193              
194 4688         8321 process_pending_chars($o);
195              
196 4688         8615 append_text($o, $o->{c});
197 4688         6611 $o->{c} = undef;
198             }
199              
200             sub end_of_value {
201 1404     1404 0 1798 my $o = shift;
202              
203 1404         2498 process_pending_chars($o);
204              
205 1404 100       2503 if (defined $o->{value}) {
206 392         490 push @{$o->{values}}, $o->{value};
  392         1001  
207 392         696 $o->{value} = undef;
208             }
209             }
210              
211 98         382 for (my $i = 0, my $l = length($nconf); $i < $l; $i++) {
212 10713         17463 my $c = $o->{c} = substr($nconf, $i, 1);
213 10713         13998 $o->{pos}++;
214              
215 10713 100       18136 if ($c ne '/') {
216 10615         13234 $o->{was_asterisk} = undef;
217             }
218              
219 10713 100 66     43896 if ($c eq '{') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
220 185 50 33     592 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
221              
222 185 50       352 if ($in_raw_mode) {
223 0         0 process_char($o);
224 0         0 next;
225             }
226              
227 185         379 end_of_value($o);
228              
229 185 100       321 if (!$o->{key}) {
230 16         39 $o->{key} = get_next_auto_key($o->{context}->[$#{$o->{context}}]);
  16         91  
231             }
232              
233 185         277 my $old_values = $o->{values};
234 185         413 my $new_context = $o->{values} = new_ixhash;
235              
236 185         518 end_of_param($o, 1); # do not push a default parameter
237              
238 185         279 $o->{value} = undef;
239 185         300 $o->{mode} = $LINE_START;
240 185         273 $o->{first_value_pos} = 0;
241              
242 185         244 push @{$o->{context}}, $new_context;
  185         352  
243 185         246 push @{$o->{context_data}}, {};
  185         324  
244              
245             # any values preceding the block will be added into it with an empty key value
246 185 100       244 if (scalar(@{$old_values}) > 0) {
  185         453  
247 18         56 $new_context->{''} = $old_values;
248             }
249              
250             } elsif ($c eq '}') {
251 185 50 33     627 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
252              
253 185 50       334 if ($in_raw_mode) {
254 0         0 process_char($o);
255 0         0 next;
256             }
257              
258 185         364 end_of_value($o);
259 185         369 end_of_param($o);
260              
261 185 100       229 if (scalar(@{$o->{context}}) == 1) {
  185         380  
262 1         13 die "Unmatched closing bracket at config line $line position $o->{pos}";
263             }
264 184         249 pop @{$o->{context}};
  184         282  
265 184         240 pop @{$o->{context_data}};
  184         271  
266 184         360 $o->{mode} = $WHITESPACE;
267 184         267 $o->{key} = '';
268 184         389 $o->{values} = Config::Neat::Array->new();
269              
270             } elsif ($c eq '\\') {
271 19 50 33     55 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
272              
273 19         40 process_pending_chars($o);
274              
275 19         24 $o->{was_backslash} = 1; # do not print current slash, but wait for the next char
276 19         38 next;
277              
278             } elsif ($c eq '/') {
279 98 50       208 next if ($o->{mode} == $LINE_COMMENT);
280 98 100 100     308 next if (!$o->{was_asterisk} and $o->{mode} == $BLOCK_COMMENT);
281              
282 96 100       160 if ($in_raw_mode) {
283 12         37 process_char($o);
284 12         21 next;
285             }
286              
287 84 100 66     176 if ($o->{was_asterisk} and ($o->{mode} == $BLOCK_COMMENT)) {
288 3         5 $o->{mode} = $o->{previous_mode};
289 3         6 next;
290             }
291              
292 81         161 process_pending_chars($o);
293              
294 81         100 $o->{was_slash} = 1; # do not print current slash, but wait for the next char
295 81         178 next;
296              
297             } elsif ($c eq '*') {
298 28 50       49 next if ($o->{mode} == $LINE_COMMENT);
299              
300 28 100       45 if ($o->{mode} == $BLOCK_COMMENT) {
301 5         6 $o->{was_asterisk} = 1;
302 5         9 next;
303             } else {
304 23 100       49 if ($o->{was_slash}) {
305 3         5 $o->{was_slash} = undef;
306 3         5 $o->{previous_mode} = $o->{mode};
307 3         3 $o->{mode} = $BLOCK_COMMENT;
308 3         7 next;
309             }
310              
311 20         37 process_char($o);
312             }
313              
314             } elsif ($c eq '`') {
315 51 50 33     160 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
316              
317 51 100       90 if ($o->{was_backslash}) {
318 5         19 $o->{was_backslash} = undef;
319 5         14 process_char($o);
320 5         9 next;
321             }
322              
323 46         64 $o->{c} = '';
324 46         87 process_char($o);
325              
326 46         65 $in_raw_mode = !$in_raw_mode;
327              
328             } elsif (($c eq ' ') or ($c eq "\t")) {
329 4494 50       6918 if ($c eq "\t") {
330 0         0 warn "Tab symbol at config line $line position $o->{pos} (replace tabs with spaces to ensure proper parsing of multiline parameters)";
331             }
332              
333 4494 100 100     12043 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
334              
335 4444 100       6845 if ($in_raw_mode) {
336 35         69 process_char($o);
337 35         67 next;
338             }
339              
340 4409 100       8419 if ($o->{mode} == $KEY) {
    100          
341 344         554 $o->{mode} = $WHITESPACE;
342             } elsif ($o->{mode} == $VALUE) {
343 102         229 end_of_value($o);
344 102         146 $o->{mode} = $WHITESPACE;
345             }
346              
347             } elsif ($c eq "\r") {
348 0         0 next;
349              
350             } elsif ($c eq "\n") {
351 838         1130 $line++;
352 838         1176 $o->{pos} = 0;
353              
354 838 100       1484 next if ($o->{mode} == $BLOCK_COMMENT);
355              
356 836 50       1342 if ($in_raw_mode) {
357 0         0 process_char($o);
358 0         0 next;
359             }
360              
361 836         1717 end_of_value($o);
362 836         1161 $o->{mode} = $LINE_START;
363              
364             } elsif ($c eq "#") {
365 57 50 33     186 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
366              
367 57 100       170 if ($in_raw_mode) {
368 6         14 process_char($o);
369 6         14 next;
370             }
371              
372 51 100 66     142 if (($o->{mode} == $LINE_START) or ($o->{mode} == $WHITESPACE)) {
373 11         20 $o->{mode} = $LINE_COMMENT;
374             } else {
375 40         71 process_char($o);
376             }
377              
378             } else {
379 4758 100 100     12923 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
380              
381 4524         6814 process_char($o);
382             }
383              
384 10255         18479 $o->{was_asterisk} = undef;
385             }
386              
387 97 50       195 die "Unmatched backtick at config line $line position $o->{pos}" if $in_raw_mode;
388              
389 97 100       121 die "Missing closing bracket at config line $line position $o->{pos}" if @{$o->{context}} > 1;
  97         235  
390              
391 96         207 end_of_value($o);
392 96         189 end_of_param($o);
393              
394 96         820 return $self->{cfg} = $o->{context}->[0];
395             } # end sub
396              
397             # Given file name, will read this file in the specified mode (defaults to UTF-8) and parse it
398             sub parse_file {
399 5     5 0 11366 my ($self, $filename, $binmode) = @_;
400 5         19 return $self->parse(read_file($filename, $binmode));
401             } # end sub
402              
403             1; # return true