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.302';
75              
76 4     4   1545 use strict;
  4         8  
  4         109  
77              
78 4     4   1328 use Config::Neat::Array;
  4         11  
  4         120  
79 4     4   26 use Config::Neat::Util qw(is_neat_array new_ixhash get_next_auto_key read_file);
  4         6  
  4         187  
80 4     4   24 use Tie::IxHash;
  4         18  
  4         5250  
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 842 my ($class) = @_;
94              
95 5         16 my $self = {
96             cfg => {}
97             };
98              
99 5         21 bless $self, $class;
100 5         12 return $self;
101             }
102              
103             # Given a string representation of the config, returns a parsed tree
104             sub parse {
105 98     98 0 547 my ($self, $nconf) = @_;
106              
107 98         302 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         248 my $in_raw_mode = undef;
126 98         165 my $line = 1;
127              
128             sub end_of_param {
129 914     914 0 1573 my ($o, $no_default_param) = @_;
130              
131 914 100       2094 if ($o->{key} ne '') {
132 462 100 100     1144 push @{$o->{values}}, 'YES' if !$no_default_param && scalar(@{$o->{values}}) == 0;
  11         28  
  277         947  
133 462         799 my $current_ctx = $o->{context}->[$#{$o->{context}}];
  462         902  
134 462         775 my $data = $o->{context_data}->[$#{$o->{context_data}}];
  462         756  
135 462 100       1941 if (exists $current_ctx->{$o->{key}}) {
136 18 100       142 $data->{is_array} = {} unless exists $data->{is_array};
137 18 100       67 if (!$data->{is_array}->{$o->{key}}) {
138 13         62 $current_ctx->{$o->{key}} = Config::Neat::Array->new([$current_ctx->{$o->{key}}]);
139 13         188 $data->{is_array}->{$o->{key}} = 1;
140             }
141 18         72 $current_ctx->{$o->{key}}->push($o->{values});
142             } else {
143 444         2978 $current_ctx->{$o->{key}} = $o->{values};
144             }
145 462         7029 $o->{values} = Config::Neat::Array->new();
146 462         891 $o->{key} = '';
147             }
148             }
149              
150             sub append_text {
151 4753     4753 0 7779 my ($o, $text) = @_;
152              
153 4753 100       11592 if ($o->{mode} == $LINE_START) {
    100          
154 453 100 100     1388 if (($o->{first_value_pos} > 0) and ($o->{pos} >= $o->{first_value_pos})) {
155 5         10 $o->{mode} = $VALUE;
156             } else {
157 448         974 end_of_param($o);
158 448         691 $o->{mode} = $KEY;
159 448         701 $o->{first_value_pos} = 0;
160             }
161             } elsif ($o->{mode} == $WHITESPACE) {
162 385         570 $o->{mode} = $VALUE;
163 385 100       812 if ($o->{first_value_pos} == 0) {
164 284         460 $o->{first_value_pos} = $o->{pos} - 1; # -1 to allow for non-hanging backtick before the first value
165             }
166             }
167              
168 4753 100       9091 if ($o->{mode} == $KEY) {
    50          
169 2538         4173 $o->{key} .= $text;
170             } elsif ($o->{mode} == $VALUE) {
171 2215         3661 $o->{value} .= $text;
172             } else {
173 0         0 die "Unexpected mode $o->{mode}";
174             }
175             }
176              
177             sub process_pending_chars {
178 6163     6163 0 8151 my $o = shift;
179              
180 6163 100       11895 if ($o->{was_slash}) {
181 78         192 append_text($o, '/');
182 78         115 $o->{was_slash} = undef;
183             }
184              
185 6163 100       12715 if ($o->{was_backslash}) {
186 14         27 append_text($o, '\\');
187 14         20 $o->{was_backslash} = undef;
188             }
189             }
190              
191             sub process_char {
192 4661     4661 0 6480 my $o = shift;
193              
194 4661         9199 process_pending_chars($o);
195              
196 4661         9618 append_text($o, $o->{c});
197 4661         7205 $o->{c} = undef;
198             }
199              
200             sub end_of_value {
201 1402     1402 0 1950 my $o = shift;
202              
203 1402         2772 process_pending_chars($o);
204              
205 1402 100       2935 if (defined $o->{value}) {
206 390         517 push @{$o->{values}}, $o->{value};
  390         999  
207 390         725 $o->{value} = undef;
208             }
209             }
210              
211 98         452 for (my $i = 0, my $l = length($nconf); $i < $l; $i++) {
212 10667         19577 my $c = $o->{c} = substr($nconf, $i, 1);
213 10667         14827 $o->{pos}++;
214              
215 10667 100       20515 if ($c ne '/') {
216 10569         14674 $o->{was_asterisk} = undef;
217             }
218              
219 10667 100 66     56671 if ($c eq '{') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
220 185 50 33     758 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
221              
222 185 50       389 if ($in_raw_mode) {
223 0         0 process_char($o);
224 0         0 next;
225             }
226              
227 185         398 end_of_value($o);
228              
229 185 100       394 if (!$o->{key}) {
230 16         30 $o->{key} = get_next_auto_key($o->{context}->[$#{$o->{context}}]);
  16         62  
231             }
232              
233 185         318 my $old_values = $o->{values};
234 185         463 my $new_context = $o->{values} = new_ixhash;
235              
236 185         466 end_of_param($o, 1); # do not push a default parameter
237              
238 185         284 $o->{value} = undef;
239 185         297 $o->{mode} = $LINE_START;
240 185         274 $o->{first_value_pos} = 0;
241              
242 185         278 push @{$o->{context}}, $new_context;
  185         355  
243 185         294 push @{$o->{context_data}}, {};
  185         343  
244              
245             # any values preceding the block will be added into it with an empty key value
246 185 100       272 if (scalar(@{$old_values}) > 0) {
  185         476  
247 18         53 $new_context->{''} = $old_values;
248             }
249              
250             } elsif ($c eq '}') {
251 185 50 33     723 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
252              
253 185 50       405 if ($in_raw_mode) {
254 0         0 process_char($o);
255 0         0 next;
256             }
257              
258 185         404 end_of_value($o);
259 185         389 end_of_param($o);
260              
261 185 100       264 if (scalar(@{$o->{context}}) == 1) {
  185         429  
262 1         21 die "Unmatched closing bracket at config line $line position $o->{pos}";
263             }
264 184         266 pop @{$o->{context}};
  184         317  
265 184         261 pop @{$o->{context_data}};
  184         309  
266 184         346 $o->{mode} = $WHITESPACE;
267 184         281 $o->{key} = '';
268 184         437 $o->{values} = Config::Neat::Array->new();
269              
270             } elsif ($c eq '\\') {
271 19 50 33     78 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
272              
273 19         39 process_pending_chars($o);
274              
275 19         29 $o->{was_backslash} = 1; # do not print current slash, but wait for the next char
276 19         39 next;
277              
278             } elsif ($c eq '/') {
279 98 50       316 next if ($o->{mode} == $LINE_COMMENT);
280 98 100 100     413 next if (!$o->{was_asterisk} and $o->{mode} == $BLOCK_COMMENT);
281              
282 96 100       211 if ($in_raw_mode) {
283 12         28 process_char($o);
284 12         26 next;
285             }
286              
287 84 100 66     232 if ($o->{was_asterisk} and ($o->{mode} == $BLOCK_COMMENT)) {
288 3         5 $o->{mode} = $o->{previous_mode};
289 3         8 next;
290             }
291              
292 81         190 process_pending_chars($o);
293              
294 81         123 $o->{was_slash} = 1; # do not print current slash, but wait for the next char
295 81         205 next;
296              
297             } elsif ($c eq '*') {
298 28 50       63 next if ($o->{mode} == $LINE_COMMENT);
299              
300 28 100       56 if ($o->{mode} == $BLOCK_COMMENT) {
301 5         12 $o->{was_asterisk} = 1;
302 5         15 next;
303             } else {
304 23 100       51 if ($o->{was_slash}) {
305 3         7 $o->{was_slash} = undef;
306 3         6 $o->{previous_mode} = $o->{mode};
307 3         5 $o->{mode} = $BLOCK_COMMENT;
308 3         9 next;
309             }
310              
311 20         33 process_char($o);
312             }
313              
314             } elsif ($c eq '`') {
315 49 50 33     192 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
316              
317 49 100       109 if ($o->{was_backslash}) {
318 5         10 $o->{was_backslash} = undef;
319 5         10 process_char($o);
320 5         11 next;
321             }
322              
323 44         64 $o->{c} = '';
324 44         93 process_char($o);
325              
326 44         74 $in_raw_mode = !$in_raw_mode;
327              
328             } elsif (($c eq ' ') or ($c eq "\t")) {
329 4474 50       8768 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 4474 100 100     15309 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
334              
335 4424 100       8406 if ($in_raw_mode) {
336 33         75 process_char($o);
337 33         87 next;
338             }
339              
340 4391 100       10061 if ($o->{mode} == $KEY) {
    100          
341 343         535 $o->{mode} = $WHITESPACE;
342             } elsif ($o->{mode} == $VALUE) {
343 101         213 end_of_value($o);
344 101         155 $o->{mode} = $WHITESPACE;
345             }
346              
347             } elsif ($c eq "\r") {
348 0         0 next;
349              
350             } elsif ($c eq "\n") {
351 837         1244 $line++;
352 837         1198 $o->{pos} = 0;
353              
354 837 100       1831 next if ($o->{mode} == $BLOCK_COMMENT);
355              
356 835 50       1657 if ($in_raw_mode) {
357 0         0 process_char($o);
358 0         0 next;
359             }
360              
361 835         1720 end_of_value($o);
362 835         1214 $o->{mode} = $LINE_START;
363              
364             } elsif ($c eq "#") {
365 57 50 33     264 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
366              
367 57 100       133 if ($in_raw_mode) {
368 6         14 process_char($o);
369 6         15 next;
370             }
371              
372 51 100 66     182 if (($o->{mode} == $LINE_START) or ($o->{mode} == $WHITESPACE)) {
373 11         24 $o->{mode} = $LINE_COMMENT;
374             } else {
375 40         73 process_char($o);
376             }
377              
378             } else {
379 4735 100 100     17419 next if ($o->{mode} == $LINE_COMMENT) or ($o->{mode} == $BLOCK_COMMENT);
380              
381 4501         7853 process_char($o);
382             }
383              
384 10211         22173 $o->{was_asterisk} = undef;
385             }
386              
387 97 50       233 die "Unmatched backtick at config line $line position $o->{pos}" if $in_raw_mode;
388              
389 97 100       150 die "Missing closing bracket at config line $line position $o->{pos}" if @{$o->{context}} > 1;
  97         288  
390              
391 96         228 end_of_value($o);
392 96         224 end_of_param($o);
393              
394 96         880 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 11302 my ($self, $filename, $binmode) = @_;
400 5         33 return $self->parse(read_file($filename, $binmode));
401             } # end sub
402              
403             1; # return true