File Coverage

blib/lib/dotconfig.pm
Criterion Covered Total %
statement 296 345 85.8
branch 162 214 75.7
condition 30 46 65.2
subroutine 38 40 95.0
pod 0 4 0.0
total 526 649 81.0


line stmt bran cond sub pod time code
1             package dotconfig;
2 14     14   40728 use strict;
  14         26  
  14         422  
3 14     14   64 use warnings;
  14         23  
  14         306  
4 14     14   66 use Carp ();
  14         27  
  14         191  
5 14     14   12048 use Encode ();
  14         162318  
  14         330  
6 14     14   90 use Exporter 'import';
  14         26  
  14         3998  
7             our $VERSION = '0.04';
8             our @EXPORT = qw( load_config decode_config );
9              
10             sub load_config {
11 48     48 0 37440 my ($path, $option) = @_;
12 48 50       1324 open my $fh, "<", $path or Carp::croak $!;
13 48         83 my $text = Encode::decode_utf8(do { local $/; <$fh> });
  48         145  
  48         733  
14 48         870 decode_config($text, $option);
15             }
16              
17             sub new {
18 0     0 0 0 my ($class, %option) = @_;
19 0         0 bless {
20             option => { %option },
21             }, $class;
22             }
23              
24             sub decode {
25 0     0 0 0 my ($self, $text) = @_;
26 0         0 decode_config($text, $self->{option});
27             }
28              
29             sub decode_config {
30 48     48 0 78 my ($text, $option) = @_;
31 48         227 my $decoder = dotconfig::Decoder->new($text, $option);
32 48 50       184 if (my $config = $decoder->config) {
33 48         711 return $$config;
34             } else {
35 0         0 die "No value found in the config";
36             }
37             }
38              
39             # sub encode {
40             # my ($self, $value) = @_;
41             # }
42             #
43             # sub encode_config {
44             # }
45              
46             package
47             dotconfig::Decoder;
48 14     14   74 use strict;
  14         22  
  14         334  
49 14     14   64 use warnings;
  14         69  
  14         383  
50 14     14   65826 use Math::BigInt;
  14         374475  
  14         61  
51 14     14   299689 use Math::BigFloat;
  14         308886  
  14         77  
52 14     14   25531 use JSON ();
  14         189032  
  14         1337  
53             use constant {
54 14         43724 DC_SPACE => ' ',
55             DC_TAB => "\t",
56             DC_LF => "\n",
57             DC_CR => "\r",
58             DC_FALSE => 'false',
59             DC_TRUE => 'true',
60             DC_NULL => 'null',
61             DC_BEGIN_ARRAY => '[',
62             DC_END_ARRAY => ']',
63             DC_BEGIN_MAP => '{',
64             DC_END_MAP => '}',
65             DC_NAME_SEPARATOR => ':',
66             DC_VALUE_SEPARATOR => ',',
67             DC_QUOTATION_MARK => '"',
68             DC_ESCAPE => '\\',
69             DC_SOLIDUS => '/',
70             DC_BACKSPACE => "\b",
71             DC_FORM_FEED => "\f",
72 14     14   156 };
  14         27  
73              
74             sub new {
75 48     48   87 my ($class, $text, $option) = @_;
76             bless {
77             tape => $text,
78             index => 0,
79             option => {
80             allow_bigint => 1,
81 48   50     83 %{$option // {}},
  48         479  
82             },
83             }, $class;
84             }
85              
86             sub _peek {
87 2212     2212   3226 my ($self, $next) = @_;
88 2212   50     5283 my $eos_pos = $self->{index} + ($next // 0) + 1;
89 2212 100       7008 if (length $self->{tape} >= $eos_pos) {
90 2165   50     9226 return substr $self->{tape}, $self->{index} + $next // 0, 1;
91             }
92             }
93              
94             sub _match_str {
95 3684     3684   5359 my ($self, $str) = @_;
96 3684         6434 my $next = substr $self->{tape}, $self->{index}, length $str;
97 3684 100 100     24135 if (length $next == length $str and $str eq $next) {
98 321         877 return 1;
99             }
100             }
101              
102             sub _consume {
103 1529     1529   2044 my ($self, $length) = @_;
104 1529   100     5647 $self->{index} += $length // 1;
105             }
106              
107             sub _consume_if {
108 1207     1207   1658 my ($self, $expected) = @_;
109 1207 100       2232 if ($self->_match_str($expected)) {
110 243         367 $self->{index} += length $expected;
111 243         667 return 1;
112             }
113             }
114              
115             sub _consume_if_space {
116 1113     1113   1572 my ($self, $char) = @_;
117 1113 100 66     9076 if ( $char eq DC_SPACE
      100        
      66        
118             or $char eq DC_TAB
119             or $char eq DC_LF
120             or $char eq DC_CR
121             ) {
122 488         655 $self->{index} += 1;
123 488         1343 return 1;
124             }
125             }
126              
127             sub config {
128 221     221   1244 my ($self, $config) = @_;
129              
130 221         239 while (1) {
131 446 50       886 if (defined(my $char = $self->_peek(0))) {
132 446 100       965 next if $self->_consume_if_space($char);
133 228 100 66     1337 if ($char eq DC_BEGIN_MAP) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
134 22         52 return $self->config(\$self->map);
135             }
136             elsif ($char eq DC_BEGIN_ARRAY) {
137 15         39 return $self->config(\$self->array);
138             }
139             elsif ($char eq DC_QUOTATION_MARK) {
140 29         70 return $self->config(\$self->string);
141             }
142             elsif ($char eq "<" and $self->_match_str("<<")) {
143 4         14 return $self->config(\$self->heredoc);
144             }
145             elsif ($char =~ /[0-9\-]/) {
146 22 100       60 if ($self->_match_str("0x")) {
    100          
    100          
147 4         12 return $self->config(\$self->hex);
148             }
149             elsif ($self->_match_str("0b")) {
150 2         8 return $self->config(\$self->binary);
151             }
152             elsif ($self->_match_str("0o")) {
153 2         5 return $self->config(\$self->octal);
154             }
155             else {
156 14         38 return $self->config(\$self->number);
157             }
158             }
159             elsif (my $false = $self->false) {
160 6         41 return $self->config($false);
161             }
162             elsif (my $true = $self->true) {
163 7         54 return $self->config($true);
164             }
165             elsif (my $null = $self->null) {
166 3         29 return $self->config($null);
167             }
168             elsif ($char eq DC_SOLIDUS) {
169 7 50 66     19 if ($self->_match_str("//") or $self->_match_str("/*")) {
170 7         15 $self->comment;
171 7         18 next;
172             } else {
173 0         0 die "Unexpected charcter `/`";
174             }
175             }
176             else {
177 113         448 return $config;
178             }
179             } else {
180 0         0 last; # EOF
181             }
182             }
183              
184 0         0 return $config;
185             }
186              
187 136 100   136   289 sub false { shift->_consume_if(DC_FALSE) ? \JSON::false : undef }
188 130 100   130   283 sub true { shift->_consume_if(DC_TRUE) ? \JSON::true : undef }
189 123 100   123   251 sub null { shift->_consume_if(DC_NULL) ? \JSON::null : undef }
190              
191             sub number {
192 14     14   20 my $self = shift;
193 14         20 my $string = "";
194              
195             # minus
196 14 100       36 if ($self->_consume_if("-")) {
197 4         6 $string .= "-";
198 4 50       11 if (defined(my $char = $self->_peek(0))) {
199 4 50       19 unless ($char =~ /[0-9]/) {
200 0         0 die "Unexpected number format (found `$char` after `-`)";
201             }
202             } else {
203 0         0 die "Unexpected number format (no number after `-`)";
204             }
205             }
206              
207             # int
208 14 50       36 if ($self->_consume_if("0")) {
209 0         0 $string .= "0";
210 0 0       0 if (defined(my $char = $self->_peek(0))) {
211 0 0       0 if ($char =~ /[0-9]/) {
212 0         0 die "Unexpected number format (found `$char` after `0`)";
213             }
214             } else {
215 0         0 return $string + 0;
216             }
217             }
218              
219 14         50 while (1) {
220 114 50       223 if (defined(my $char = $self->_peek(0))) {
221 114 100       293 if ($char =~ /[0-9.]/) { # `.` for frac
222 100         117 $string .= $char;
223 100         188 $self->_consume;
224 100         146 next;
225             } else {
226 14         25 last;
227             }
228             } else {
229 0         0 last;
230             }
231             }
232              
233             # exp
234 14 100 66     32 if ($self->_consume_if("e") or $self->_consume_if("E")) {
235 3         4 $string .= "e";
236 3 50       7 if ($self->_consume_if("+")) {
    100          
237 0         0 $string .= "+";
238             } elsif ($self->_consume_if("-")) {
239 2         3 $string .= "-";
240             }
241              
242 3         7 my $digit_after_exp;
243 3         4 while (1) {
244 8 50       17 if (defined(my $char = $self->_peek(0))) {
245 8 100       23 if ($char =~ /[1-9]/) {
246 5         8 $string .= $char;
247 5         12 $self->_consume;
248 5         7 $digit_after_exp = 1;
249             } else {
250 3         5 last;
251             }
252             } else {
253 0         0 last;
254             }
255             }
256              
257 3 50       8 unless ($digit_after_exp) {
258 0         0 die "Unexpected number format (no digit after exp)";
259             }
260             }
261              
262 14 100       42 if ($string =~ /[.eE]/) { # is float
263 5 50       35 return $self->{option}{allow_bigint} ? Math::BigFloat->new($string) : $string;
264             } else { # is integer
265 9 100       43 if (($string + 0) =~ /[.eE]/) {
266 1 50       8 return $self->{option}{allow_bigint} ? Math::BigInt->new($string) : $string;
267             } else {
268 8         36 return $string + 0;
269             }
270             }
271             }
272              
273             sub hex {
274 4     4   7 my $self = shift;
275 4         7 my $prefix = "0x";
276 4 50       12 $self->_consume_if($prefix)
277             or die "Expected `$prefix`";
278              
279 4         8 my $string = "";
280 4         5 while (1) {
281 36 50       68 if (defined(my $char = $self->_peek(0))) {
282 36 100       95 if ($char =~ /[A-F0-9]/i) {
283 32         39 $string .= $char;
284 32         62 $self->_consume;
285 32         47 next;
286             } else {
287 4         27 return oct "$prefix$string";
288             }
289             } else {
290 0         0 return oct "$prefix$string";
291             }
292             }
293             }
294              
295             sub binary {
296 2     2   2 my $self = shift;
297 2         3 my $prefix = "0b";
298 2 50       10 $self->_consume_if($prefix)
299             or die "Expected `$prefix`";
300              
301 2         3 my $string = "";
302 2         4 while (1) {
303 14 50       27 if (defined(my $char = $self->_peek(0))) {
304 14 100       38 if ($char =~ /[01]/i) {
305 12         15 $string .= $char;
306 12         24 $self->_consume;
307 12         17 next;
308             } else {
309 2         12 return oct "$prefix$string";
310             }
311             } else {
312 0         0 return oct "$prefix$string";
313             }
314             }
315             }
316              
317             sub octal {
318 2     2   3 my $self = shift;
319 2         3 my $prefix = "0o";
320 2 50       5 $self->_consume_if($prefix)
321             or die "Expected `$prefix`";
322              
323 2         4 my $string = "";
324 2         3 while (1) {
325 8 50       17 if (defined(my $char = $self->_peek(0))) {
326 8 100       21 if ($char =~ /[0-7]/i) {
327 6         9 $string .= $char;
328 6         9 $self->_consume;
329 6         10 next;
330             } else {
331 2         11 return oct "0$string";
332             }
333             } else {
334 0         0 return oct "0$string";
335             }
336             }
337             }
338              
339             sub comment {
340 10     10   16 my $self = shift;
341 10 100       20 if ($self->_match_str("//")) {
    50          
342 5         16 $self->inline_comment;
343             }
344             elsif ($self->_match_str("/*")) {
345 5         10 $self->block_comment;
346             }
347             else {
348 0         0 die "Unexpected charcter `/`";
349             }
350             }
351              
352             sub inline_comment {
353 6     6   9 my $self = shift;
354 6 50       18 $self->_consume_if(DC_SOLIDUS . DC_SOLIDUS)
355             or die "Expected `" . DC_SOLIDUS . DC_SOLIDUS . "`";
356              
357 6         10 while (1) {
358 79 50       147 if (defined(my $char = $self->_peek(0))) {
359 79         157 $self->_consume;
360 79 100       134 if ($char eq DC_LF) {
361 6         12 return;
362             } else {
363 73         98 next;
364             }
365             } else {
366 0         0 $self->_consume;
367 0         0 return;
368             }
369             }
370             }
371              
372             sub block_comment {
373 7     7   11 my $self = shift;
374 7 50       14 $self->_consume_if("/*")
375             or die "Expected `/*`";
376              
377 7         9 while (1) {
378 450 100       857 if ($self->_match_str("//")) {
    100          
    100          
379 1         2 $self->inline_comment;
380             }
381             elsif ($self->_match_str("/*")) {
382 2         5 $self->block_comment;
383             }
384             elsif ($self->_match_str("*/")) {
385 7         16 $self->_consume(2);
386 7         14 return;
387             }
388             else {
389 440         852 $self->_consume;
390             }
391             }
392              
393             }
394              
395             sub string {
396 36     36   47 my $self = shift;
397 36 50       83 $self->_consume_if(DC_QUOTATION_MARK)
398             or die "Expected `" . DC_QUOTATION_MARK . "`";
399              
400 36         51 my $string = "";
401 36         41 while (1) {
402 333 50       632 if (defined(my $char = $self->_peek(0))) {
403 333 100       888 if ($char eq DC_ESCAPE) {
    100          
404 18 50       41 if (defined(my $next_char = $self->_peek(1))) {
405 18         98 my $escapes = {
406             DC_QUOTATION_MARK() => DC_QUOTATION_MARK,
407             DC_ESCAPE() => DC_ESCAPE,
408             DC_SOLIDUS() => DC_SOLIDUS,
409             "b" => DC_BACKSPACE,
410             "f" => DC_FORM_FEED,
411             "n" => DC_LF,
412             "r" => DC_CR,
413             "t" => DC_TAB,
414             };
415 18 100       57 if (my $ch = $escapes->{$next_char}) {
    50          
416 4         5 $string .= $ch;
417 4         12 $self->_consume(2);
418 4         15 next;
419             } elsif ($next_char eq 'u') { # TODO UTF-16 support?
420 14         17 my $utf = "";
421 14         31 for (1..4) {
422 56         121 my $char = $self->_peek(1 + $_);
423 56 50 33     296 if (defined $char && $char =~ /[A-F0-9]/i) {
424 56         101 $utf .= $char;
425             } else {
426 0         0 die "Unexpected end of escaped UTF string";
427             }
428             }
429 14         31 $self->_consume(6);
430              
431 14 50       35 if ((my $hex = CORE::hex $utf) > 127) {
432 14         75 $string .= pack U => $hex;
433             } else {
434 0         0 $string .= chr $hex;
435             }
436             } else {
437 0         0 die "Unexpected escape sequence";
438             }
439             } else {
440 0         0 die "Unexpected end of string literal";
441             }
442              
443             } elsif ($char eq DC_QUOTATION_MARK) {
444 36 50       83 if ($self->_peek(-1) eq DC_ESCAPE) {
445 0         0 $string .= $char;
446 0         0 $self->_consume;
447 0         0 next;
448             } else {
449 36         79 $self->_consume;
450 36         147 return $string;
451             }
452             } else {
453 279         345 $string .= $char;
454 279         532 $self->_consume;
455 279         405 next;
456             }
457             } else {
458 0         0 die "Unterminated string";
459             }
460             }
461             }
462              
463             sub heredoc {
464 4     4   7 my $self = shift;
465              
466 4 50       9 $self->_consume_if("<<")
467             or die "Expected `<<`";
468              
469 4 100       12 my $strip_space = $self->_consume_if("-") ? 1 : 0;
470              
471 4         6 my $delimiter = "";
472 4         5 while (1) {
473 16 50       30 if (defined(my $char = $self->_peek(0))) {
474 16         30 $self->_consume;
475 16 50 33     78 if ($char eq DC_SPACE or $char eq DC_TAB) {
    100          
476 0         0 next;
477             } elsif ($char eq DC_LF) {
478 4         13 last;
479             } else {
480 12         23 $delimiter .= $char;
481             }
482             } else {
483 0         0 die "Unexpected end of heredoc";
484             }
485             }
486              
487 4         7 my $string = "";
488 4         5 while (1) {
489 315 100       578 last if $self->_consume_if($delimiter);
490 311 50       702 if (defined(my $char = $self->_peek(0))) {
491 311         585 $self->_consume;
492 311         379 $string .= $char;
493 311         394 next;
494             } else {
495 0         0 die "Unexpected end of heredoc";
496             }
497             }
498 4         10 chomp $string;
499              
500 4 100       11 if ($strip_space) {
501 2         11 my @lines = split /\n/, $string;
502 2         4 my $last_line = pop @lines;
503 2         4 my $indent = 0;
504 2         8 for (split //, $last_line) {
505 8 50       37 $indent++ if $_ eq DC_SPACE
506             }
507              
508 2         5 $string = join DC_LF, map { substr $_, $indent } @lines;
  4         15  
509             }
510              
511 4         16 return $string;
512             }
513              
514             sub array {
515 15     15   21 my $self = shift;
516              
517 15 50       31 $self->_consume_if(DC_BEGIN_ARRAY)
518             or die "Expected `" . DC_BEGIN_ARRAY . "`";
519              
520 15         26 my $array = [];
521 15         21 while (1) {
522 59 50       118 if (defined(my $char = $self->_peek(0))) {
523 59 100       118 if ($self->_consume_if(DC_END_ARRAY)) {
    100          
524 15         54 return $array;
525             }
526             elsif ($self->_consume_if(DC_VALUE_SEPARATOR)) {
527 15         27 next;
528             }
529             else {
530 29 100       70 if (defined(my $value = $self->config)) {
531 24         66 push @$array, $$value;
532             } else {
533 5         12 next; # trailing comma is valid
534             }
535             }
536             } else {
537 0         0 return $array;
538             }
539             }
540             }
541              
542             sub map {
543 22     22   32 my $self = shift;
544 22 50       40 $self->_consume_if(DC_BEGIN_MAP)
545             or die "Expected `" . DC_BEGIN_MAP . "`";
546              
547 22         38 my $map = [];
548              
549 22         27 while (1) {
550 139 50       256 if (defined(my $char = $self->_peek(0))) {
551 139 100       274 next if $self->_consume_if_space($char);
552 23 100 66     52 if ($self->_match_str("//") or $self->_match_str("/*")) {
    100          
553 1         2 $self->comment;
554 1         2 next;
555             }
556             elsif ($self->_consume_if(DC_END_MAP)) {
557 1         2 last;
558             }
559             else {
560 21         60 $self->map_members($map);
561 21         48 last;
562             }
563             } else {
564 0         0 last;
565             }
566             }
567              
568 22         116 return { @$map };
569             }
570              
571             sub map_members {
572 14         34 use constant { map { ($_ => $_) } qw/
  56         7062  
573             STATE_KEY
574             STATE_KEY_SEPARATOR
575             STATE_VALUE
576             STATE_VALUE_SEPARATOR
577 14     14   91 / };
  14         27  
578              
579 155     155   204 my $self = shift;
580 155         199 my $members = shift;
581 155   100     346 my $state = shift // STATE_KEY;
582              
583 155         170 while (1) {
584 311 50       589 if (defined(my $char = $self->_peek(0))) {
585 311 100       619 next if $self->_consume_if_space($char);
586 157 100 66     350 if ($self->_match_str("//") or $self->_match_str("/*")) {
    100          
587 2         5 $self->comment;
588 2         4 next;
589             }
590             elsif ($self->_consume_if(DC_END_MAP)) {
591 21         36 return;
592             }
593             else {
594 134 100       403 if ($state eq STATE_KEY) {
    100          
    100          
    50          
595 36 50       100 if (defined(my $key = $self->map_key)) {
596 36         65 push @$members, $key;
597 36         99 $self->map_members($members, STATE_KEY_SEPARATOR);
598 36         80 return;
599             } else {
600 0         0 die "Unexpected member key name in map";
601             }
602             }
603             elsif ($state eq STATE_KEY_SEPARATOR) {
604 36 50       71 if ($self->_consume_if(DC_NAME_SEPARATOR)) {
605 36         106 $self->map_members($members, STATE_VALUE);
606 36         83 return;
607             } else {
608 0         0 die "Expected `" . DC_NAME_SEPARATOR . "` but got unexpected char at " . $self->{index};
609             }
610             }
611             elsif ($state eq STATE_VALUE) {
612 36 50       72 if (defined(my $value = $self->config)) {
613 36         57 push @$members, $$value;
614 36         89 $self->map_members($members, STATE_VALUE_SEPARATOR);
615 36         73 return;
616             } else {
617 0         0 die "Invalid value";
618             }
619             }
620             elsif ($state eq STATE_VALUE_SEPARATOR) {
621 26 50       53 if ($self->_consume_if(DC_VALUE_SEPARATOR)) {
622 26         69 $self->map_members($members, STATE_KEY);
623 26         57 return;
624             } else {
625 0         0 die "Expected `" . DC_VALUE_SEPARATOR . "` but got `$char` at " . $self->{index};
626 0         0 return;
627             }
628             }
629             else {
630 0         0 die "Unexpected state: `$state`";
631             }
632             }
633             } else {
634 0         0 return;
635             }
636             }
637             }
638              
639             sub map_key {
640 36     36   44 my $self = shift;
641              
642 14         43 use constant { map { ($_ => $_) } qw/
  28         3332  
643             MODE_MAP_KEY_NAKED
644             MODE_MAP_KEY_QUOTED
645 14     14   75 / };
  14         26  
646              
647 36 100       79 my $mode = $self->_match_str(DC_QUOTATION_MARK)
648             ? MODE_MAP_KEY_QUOTED
649             : MODE_MAP_KEY_NAKED;
650              
651 36         53 my $string = "";
652 36         46 while (1) {
653 224 50       410 if (defined(my $char = $self->_peek(0))) {
654 224 100       406 if ($mode eq MODE_MAP_KEY_QUOTED) {
655 7         15 return $self->string;
656             } else {
657 217 50 33     467 if ($self->_consume_if_space($char)) {
    50          
    100          
658 0         0 next;
659             }
660             elsif ($self->_match_str("//") or $self->_match_str("/*")) {
661 0         0 $self->comment;
662 0         0 next;
663             }
664             elsif ($self->_match_str(DC_NAME_SEPARATOR)) {
665 29         93 return $string;
666             }
667             else {
668 188         235 $string .= $char;
669 188         359 $self->_consume;
670 188         336 next;
671             }
672             }
673             } else {
674 0           die "Unterminated string";
675             }
676             }
677             }
678              
679             1;
680             __END__