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   229071 use strict;
  10         21  
  10         364  
4 10     10   55 use Carp;
  10         21  
  10         938  
5 10     10   5157 use Parse::JCONF::Boolean qw(TRUE FALSE);
  10         26  
  10         724  
6 10     10   5217 use Parse::JCONF::Error;
  10         24  
  10         38622  
7              
8             our $VERSION = '0.04';
9             our $HashClass = 'Tie::IxHash';
10              
11             sub new {
12 9     9 1 462 my ($class, %opts) = @_;
13            
14 9         52 my $self = {
15             autodie => delete $opts{autodie},
16             keep_order => delete $opts{keep_order}
17             };
18            
19 9 50       46 %opts and croak 'unrecognized options: ', join(', ', keys %opts);
20            
21 9 50       37 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         43 bless $self, $class;
27             }
28              
29             sub parse {
30 24     24 1 7470 my ($self, $data) = @_;
31            
32 24         61 $self->_err(undef);
33            
34 24         26 my %rv;
35 24 50       69 if ($self->{keep_order}) {
36 0         0 tie %rv, $HashClass;
37             }
38            
39 24         30 my $offset = 0;
40 24         30 my $line = 1;
41 24         76 my $len = length $data;
42            
43 24   100     121 while ($offset < $len && $self->_parse_space_and_comments(\$data, \$offset, \$line)) {
44 51 50       181 $self->_parse_bareword(\$data, \$offset, \$line, \my $key)
45             or return;
46 51 100       143 $self->_parse_eq_sign(\$data, \$offset, \$line)
47             or return;
48 37 50       99 $self->_parse_value(\$data, \$offset, \$line, \my $val)
49             or return;
50 37 50       98 $self->_parse_delim(undef, \$data, \$offset, \$line)
51             or return;
52            
53 37         189 $rv{$key} = $val;
54             }
55            
56 10         63 return \%rv;
57             }
58              
59             sub _parse_space_and_comments {
60 571     571   686 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
61            
62 571         1023 pos($$data_ref) = $$offset_ref;
63            
64 571         2068 while ($$data_ref =~ /\G(?:(\n+)|\s|#[^\n]+)/gc) {
65 357 100       1178 if (defined $1) {
66 109         379 $$line_ref += length $1;
67             }
68             }
69            
70 571         733 $$offset_ref = pos($$data_ref);
71 571         1608 return $$offset_ref < length $$data_ref;
72             }
73              
74             sub _parse_bareword {
75 80     80   123 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
76            
77 80 50       133 $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         136 pos($$data_ref) = $$offset_ref;
83            
84 80 50       300 $$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         177 $$rv_ref = $1;
90 80         107 $$offset_ref = pos($$data_ref);
91            
92 80         203 1;
93             }
94              
95             sub _parse_bareword_or_string {
96 30     30   77 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
97            
98 30 50       51 $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         56 pos($$data_ref) = $$offset_ref;
104            
105 30 100       75 if (substr($$data_ref, $$offset_ref, 1) eq '"') {
106 1         5 $self->_parse_string($data_ref, $offset_ref, $line_ref, $rv_ref);
107             }
108             else {
109 29         54 $self->_parse_bareword($data_ref, $offset_ref, $line_ref, $rv_ref);
110             }
111             }
112              
113             sub _parse_delim {
114 110     110   159 my ($self, $ok_if, $data_ref, $offset_ref, $line_ref) = @_;
115            
116 110         124 my $line_was = $$line_ref;
117 110         193 my $has_data = $self->_parse_space_and_comments($data_ref, $offset_ref, $line_ref);
118            
119 110 100 100     559 if ($has_data && substr($$data_ref, $$offset_ref, 1) eq ',') {
120             # comma delimiter
121 49         51 $$offset_ref++;
122 49         129 return 1;
123             }
124            
125 61 100       125 if ($line_was != $$line_ref) {
126             # newline delimiter
127 47         143 return 1;
128             }
129            
130 14 0 33     34 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     83 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         39 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   68 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
147            
148 51 50       119 $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       152 unless (substr($$data_ref, $$offset_ref, 1) eq '=') {
154 14         80 return $self->_err(
155             Parser => "Expected equals sign `=' at line $$line_ref:\n" . _parser_msg($data_ref, $$offset_ref)
156             );
157             }
158            
159 37         36 $$offset_ref++;
160 37         83 1;
161             }
162              
163             sub _parse_value {
164 110     110   151 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
165            
166 110 50       188 $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         215 my $c = substr($$data_ref, $$offset_ref, 1);
172 110 100       533 if ($c eq '{') {
    100          
    100          
    100          
    100          
    100          
    50          
173 17         59 $self->_parse_object($data_ref, $offset_ref, $line_ref, $rv_ref);
174             }
175             elsif ($c eq '[') {
176 17         43 $self->_parse_array($data_ref, $offset_ref, $line_ref, $rv_ref);
177             }
178             elsif ($c eq 't') {
179 6         32 $self->_parse_constant('true', TRUE, $data_ref, $offset_ref, $line_ref, $rv_ref);
180             }
181             elsif ($c eq 'f') {
182 6         22 $self->_parse_constant('false', FALSE, $data_ref, $offset_ref, $line_ref, $rv_ref);
183             }
184             elsif ($c eq 'n') {
185 6         17 $self->_parse_constant('null', undef, $data_ref, $offset_ref, $line_ref, $rv_ref);
186             }
187             elsif ($c eq '"') {
188 23         56 $self->_parse_string($data_ref, $offset_ref, $line_ref, $rv_ref);
189             }
190             elsif ($c =~ /-|\d/) {
191 35         91 $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   37 my ($self, $constant, $constant_val, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
203            
204 18         23 my $len = length $constant;
205 18 50 33     171 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         24 $$offset_ref += $len;
213 18         20 $$rv_ref = $constant_val;
214            
215 18         60 1;
216             }
217              
218             sub _parse_number {
219 35     35   49 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
220            
221 35 50       148 $$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         56 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         110 1;
232             }
233              
234             sub _parse_array {
235 17     17   27 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
236            
237 17         18 $$offset_ref++;
238 17         19 my @rv;
239            
240 17         18 while (1) {
241 60 50       116 $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       160 substr($$data_ref, $$offset_ref, 1) eq ']'
247             and last;
248 43 50       146 $self->_parse_value($data_ref, $offset_ref, $line_ref, \my $val)
249             or return;
250 43 50       94 $self->_parse_delim(']', $data_ref, $offset_ref, $line_ref)
251             or return;
252            
253 43         83 push @rv, $val;
254             }
255            
256 17         29 $$rv_ref = \@rv;
257 17         25 $$offset_ref++;
258            
259 17         57 1;
260             }
261              
262             sub _parse_object {
263 17     17   24 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
264            
265 17         18 $$offset_ref++;
266 17         52 my %rv;
267 17 50       48 if ($self->{keep_order}) {
268 0         0 tie %rv, $HashClass;
269             }
270            
271 17         19 while (1) {
272 47 50       95 $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       131 substr($$data_ref, $$offset_ref, 1) eq '}'
278             and last;
279 30 50       66 $self->_parse_bareword_or_string($data_ref, $offset_ref, $line_ref, \my $key)
280             or return;
281 30 50       66 $self->_parse_colon_sign($data_ref, $offset_ref, $line_ref)
282             or return;
283 30 50       74 $self->_parse_value($data_ref, $offset_ref, $line_ref, \my $val)
284             or return;
285 30 50       66 $self->_parse_delim('}', $data_ref, $offset_ref, $line_ref)
286             or return;
287            
288 30         91 $rv{$key} = $val;
289             }
290            
291 17         24 $$rv_ref = \%rv;
292 17         21 $$offset_ref++;
293            
294 17         67 1;
295             }
296              
297             sub _parse_colon_sign {
298 30     30   43 my ($self, $data_ref, $offset_ref, $line_ref) = @_;
299            
300 30 50       51 $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       97 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         32 $$offset_ref++;
312 30         68 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   31 my ($self, $data_ref, $offset_ref, $line_ref, $rv_ref) = @_;
327            
328 24         45 pos($$data_ref) = ++$$offset_ref;
329 24         42 my $str = '';
330            
331 24         104 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       180 if (defined $1) {
    100          
    100          
333 10         14 $$line_ref += length $1;
334 10         34 $str .= $1;
335             }
336             elsif (defined $2) {
337 8         44 $str .= $ESCAPES{$2};
338             }
339             elsif (defined $3) {
340 7         57 $str .= pack 'U', hex $3;
341             }
342             else {
343 41         172 $str .= $4;
344             }
345             }
346            
347 24         42 $$offset_ref = pos($$data_ref);
348 24 50       44 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       68 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         30 $$offset_ref++;
371 24         29 $$rv_ref = $str;
372            
373 24         71 1;
374             }
375              
376             sub parse_file {
377 10     10 1 1300 my ($self, $path) = @_;
378            
379 10         40 $self->_err(undef);
380            
381 10 100       529 open my $fh, '<:utf8', $path
382             or return $self->_err(IO => "open `$path': $!");
383            
384 8         26 my $data = do {
385 8         35 local $/;
386 8         419 <$fh>;
387             };
388            
389 8         93 close $fh;
390            
391 8         41 $self->parse($data);
392             }
393              
394             sub last_error {
395 8     8 1 3408 return $_[0]->{last_error};
396             }
397              
398             sub _err {
399 50     50   77 my ($self, $err_type, $msg) = @_;
400            
401 50 100       136 unless (defined $err_type) {
402 34         93 $self->{last_error} = undef;
403 34         94 return;
404             }
405            
406 16         88 $self->{last_error} = "Parse::JCONF::Error::$err_type"->new($msg);
407 16 100       43 if ($self->{autodie}) {
408 8         29 $self->{last_error}->throw();
409             }
410            
411 8         32 return;
412             }
413              
414             sub _parser_msg {
415 14     14   19 my ($data_ref, $offset) = @_;
416            
417 14         22 my $msg = '';
418 14         15 my $non_space_chars = 0;
419 14         13 my $c;
420             my $i;
421            
422 14         50 for ($i=$offset; $i>=0; $i--) {
423 28         39 $c = substr($$data_ref, $i, 1);
424 28 50       86 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         48 substr($msg, 0, 0) = $c;
435            
436 28 50       75 if ($c =~ /\S/) {
437 28 50       79 if (++$non_space_chars > 5) {
438 0         0 last;
439             }
440             }
441             }
442            
443 14         15 substr($msg, 0, 0) = ' ';
444 14         17 my $bad_char = length $msg;
445            
446 14         19 my $len = length $$data_ref;
447 14         14 $non_space_chars = 0;
448            
449 14         34 for ($i=$offset+1; $i<$len; $i++) {
450 56         79 $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         57 substr($msg, length $msg) = $c;
462            
463 56 50       129 if ($c =~ /\S/) {
464 56 100       133 if (++$non_space_chars > 3) {
465 14         16 last;
466             }
467             }
468             }
469            
470 14         41 substr($msg, length $msg) = "\n" . ' 'x($bad_char-1).'^';
471 14         48 return $msg;
472             }
473              
474             1;
475              
476             __END__