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.4';
75              
76 4     4   2045 use strict;
  4         7  
  4         105  
77              
78 4     4   1416 use Config::Neat::Array;
  4         8  
  4         118  
79 4     4   22 use Config::Neat::Util qw(is_neat_array new_ixhash get_next_auto_key read_file);
  4         6  
  4         188  
80 4     4   20 use Tie::IxHash;
  4         6  
  4         5696  
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 837 my ($class) = @_;
94              
95 5         12 my $self = {
96             cfg => {}
97             };
98              
99 5         12 bless $self, $class;
100 5         13 return $self;
101             }
102              
103             # Given a string representation of the config, returns a parsed tree
104             sub parse {
105 98     98 0 440 my ($self, $nconf) = @_;
106              
107 98         206 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         146 my $in_raw_mode = undef;
126 98         122 my $line = 1;
127              
128             sub end_of_param {
129 915     915 0 1133 my ($o, $no_default_param) = @_;
130              
131 915 100       1467 if ($o->{key} ne '') {
132 463 100 100     745 push @{$o->{values}}, 'YES' if !$no_default_param && scalar(@{$o->{values}}) == 0;
  11         20  
  278         580  
133 463         556 my $current_ctx = $o->{context}->[$#{$o->{context}}];
  463         705  
134 463         535 my $data = $o->{context_data}->[$#{$o->{context_data}}];
  463         594  
135 463 100       1467 if (exists $current_ctx->{$o->{key}}) {
136 18 100       96 $data->{is_array} = {} unless exists $data->{is_array};
137 18 100       47 if (!$data->{is_array}->{$o->{key}}) {
138 13         45 $current_ctx->{$o->{key}} = Config::Neat::Array->new([$current_ctx->{$o->{key}}]);
139 13         145 $data->{is_array}->{$o->{key}} = 1;
140             }
141 18         50 $current_ctx->{$o->{key}}->push($o->{values});
142             } else {
143 445         2428 $current_ctx->{$o->{key}} = $o->{values};
144             }
145 463         5859 $o->{values} = Config::Neat::Array->new();
146 463         667 $o->{key} = '';
147             }
148             }
149              
150             sub append_text {
151 4780     4780 0 5798 my ($o, $text) = @_;
152              
153 4780 100       7632 if ($o->{mode} == $LINE_START) {
    100          
154 454 100 100     946 if (($o->{first_value_pos} > 0) and ($o->{pos} >= $o->{first_value_pos})) {
155 5         6 $o->{mode} = $VALUE;
156             } else {
157 449         696 end_of_param($o);
158 449         501 $o->{mode} = $KEY;
159 449         547 $o->{first_value_pos} = 0;
160             }
161             } elsif ($o->{mode} == $WHITESPACE) {
162 387         437 $o->{mode} = $VALUE;
163 387 100       532 if ($o->{first_value_pos} == 0) {
164 285         367 $o->{first_value_pos} = $o->{pos} - 1; # -1 to allow for non-hanging backtick before the first value
165             }
166             }
167              
168 4780 100       6322 if ($o->{mode} == $KEY) {
    50          
169 2555         3151 $o->{key} .= $text;
170             } elsif ($o->{mode} == $VALUE) {
171 2225         2708 $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 5992 my $o = shift;
179              
180 6192 100       7926 if ($o->{was_slash}) {
181 78         137 append_text($o, '/');
182 78         122 $o->{was_slash} = undef;
183             }
184              
185 6192 100       8496 if ($o->{was_backslash}) {
186 14         23 append_text($o, '\\');
187 14         18 $o->{was_backslash} = undef;
188             }
189             }
190              
191             sub process_char {
192 4688     4688 0 4805 my $o = shift;
193              
194 4688         6888 process_pending_chars($o);
195              
196 4688         6910 append_text($o, $o->{c});
197 4688         5400 $o->{c} = undef;
198             }
199              
200             sub end_of_value {
201 1404     1404 0 1417 my $o = shift;
202              
203 1404         2009 process_pending_chars($o);
204              
205 1404 100       1998 if (defined $o->{value}) {
206 392         393 push @{$o->{values}}, $o->{value};
  392         771  
207 392         547 $o->{value} = undef;
208             }
209             }
210              
211 98         320 for (my $i = 0, my $l = length($nconf); $i < $l; $i++) {
212 10713         14214 my $c = $o->{c} = substr($nconf, $i, 1);
213 10713         10608 $o->{pos}++;
214              
215 10713 100       13903 if ($c ne '/') {
216 10615         10715 $o->{was_asterisk} = undef;
217             }
218              
219 10713 100 66     35582 if ($c eq '{') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
220 185 50 33     496 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
221              
222 185 50       256 if ($in_raw_mode) {
223 0         0 process_char($o);
224 0         0 next;
225             }
226              
227 185         316 end_of_value($o);
228              
229 185 100       261 if (!$o->{key}) {
230 16         32 $o->{key} = get_next_auto_key($o->{context}->[$#{$o->{context}}]);
  16         50  
231             }
232              
233 185         204 my $old_values = $o->{values};
234 185         363 my $new_context = $o->{values} = new_ixhash;
235              
236 185         330 end_of_param($o, 1); # do not push a default parameter
237              
238 185         226 $o->{value} = undef;
239 185         208 $o->{mode} = $LINE_START;
240 185         201 $o->{first_value_pos} = 0;
241              
242 185         185 push @{$o->{context}}, $new_context;
  185         269  
243 185         208 push @{$o->{context_data}}, {};
  185         265  
244              
245             # any values preceding the block will be added into it with an empty key value
246 185 100       207 if (scalar(@{$old_values}) > 0) {
  185         321  
247 18         62 $new_context->{''} = $old_values;
248             }
249              
250             } elsif ($c eq '}') {
251 185 50 33     520 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
252              
253 185 50       253 if ($in_raw_mode) {
254 0         0 process_char($o);
255 0         0 next;
256             }
257              
258 185         305 end_of_value($o);
259 185         288 end_of_param($o);
260              
261 185 100       188 if (scalar(@{$o->{context}}) == 1) {
  185         310  
262 1         12 die "Unmatched closing bracket at config line $line position $o->{pos}";
263             }
264 184         198 pop @{$o->{context}};
  184         266  
265 184         208 pop @{$o->{context_data}};
  184         213  
266 184         270 $o->{mode} = $WHITESPACE;
267 184         207 $o->{key} = '';
268 184         291 $o->{values} = Config::Neat::Array->new();
269              
270             } elsif ($c eq '\\') {
271 19 50 33     60 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
272              
273 19         34 process_pending_chars($o);
274              
275 19         23 $o->{was_backslash} = 1; # do not print current slash, but wait for the next char
276 19         36 next;
277              
278             } elsif ($c eq '/') {
279 98 50       172 next if ($o->{mode} == $LINE_COMMENT);
280 98 100 100     269 next if (!$o->{was_asterisk} and $o->{mode} == $BLOCK_COMMENT);
281              
282 96 100       137 if ($in_raw_mode) {
283 12         32 process_char($o);
284 12         19 next;
285             }
286              
287 84 100 66     158 if ($o->{was_asterisk} and ($o->{mode} == $BLOCK_COMMENT)) {
288 3         4 $o->{mode} = $o->{previous_mode};
289 3         6 next;
290             }
291              
292 81         147 process_pending_chars($o);
293              
294 81         93 $o->{was_slash} = 1; # do not print current slash, but wait for the next char
295 81         144 next;
296              
297             } elsif ($c eq '*') {
298 28 50       43 next if ($o->{mode} == $LINE_COMMENT);
299              
300 28 100       41 if ($o->{mode} == $BLOCK_COMMENT) {
301 5         7 $o->{was_asterisk} = 1;
302 5         7 next;
303             } else {
304 23 100       49 if ($o->{was_slash}) {
305 3         4 $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         32 process_char($o);
312             }
313              
314             } elsif ($c eq '`') {
315 51 50 33     138 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
316              
317 51 100       87 if ($o->{was_backslash}) {
318 5         14 $o->{was_backslash} = undef;
319 5         13 process_char($o);
320 5         8 next;
321             }
322              
323 46         54 $o->{c} = '';
324 46         74 process_char($o);
325              
326 46         60 $in_raw_mode = !$in_raw_mode;
327              
328             } elsif (($c eq ' ') or ($c eq "\t")) {
329 4494 50       5656 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     9783 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
334              
335 4444 100       5533 if ($in_raw_mode) {
336 35         55 process_char($o);
337 35         50 next;
338             }
339              
340 4409 100       6858 if ($o->{mode} == $KEY) {
    100          
341 344         375 $o->{mode} = $WHITESPACE;
342             } elsif ($o->{mode} == $VALUE) {
343 102         164 end_of_value($o);
344 102         110 $o->{mode} = $WHITESPACE;
345             }
346              
347             } elsif ($c eq "\r") {
348 0         0 next;
349              
350             } elsif ($c eq "\n") {
351 838         867 $line++;
352 838         852 $o->{pos} = 0;
353              
354 838 100       1219 next if ($o->{mode} == $BLOCK_COMMENT);
355              
356 836 50       1089 if ($in_raw_mode) {
357 0         0 process_char($o);
358 0         0 next;
359             }
360              
361 836         1294 end_of_value($o);
362 836         903 $o->{mode} = $LINE_START;
363              
364             } elsif ($c eq "#") {
365 57 50 33     167 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
366              
367 57 100       130 if ($in_raw_mode) {
368 6         12 process_char($o);
369 6         11 next;
370             }
371              
372 51 100 66     129 if (($o->{mode} == $LINE_START) or ($o->{mode} == $WHITESPACE)) {
373 11         16 $o->{mode} = $LINE_COMMENT;
374             } else {
375 40         60 process_char($o);
376             }
377              
378             } else {
379 4758 100 100     10638 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
380              
381 4524         5427 process_char($o);
382             }
383              
384 10255         14972 $o->{was_asterisk} = undef;
385             }
386              
387 97 50       166 die "Unmatched backtick at config line $line position $o->{pos}" if $in_raw_mode;
388              
389 97 100       101 die "Missing closing bracket at config line $line position $o->{pos}" if @{$o->{context}} > 1;
  97         213  
390              
391 96         176 end_of_value($o);
392 96         164 end_of_param($o);
393              
394 96         674 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 11108 my ($self, $filename, $binmode) = @_;
400 5         20 return $self->parse(read_file($filename, $binmode));
401             } # end sub
402              
403             1; # return true