File Coverage

blib/lib/Parse/JCONF.pm
Criterion Covered Total %
statement 179 198 90.4
branch 79 122 64.7
condition 10 18 55.5
subroutine 22 22 100.0
pod 4 4 100.0
total 294 364 80.7


line stmt bran cond sub pod time code
1             package Parse::JCONF;
2              
3 10     10   217329 use strict;
  10         23  
  10         334  
4 10     10   52 use Carp;
  10         22  
  10         957  
5 10     10   5132 use Parse::JCONF::Boolean qw(TRUE FALSE);
  10         24  
  10         635  
6 10     10   5216 use Parse::JCONF::Error;
  10         26  
  10         27750  
7              
8             our $VERSION = '0.05';
9             our $HashClass = 'Tie::IxHash';
10              
11             sub new {
12 9     9 1 489 my ($class, %opts) = @_;
13            
14             my $self = {
15             autodie => delete $opts{autodie},
16             keep_order => delete $opts{keep_order}
17 9         48 };
18            
19 9 50       39 %opts and croak 'unrecognized options: ', join(', ', keys %opts);
20            
21 9 50       35 if ($self->{keep_order}) {
22 0 0       0 eval "require $HashClass"
23             or croak "you need to install $HashClass for `keep_order' option";
24             }
25            
26 9         33 bless $self, $class;
27             }
28              
29             sub parse {
30 24     24 1 6177 my ($self, $data) = @_;
31            
32 24         59 $self->_err(undef);
33            
34 24         29 my %rv;
35 24 50       67 if ($self->{keep_order}) {
36 0         0 tie %rv, $HashClass;
37             }
38            
39 24         35 my $offset = 0;
40 24         28 my $line = 1;
41 24         55 my $len = length $data;
42            
43 24   100     110 while ($offset < $len && $self->_parse_space_and_comments(\$data, \$offset, \$line)) {
44 51 50       141 $self->_parse_bareword(\$data, \$offset, \$line, \my $key)
45             or return;
46 51 100       137 $self->_parse_eq_sign(\$data, \$offset, \$line)
47             or return;
48 37 50       100 $self->_parse_value(\$data, \$offset, \$line, \my $val)
49             or return;
50 37 50       90 $self->_parse_delim(undef, \$data, \$offset, \$line)
51             or return;
52            
53 37         176 $rv{$key} = $val;
54             }
55            
56 10         49 return \%rv;
57             }
58              
59             sub _parse_space_and_comments {
60 571     571   703 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
61            
62 571         987 pos($$data_ref) = $$offset_ref;
63            
64 571         2273 while ($$data_ref =~ /\G(?:(\n+)|\s|#[^\n]*)/gc) {
65 358 100       1085 if (defined $1) {
66 109         352 $$line_ref += length $1;
67             }
68             }
69            
70 571         736 $$offset_ref = pos($$data_ref);
71 571         1522 return $$offset_ref < length $$data_ref;
72             }
73              
74             sub _parse_bareword {
75 80     80   113 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
76            
77 80 50       153 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
78             or return $self->_err(
79             Parser => "Unexpected end of data, expected bareword at line $$line_ref"
80             );
81            
82 80         143 pos($$data_ref) = $$offset_ref;
83            
84 80 50       275 $$data_ref =~ /\G(\w+)/g
85             or return $self->_err(
86             Parser => "Expected bareword at line $$line_ref:\n" . _parser_msg($data_ref, $$offset_ref)
87             );
88            
89 80         165 $$rv_ref = $1;
90 80         158 $$offset_ref = pos($$data_ref);
91            
92 80         184 1;
93             }
94              
95             sub _parse_bareword_or_string {
96 30     30   45 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
97            
98 30 50       68 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
99             or return $self->_err(
100             Parser => "Unexpected end of data, expected bareword or string at line $$line_ref"
101             );
102            
103 30         53 pos($$data_ref) = $$offset_ref;
104            
105 30 100       78 if (substr($$data_ref, $$offset_ref, 1) eq '"') {
106 1         3 $self->_parse_string($data_ref, $offset_ref, $line_ref, $rv_ref);
107             }
108             else {
109 29         50 $self->_parse_bareword($data_ref, $offset_ref, $line_ref, $rv_ref);
110             }
111             }
112              
113             sub _parse_delim {
114 110     110   154 my ($self, $ok_if, $data_ref, $offset_ref, $line_ref) = @_;
115            
116 110         120 my $line_was = $$line_ref;
117 110         206 my $has_data = $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref);
118            
119 110 100 100     484 if ($has_data && substr($$data_ref, $$offset_ref, 1) eq ',') {
120             # comma delimiter
121 49         48 $$offset_ref++;
122 49         122 return 1;
123             }
124            
125 61 100       118 if ($line_was != $$line_ref) {
126             # newline delimiter
127 47         121 return 1;
128             }
129            
130 14 0 33     29 if (!defined $ok_if && !$has_data) {
131             # we may not have delimiter at the end of data
132 0         0 return 1;
133             }
134            
135 14 50 33     61 if ($has_data && substr($$data_ref, $$offset_ref, 1) eq $ok_if) {
136             # we may not have delimiter at the end of object, array
137 14         34 return 1;
138             }
139            
140             $self->_err(
141 0         0 Parser => "Expected delimiter `,' at line $$line_ref:\n" . _parser_msg($data_ref, $$offset_ref)
142             );
143             }
144              
145             sub _parse_eq_sign {
146 51     51   78 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
147            
148 51 50       104 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
149             or return $self->_err(
150             Parser => "Unexpected end of data, expected equals sign `=' at line $$line_ref"
151             );
152            
153 51 100       142 unless (substr($$data_ref, $$offset_ref, 1) eq '=') {
154 14         45 return $self->_err(
155             Parser => "Expected equals sign `=' at line $$line_ref:\n" . _parser_msg($data_ref, $$offset_ref)
156             );
157             }
158            
159 37         40 $$offset_ref++;
160 37         87 1;
161             }
162              
163             sub _parse_value {
164 110     110   150 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
165            
166 110 50       184 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
167             or return $self->_err(
168             Parser => "Unexpected end of data, expected value at line $$line_ref"
169             );
170            
171 110         197 my $c = substr($$data_ref, $$offset_ref, 1);
172 110 100       469 if ($c eq '{') {
    100          
    100          
    100          
    100          
    100          
    50          
173 17         46 $self->_parse_object($data_ref, $offset_ref, $line_ref, $rv_ref);
174             }
175             elsif ($c eq '[') {
176 17         33 $self->_parse_array($data_ref, $offset_ref, $line_ref, $rv_ref);
177             }
178             elsif ($c eq 't') {
179 6         19 $self->_parse_constant('true', TRUE, $data_ref, $offset_ref, $line_ref, $rv_ref);
180             }
181             elsif ($c eq 'f') {
182 6         17 $self->_parse_constant('false', FALSE, $data_ref, $offset_ref, $line_ref, $rv_ref);
183             }
184             elsif ($c eq 'n') {
185 6         16 $self->_parse_constant('null', undef, $data_ref, $offset_ref, $line_ref, $rv_ref);
186             }
187             elsif ($c eq '"') {
188 23         53 $self->_parse_string($data_ref, $offset_ref, $line_ref, $rv_ref);
189             }
190             elsif ($c =~ /-|\d/) {
191 35         70 $self->_parse_number($data_ref, $offset_ref, $line_ref, $rv_ref);
192             }
193             else {
194 0         0 $self->_err(
195             Parser => "Unexpected value, expected array/object/string/number/true/false/null at line $$line_ref:\n" .
196             _parser_msg($data_ref, $$offset_ref)
197             );
198             }
199             }
200              
201             sub _parse_constant {
202 18     18   33 my ($self, $constant, $constant_val, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
203            
204 18         20 my $len = length $constant;
205 18 50 33     153 substr($$data_ref, $$offset_ref, $len) eq $constant &&
      33        
206             ($len + $$offset_ref == length $$data_ref || substr($$data_ref, $$offset_ref+$len, 1) =~ /[\s,\]}]/)
207             or return $self->_err(
208             Parser => "Unexpected value, expected `$constant' at line $$line_ref:\n" .
209             _parser_msg($data_ref, $$offset_ref)
210             );
211            
212 18         29 $$offset_ref += $len;
213 18         20 $$rv_ref = $constant_val;
214            
215 18         52 1;
216             }
217              
218             sub _parse_number {
219 35     35   47 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
220            
221 35 50       135 $$data_ref =~ /\G(-?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/gc
222             or return $self->_err(
223             Parser => "Unexpected value, expected number at line $$line_ref:\n" .
224             _parser_msg($data_ref, $$offset_ref)
225             );
226            
227 35         58 my $num = $1;
228 35         76 $$rv_ref = $num + 0; # WTF: $1 + 0 is string if we can believe Data::Dumper, so use temp var
229 35         48 $$offset_ref = pos($$data_ref);
230            
231 35         95 1;
232             }
233              
234             sub _parse_array {
235 17     17   27 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
236            
237 17         15 $$offset_ref++;
238 17         21 my @rv;
239            
240 17         18 while (1) {
241 60 50       112 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
242             or return $self->_err(
243             Parser => "Unexpected end of data, expected end of array `]' at line $$line_ref"
244             );
245            
246 60 100       145 substr($$data_ref, $$offset_ref, 1) eq ']'
247             and last;
248 43 50       109 $self->_parse_value($data_ref, $offset_ref, $line_ref, \my $val)
249             or return;
250 43 50       92 $self->_parse_delim(']', $data_ref, $offset_ref, $line_ref)
251             or return;
252            
253 43         72 push @rv, $val;
254             }
255            
256 17         26 $$rv_ref = \@rv;
257 17         25 $$offset_ref++;
258            
259 17         47 1;
260             }
261              
262             sub _parse_object {
263 17     17   24 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
264            
265 17         15 $$offset_ref++;
266 17         19 my %rv;
267 17 50       36 if ($self->{keep_order}) {
268 0         0 tie %rv, $HashClass;
269             }
270            
271 17         17 while (1) {
272 47 50       84 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
273             or return $self->_err(
274             Parser => "Unexpected end of data, expected end of object `}' at line $$line_ref"
275             );
276            
277 47 100       126 substr($$data_ref, $$offset_ref, 1) eq '}'
278             and last;
279 30 50       62 $self->_parse_bareword_or_string($data_ref, $offset_ref, $line_ref, \my $key)
280             or return;
281 30 50       64 $self->_parse_colon_sign($data_ref, $offset_ref, $line_ref)
282             or return;
283 30 50       64 $self->_parse_value($data_ref, $offset_ref, $line_ref, \my $val)
284             or return;
285 30 50       104 $self->_parse_delim('}', $data_ref, $offset_ref, $line_ref)
286             or return;
287            
288 30         84 $rv{$key} = $val;
289             }
290            
291 17         20 $$rv_ref = \%rv;
292 17         19 $$offset_ref++;
293            
294 17         61 1;
295             }
296              
297             sub _parse_colon_sign {
298 30     30   40 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
299            
300 30 50       49 $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref)
301             or return $self->_err(
302             Parser => "Unexpected end of data, expected colon sign `:' at line $$line_ref"
303             );
304            
305 30 50       79 unless (substr($$data_ref, $$offset_ref, 1) eq ':') {
306 0         0 return $self->_err(
307             Parser => "Expected colon sign `:' at line $$line_ref:\n" . _parser_msg($data_ref, $$offset_ref)
308             );
309             }
310            
311 30         30 $$offset_ref++;
312 30         62 1;
313             }
314              
315             my %ESCAPES = (
316             'b' => "\b",
317             'f' => "\f",
318             'n' => "\n",
319             'r' => "\r",
320             't' => "\t",
321             '"' => '"',
322             '\\' => '\\'
323             );
324              
325             sub _parse_string {
326 24     24   36 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
327            
328 24         41 pos($$data_ref) = ++$$offset_ref;
329 24         46 my $str = '';
330            
331 24         116 while ($$data_ref =~ /\G(?:(\n+)|\\((?:[bfnrt"\\]))|\\u([0-9a-fA-F]{4})|([^\\"\x{0}-\x{8}\x{A}-\x{C}\x{E}-\x{1F}]+))/gc) {
332 66 100       196 if (defined $1) {
    100          
    100          
333 10         18 $$line_ref += length $1;
334 10         38 $str .= $1;
335             }
336             elsif (defined $2) {
337 8         38 $str .= $ESCAPES{$2};
338             }
339             elsif (defined $3) {
340 7         63 $str .= pack 'U', hex $3;
341             }
342             else {
343 41         177 $str .= $4;
344             }
345             }
346            
347 24         43 $$offset_ref = pos($$data_ref);
348 24 50       46 if ($$offset_ref == length $$data_ref) {
349 0         0 return $self->_err(
350             Parser => "Unexpected end of data, expected string terminator `\"' at line $$line_ref"
351             );
352             }
353            
354 24 50       66 if ((my $c = substr($$data_ref, $$offset_ref, 1)) ne '"') {
355 0 0       0 if ($c eq '\\') {
356 0         0 return $self->_err(
357             Parser => "Unrecognized escape sequence in string at line $$line_ref:\n" .
358             _parser_msg($data_ref, $$offset_ref)
359             );
360             }
361             else {
362 0         0 my $hex = sprintf('"\x%02x"', ord $c);
363 0         0 return $self->_err(
364             Parser => "Bad character $hex in string at line $$line_ref:\n" .
365             _parser_msg($data_ref, $$offset_ref)
366             );
367             }
368             }
369            
370 24         26 $$offset_ref++;
371 24         31 $$rv_ref = $str;
372            
373 24         70 1;
374             }
375              
376             sub parse_file {
377 10     10 1 1438 my ($self, $path) = @_;
378            
379 10         42 $self->_err(undef);
380            
381 10 100       419 open my $fh, '<:utf8', $path
382             or return $self->_err(IO => "open `$path': $!");
383            
384 8         15 my $data = do {
385 8         28 local $/;
386 8         312 <$fh>;
387             };
388            
389 8         53 close $fh;
390            
391 8         34 $self->parse($data);
392             }
393              
394             sub last_error {
395 8     8 1 3247 return $_[0]->{last_error};
396             }
397              
398             sub _err {
399 50     50   85 my ($self, $err_type, $msg) = @_;
400            
401 50 100       174 unless (defined $err_type) {
402 34         83 $self->{last_error} = undef;
403 34         86 return;
404             }
405            
406 16         83 $self->{last_error} = "Parse::JCONF::Error::$err_type"->new($msg);
407 16 100       43 if ($self->{autodie}) {
408 8         25 $self->{last_error}->throw();
409             }
410            
411 8         42 return;
412             }
413              
414             sub _parser_msg {
415 14     14   19 my ($data_ref, $offset) = @_;
416            
417 14         17 my $msg = '';
418 14         15 my $non_space_chars = 0;
419 14         13 my $c;
420             my $i;
421            
422 14         35 for ($i=$offset; $i>=0; $i--) {
423 28         64 $c = substr($$data_ref, $i, 1);
424 28 50       85 if ($c eq "\n") {
    50          
    50          
425 0         0 last;
426             }
427             elsif ($c eq "\t") {
428 0         0 $c = ' ';
429             }
430             elsif (ord $c < 32) {
431 0         0 $c = ' ';
432             }
433            
434 28         35 substr($msg, 0, 0) = $c;
435            
436 28 50       75 if ($c =~ /\S/) {
437 28 50       80 if (++$non_space_chars > 5) {
438 0         0 last;
439             }
440             }
441             }
442            
443 14         18 substr($msg, 0, 0) = ' ';
444 14         15 my $bad_char = length $msg;
445            
446 14         16 my $len = length $$data_ref;
447 14         14 $non_space_chars = 0;
448            
449 14         38 for ($i=$offset+1; $i<$len; $i++) {
450 56         83 $c = substr($$data_ref, $i, 1);
451 56 50       150 if ($c eq "\n") {
    50          
    50          
452 0         0 last;
453             }
454             elsif ($c eq "\t") {
455 0         0 $c = ' ';
456             }
457             elsif (ord $c < 32) {
458 0         0 $c = ' ';
459             }
460            
461 56         64 substr($msg, length $msg) = $c;
462            
463 56 50       134 if ($c =~ /\S/) {
464 56 100       134 if (++$non_space_chars > 3) {
465 14         17 last;
466             }
467             }
468             }
469            
470 14         33 substr($msg, length $msg) = "\n" . ' 'x($bad_char-1).'^';
471 14         47 return $msg;
472             }
473              
474             1;
475              
476             __END__