File Coverage

blib/lib/JSON/Relaxed/Parser.pm
Criterion Covered Total %
statement 362 526 68.8
branch 150 244 61.4
condition 89 142 62.6
subroutine 44 68 64.7
pod 0 35 0.0
total 645 1015 63.5


line stmt bran cond sub pod time code
1             #! perl
2              
3 11     11   128143 use v5.26;
  11         42  
4 11     11   7673 use Object::Pad;
  11         172803  
  11         68  
5 11     11   2732 use utf8;
  11         444  
  11         111  
6              
7             package JSON::Relaxed::Parser;
8              
9             our $VERSION = "0.098";
10              
11             class JSON::Relaxed::Parser;
12              
13             # Instance data.
14 6     6 0 15 field $data :mutator; # RJSON string being parser
  6         41  
15             field @pretoks; # string in pre-tokens
16             field @tokens; # string as tokens
17              
18             # Instance properties.
19              
20             # Enforce strictness to official standard.
21             # Strict true -> RJSON conformant.
22             # Strict false (default) -> RRJSON. Everything goes :).
23 3     3 0 20 field $strict :mutator :param = 0;
24              
25             # Allow extra stuff after the JSON structure.
26             # Strict mode only.
27 3     0 0 10 field $extra_tokens_ok :mutator :param = 0;
  0         0  
28              
29             # Define the values to be used for true and false.
30 0     20 0 0 field $booleans :mutator :param = 1;
  20         104  
31              
32             # Signal error with exceptions.
33 20     1 0 103 field $croak_on_error :mutator :param = 1;
  1         9  
34 1         3 field $croak_on_error_internal;
35              
36             # Some non-strict extensions can be controlled individually.
37             # This may disappear in some futer version, so do not use.
38             # Extension: a.b:c -> a:{b:c}
39             ## Non-strict only.
40 0     0 0 0 field $combined_keys :mutator :param = 1;
41              
42             # Extension: a:b -> {a:b} (if outer)
43             ## Non-strict only.
44 0     0 0 0 field $implied_outer_hash :mutator :param = 1;
  0         0  
45              
46             # Extension: = as :, and optional before {, off/on as false/true
47             ## Non-strict only.
48 0     99 0 0 field $prp :mutator :param = 1;
  99         199  
49              
50             # Formatted output.
51 99     0 0 534 field $pretty :mutator :param = 0;
  0         0  
52              
53             # Retain key order. Warning: adds a key " key order " to each hash!
54             ## Non-strict only.
55 0     0 0 0 field $key_order :mutator :param = 0;
  0         0  
56              
57             # Error indicators.
58 0 50   2 0 0 field $err_id :accessor;
  2         8  
  2         14  
59 2 50   2 0 1045 field $err_msg :accessor;
  2         28  
60 0 0   0 0 0 field $err_pos :accessor;
  0         0  
61              
62 30     30 0 50588 method decode( $str ) {
  30         143  
  30         53  
  30         48  
63 30         47 $croak_on_error_internal = $croak_on_error;
64 30         87 $self->_decode($str);
65             }
66              
67             # Legacy.
68 65     65 0 3584 method parse( $str ) {
  65         250  
  65         323  
  65         146  
69 65         143 $croak_on_error_internal = 0;
70 65         391 $self->_decode($str);
71             }
72              
73 95     95   175 method _decode( $str ) {
  95         258  
  95         153  
  95         143  
74              
75 95         181 $data = $str;
76 95 50 33     585 return $self->error('missing-input')
77             unless defined $data && length $data;
78              
79 95         177 undef $err_id;
80 95         150 $err_pos = -1;
81 95         165 undef $err_msg;
82              
83 95         418 $self->pretokenize;
84 95 50       351 return if $self->is_error;
85              
86 95         416 $self->tokenize;
87 95 50       216 return $self->error('empty-input') unless @tokens;
88              
89 95         291 $self->structure( top => 1 );
90             }
91              
92             ################ Character classifiers ################
93              
94             # Reserved characters.
95             # '[' beginning of array
96             # ']' end of array
97             # '{' beginning of hash
98             # '}' end of hash
99             # ':' delimiter between name and value of hash element
100             # ',' separator between elements in hashes and arrays
101              
102             my $p_reserved = q<[,:{}\[\]]>;
103              
104 621     621 0 854 method is_reserved ($c) {
  621         1193  
  621         812  
  621         755  
105 621         5744 $c =~ /^$p_reserved$/;
106             }
107              
108             # Newlines. CRLF (Windows), CR (MacOS) and newline (sane systems).
109              
110             my $p_newlines = q{(?:\r\n|\r|\n|\\\n)};
111              
112 0     0 0 0 method is_newline ($c) {
  0         0  
  0         0  
  0         0  
113 0         0 $c =~ /^$p_newlines$/o;
114             }
115              
116             # Quotes. Single, double and backtick.
117              
118             my $p_quotes = q{["'`]};
119              
120 178     178 0 322 method is_quote ($c) {
  178         338  
  178         266  
  178         249  
121 178         824 $c =~ /^$p_quotes$/o;
122             }
123              
124             # Numbers. A special case of unquoted strings.
125             my $p_number = q{[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?};
126              
127             method pretokenize {
128              
129             # \u escape (4 hexits)
130             my @p = ( qq<\\\\u[[:xdigit:]]{4}> );
131              
132             # Any escaped char (strict mode).
133             if ( $strict ) {
134             push( @p, qq<\\\\.> );
135             }
136              
137             # Otherwise, match \u{ ... } also.
138             else {
139             push( @p, qq<\\\\u\\{[[:xdigit:]]+\\}>, qq<\\\\[^u]> ); # escaped char
140             }
141              
142             if ( $prp && !$strict ) {
143             # Add = to the reserved characters
144             $p_reserved = q<[,=:{}\[\]]>;
145             # Massage # comments into // comments without affecting position.
146             $data =~ s/^(\s*)#.(.*)$/$1\/\/$2/gm;
147             $data =~ s/^(\s*)#$/$1 /gm;
148             }
149              
150             push( @p, $p_newlines,
151             qq< // [^\\n]* \\n >, # line comment
152             qq< /\\* .*? \\*/ >, # comment start
153             qq< /\\* >, # comment start
154             qq< $p_reserved >, # reserved chars
155             qq< "(?:\\\\.|.)*?" >, # "string"
156             qq< `(?:\\\\.|.)*?` >, # `string`
157             qq< '(?:\\\\.|.)*?' >, # 'string'
158             qq< $p_quotes >, # stringquote
159             qq< \\s+ > ); # whitespace
160              
161             my $p = join( "|", @p );
162              
163             @pretoks = split( m< ( $p ) >sox, $data );
164              
165             # Remove empty strings.
166             @pretoks = grep { length($_) } @pretoks;
167              
168             return;
169             }
170              
171             # Accessor for @pretoks.
172 0     0 0 0 method pretoks() { \@pretoks }
  0         0  
  0         0  
  0         0  
173              
174             method tokenize {
175              
176             @tokens = ();
177             my $offset = 0; # token offset in input
178              
179             if ( $booleans ) {
180             if ( ref($booleans) ne 'ARRAY' ) {
181             $booleans = [ $JSON::Boolean::false, $JSON::Boolean::true ];
182             }
183             }
184             else {
185             $booleans = [ 0, 1 ];
186             }
187              
188             my $glue = 0; # can glue strings
189             my $uq_open = 0; # collecting pretokens for unquoted string
190              
191             # Loop through characters.
192             while ( @pretoks ) {
193             my $pretok = shift(@pretoks);
194              
195             # White space: ignore.
196             if ( $pretok !~ /\S/ ) {
197             $offset += length($pretok);
198             $uq_open = 0;
199             next;
200             }
201              
202             if ( $pretok eq "\\\n" ) {
203             $glue++ if $glue;
204             $uq_open = 0;
205             $offset += length($pretok);
206             next;
207             }
208              
209             # Strings.
210             if ( $pretok =~ /^(["'`])(.*?)\1$/s ) {
211             my ( $quote, $content ) = ( $1, $2 );
212             if ( $glue > 1 ) {
213             $tokens[-1]->append($content);
214             }
215             else {
216             $self->addtok( $content, 'Q', $offset, $quote );
217             $glue = 1 unless $strict;
218             }
219             $offset += length($pretok);
220             $uq_open = 0;
221             next;
222             }
223             $glue = 0;
224              
225             # // comment.
226             if ( $pretok =~ m<^//(.*)> ) {
227             # $self->addtok( $1, 'L', $offset );
228             $offset += length($pretok);
229             $uq_open = 0;
230             }
231              
232             # /* comment */
233             elsif ( $pretok =~ m<^/\*.+>s ) {
234             $offset += length($pretok);
235             $uq_open = 0;
236             }
237              
238             elsif ( $pretok eq '/*' ) {
239             return $self->error('unclosed-inline-comment');
240             }
241              
242             # Reserved characters.
243             elsif ( $self->is_reserved($pretok) ) {
244             $self->addtok( $pretok, 'C', $offset );
245             $offset += length($pretok);
246             $uq_open = 0;
247             }
248              
249              
250             # Numbers.
251             elsif ( $pretok =~ /^$p_number$/ ) {
252             $self->addtok( 0+$pretok, 'N', $offset );
253             $offset += length($pretok);
254             $uq_open = 0;
255             }
256              
257             # Quotes
258             # Can't happen -- should be an encosed string.
259             elsif ( $self->is_quote($pretok) ) {
260             $offset += length($pretok);
261             $self->addtok( $pretok, '?', $offset );
262             return $self->error('unclosed-quote', $tokens[-1] );
263             }
264              
265             # Else it's an unquoted string.
266             else {
267             if ( $uq_open ) {
268             $tokens[-1]->append($pretok);
269             }
270             else {
271             $self->addtok( $pretok, 'U', $offset );
272             $uq_open++;
273             }
274             $offset += length($pretok);
275             }
276             }
277             return;
278             }
279              
280             # Accessor for @tokens,
281 0     0 0 0 method tokens() { \@tokens }
  0         0  
  0         0  
  0         0  
282              
283             # Add a new token to @tokens.
284 807     807 0 957 method addtok( $tok, $typ, $off, $quote=undef ) {
  807         1466  
  807         1058  
  807         1034  
  807         1078  
  807         1175  
  807         939  
285              
286 807 100 100     8378 push( @tokens,
    100          
287             $typ eq 'U' || $typ eq 'N'
288             ? JSON::Relaxed::String::Unquoted->new( token => $tok,
289             content => $tok,
290             type => $typ,
291             parent => $self,
292             offset => $off )
293             : $typ eq 'Q'
294             ? JSON::Relaxed::String::Quoted->new( token => $tok,
295             type => $typ,
296             content => $tok,
297             quote => $quote,
298             parent => $self,
299             offset => $off )
300             : JSON::Relaxed::Token->new( token => $tok,
301             parent => $self,
302             type => $typ,
303             offset => $off ) );
304             }
305              
306             # Build the result structure out of the tokens.
307 170     170 0 256 method structure( %opts ) {
  170         380  
  170         316  
  170         203  
308              
309 170 50       439 @tokens = @{$opts{tokens}} if $opts{tokens}; # for debugging
  0         0  
310              
311 170 100 100     610 if ( $implied_outer_hash && !$strict ) {
312             # Note that = can only occur with $prp.
313 163 100 100     570 if ( @tokens > 2 && $tokens[0]->is_string
      66        
314             && $tokens[1]->token =~ /[:={]/ ) {
315 5         38 $self->addtok( '}', 'C', $tokens[-1]->offset );
316 5         35 $self->addtok( '{', 'C', $tokens[0]->offset );
317 5         14 unshift( @tokens, pop(@tokens ));
318             }
319             }
320              
321 170   50     407 my $this = shift(@tokens) // return;
322 170         270 my $rv;
323              
324 170 100       432 if ( $this->is_string ) { # (un)quoted string
325 55         157 $rv = $this->as_perl;
326             }
327             else {
328 115         200 my $t = $this->token;
329 115 100       265 if ( $t eq '{' ) {
    50          
330 56         178 $rv = $self->build_hash;
331             }
332             elsif ( $t eq '[' ) {
333 59         156 $rv = $self->build_array;
334             }
335             else {
336 0         0 return $self->error( 'invalid-structure-opening-character',
337             $this );
338             }
339             }
340              
341             # If this is the outer structure, then no tokens should remain.
342 170 100 100     714 if ( $opts{top}
      66        
      66        
      66        
343             && @tokens
344             && ( $strict || !$extra_tokens_ok )
345             && !$self->is_error
346             ) {
347 3         11 return $self->error( 'multiple-structures', $tokens[0] );
348             }
349              
350 167         1003 return $rv;
351             }
352              
353              
354 4     4 0 29 method error( $id, $aux = undef ) {
  4         9  
  4         9  
  4         8  
  4         7  
355 4         2315 require JSON::Relaxed::ErrorCodes;
356 4         14 $err_id = $id;
357 4 50       42 $err_pos = $aux ? $aux->offset : -1;
358 4         17 $err_msg = JSON::Relaxed::ErrorCodes->message( $id, $aux );
359              
360 4 50       12 die( $err_msg, "\n" ) if $croak_on_error_internal;
361 4         35 return; # undef
362             }
363              
364 198     198 0 23333 method is_error() {
  198         640  
  198         280  
365 198         598 $err_id;
366             }
367              
368             # For debugging.
369 0     0 0 0 method dump_tokens() {
  0         0  
  0         0  
370 0         0 my $tokens = \@tokens;
371 0 0       0 return unless require DDP;
372 0 0       0 if ( -t STDERR ) {
373 0         0 DDP::p($tokens);
374             }
375             else {
376 0         0 warn DDP::np($tokens), "\n";
377             }
378             }
379              
380 56     56 0 70 method build_hash() {
  56         100  
  56         67  
381              
382 56         87 my $rv = {};
383 56         93 my @ko; # order of keys
384              
385 56         125 while ( @tokens ) {
386 147         204 my $this = shift(@tokens);
387             # What is allowed after opening brace:
388             # closing brace
389             # comma
390             # string
391              
392             # If closing brace, return.
393 147         327 my $t = $this->token;
394 147 100       313 if ( $t eq '}' ) {
395 55 100 66     139 $rv->{" key order "} = \@ko
      100        
396             if $key_order && !$strict && @ko > 1;
397 55         151 return $rv;
398             }
399              
400             # If comma, do nothing.
401 92 100       211 next if $t eq ',';
402              
403             # String
404             # If the token is a string then it is a key. The token after that
405             # should be a value.
406 79 50       176 if ( $this->is_string ) {
407 79         126 my ( $key, $value );
408              
409             # Set key using string.
410 79         174 $key = $this->as_perl( always_string => 1 );
411 79         232 $self->set_value( $rv, $key );
412 79 100       160 if ( $key_order ) {
413 6 50 33     24 if ( $combined_keys && !$strict ) {
414 6         33 push( @ko, $key =~ s/\..*//r );
415             }
416             else {
417 0         0 push( @ko, $key );
418             }
419             }
420              
421 79         119 my $next = $tokens[0];
422             # If anything follows the string.
423 79 50       144 last unless defined $next;
424              
425             # A comma or closing brace is acceptable after a string.
426 79 50 33     165 next if $next->token eq ',' || $next->token eq '}';
427              
428             # If next token is a colon or equals then it should be followed by a value.
429             # Note that = can only occur with $prp.
430 79 100 33     150 if ( $next->token =~ /^[:=]$/ ) {
    50          
431             # Step past the colon.
432 75         107 shift(@tokens);
433              
434             # If at end of token array, exit loop.
435 75 50       148 last unless @tokens;
436              
437             # Get hash value.
438 75         167 $value = $self->get_value;
439              
440             # If there is a global error, return undef.
441 75 100       209 return undef if $self->is_error;
442             }
443              
444             # Extension (prp): Implied colon.
445             elsif ( $prp && $next->token eq '{' ) {
446             # Get hash value.
447 4         15 $value = $self->get_value;
448              
449             # If there is a global error, return undef.
450 4 50       11 return undef if $self->is_error;
451             }
452              
453             # Anything else is an error.
454             else {
455 0         0 return $self->error('unknown-token-after-key', $next );
456             }
457              
458             # Set key and value in return hash.
459 78         265 $self->set_value( $rv, $key, $value );
460             }
461              
462             # Anything else is an error.
463             else {
464 0         0 return $self->error('unknown-token-for-hash-key', $this );
465             }
466             }
467              
468             # If we get this far then unclosed brace.
469 0         0 return $self->error('unclosed-hash-brace');
470              
471             }
472              
473 79     79 0 139 method get_value() {
  79         166  
  79         99  
474              
475             # Get token.
476 79         126 my $this = shift(@tokens);
477              
478             # Token must be string, array, or hash.
479              
480             # String.
481 79 100       169 if ( $this->is_string ) {
    50          
482 56         112 return $this->as_perl;
483             }
484              
485             # Token opens a hash or array.
486             elsif ( $this->is_list_opener ) {
487 23         52 unshift( @tokens, $this );
488 23         163 return $self->structure;
489             }
490              
491             # At this point it's an illegal token.
492 0         0 return $self->error('unexpected-token-after-colon', $this );
493             }
494              
495 157     157 0 257 method set_value ( $rv, $key, $value = undef ) {
  157         271  
  157         206  
  157         231  
  157         246  
  157         219  
496 157 100 100     1014 return $rv->{$key} = $value
      100        
497             unless $combined_keys && !$strict && $key =~ /\./s;
498              
499 18         58 my @keys = split(/\./, $key, -1 );
500 18         51 my $c = \$rv;
501 18         43 for ( @keys ) {
502 36 50       144 if ( /^[+-]?\d+$/ ) {
503 0         0 $c = \( $$c->[$_] );
504             }
505             else {
506 36         102 $c = \( $$c->{$_} );
507             }
508             }
509 18         110 $$c = $value;
510             }
511              
512 59     59 0 75 method build_array() {
  59         95  
  59         72  
513              
514 59         79 my $rv = [];
515              
516             # Build array. Work through tokens until closing brace.
517 59         119 while ( @tokens ) {
518 357         562 my $this = shift(@tokens);
519              
520 357         606 my $t = $this->token;
521             # Closing brace: we're done building this array.
522 357 100       845 return $rv if $t eq ']';
523              
524             # Comma: if we get to a comma at this point, and we have
525             # content, do nothing with it in strict mode. Ignore otherwise.
526 299 100 100     996 if ( $t eq ',' && (!$strict || @$rv) ) {
    100 100        
    100          
527             }
528              
529             # Opening brace of hash or array.
530             elsif ( $this->is_list_opener ) {
531 52         76 unshift( @tokens, $this );
532 52         96 my $object = $self->structure;
533 52 50       83 defined($object) or return undef;
534 52         123 push( @$rv, $object );
535             }
536              
537             # if string, add it to the array
538             elsif ( $this->is_string ) {
539             # add the string to the array
540 158         330 push( @$rv, $this->as_perl );
541              
542             # Check following token.
543 158 50       293 if ( @tokens ) {
544 158   50     371 my $next = $tokens[0] || '';
545             # Spec say: Commas are optional between objects pairs
546             # and array items.
547             # The next element must be a comma or the closing brace,
548             # or a string or list.
549             # Anything else is an error.
550 158 50 66     268 unless ( $next->token =~ /^[,\]]$/
      33        
551             || $next->is_string
552             || $next->is_list_opener ) {
553 0         0 return $self->error( 'missing_comma-between-array-elements',
554             $next );
555             }
556             }
557             }
558              
559             # Else unkown object or character, so throw error.
560             else {
561 1         7 return $self->error( 'unknown-array-token', $this );
562             }
563             }
564              
565             # If we get this far then unclosed brace.
566 0         0 return $self->error('unclosed-array-brace');
567             }
568              
569 0     0 0 0 method is_comment_opener( $pretok ) {
  0         0  
  0         0  
  0         0  
570 0 0       0 $pretok eq '//' || $pretok eq '/*';
571             }
572              
573 11     11   147778 use List::Util qw( min max uniqstr );
  11         32  
  11         96617  
574              
575 17     17 0 5293 method encode(%opts) {
  17         78  
  17         76  
  17         24  
576 17         62 my $schema = $opts{schema};
577 17   100     61 my $level = $opts{level} // 0;
578 17         33 my $rv = $opts{data}; # allow undef
579 17   50     62 my $indent = $opts{indent} // 2;
580 17   33     56 my $impoh = $opts{implied_outer_hash} // $implied_outer_hash;
581 17   33     57 my $ckeys = $opts{combined_keys} // $combined_keys;
582 17   33     60 my $prpmode = $opts{prp} // $prp;
583 17   66     65 my $pretty = $opts{pretty} // $pretty;
584 17   33     50 my $strict = $opts{strict} // $strict;
585 17   50     58 my $nouesc = $opts{nounicodeescapes} // 0;
586              
587 17 50       61 if ( $strict ) {
588 0         0 $ckeys = $prpmode = $impoh = 0;
589             }
590              
591 17 50       39 $schema = resolve( $schema, $schema ) if $schema;
592              
593 17         33 my $s = "";
594 17         26 my $i = 0;
595 17         35 my $props = $schema->{properties};
596             #warn("L$level - ", join(" ", sort keys(%$props)),"\n");
597              
598             # Add comments from schema, if any.
599 5     5   11 my $comments = sub( $p ) {
  5         9  
  5         10  
600 5         11 my $s = "";
601 5         10 my $did = 0;#$level;
602 5         13 for my $topic ( qw( title description ) ) {
603 10 50       31 next unless $p->{$topic};
604 0 0       0 $s .= "\n" unless $did++;
605             $s .= (" " x $i) . "// $_\n"
606 0         0 for split( /\s*|\\n|\n/, $p->{$topic} );
607             }
608 5         19 return $s;
609 17         129 };
610              
611 17 100       47 if ( !$level ) {
612 5         40 $s .= $comments->($schema);
613             }
614              
615             # Format a string value.
616 25     25   39 my $pr_string = sub ( $str, $force = 0 ) {
  25         44  
  25         47  
  25         35  
617              
618             # Reserved strings.
619 25 50       62 if ( !defined($str) ) {
620 0         0 return "null";
621             }
622              
623 25 100 66     186 if ( UNIVERSAL::isa( $str, 'JSON::Boolean' )
624             || UNIVERSAL::isa( $str, 'JSON::PP::Boolean' ) ) {
625 1         70 return (qw(false true))[$str]; # force string result
626             }
627              
628 24         47 my $v = $str;
629              
630             # Escapes.
631 24         64 $v =~ s/\\/\\\\/g;
632 24         43 $v =~ s/\n/\\n/g;
633 24         46 $v =~ s/\r/\\r/g;
634 24         45 $v =~ s/\f/\\f/g;
635 24         49 $v =~ s/\013/\\v/g;
636 24         74 $v =~ s/\010/\\b/g;
637 24         38 $v =~ s/\t/\\t/g;
638 24 0       85 $v =~ s/([^ -ÿ])/sprintf( ord($1) < 0xffff ? "\\u%04x" : "\\u{%x}", ord($1))/ge unless $nouesc;
  0 50       0  
639              
640             # Force quotes unless the string can be represented as unquoted.
641 24 100 33     614 if ( # contains escapes
      33        
      66        
      66        
      66        
      66        
      100        
642             $v ne $str
643             # not value-formed numeric
644             || ( $v =~ /^$p_number$/ && 0+$v ne $v )
645             # contains reserved, quotes or spaces
646             || $v =~ $p_reserved
647             || $v =~ $p_quotes
648             || $v =~ /\s/
649             || $v =~ /^(true|false|null)$/
650             || !length($v)
651             ) {
652 13 50       60 if ( $v !~ /\"/ ) {
653 13         54 return '"' . $v . '"';
654             }
655 0 0       0 if ( $v !~ /\'/ ) {
656 0         0 return "'" . $v . "'";
657             }
658 0 0       0 if ( $v !~ /\`/ ) {
659 0         0 return "`" . $v . "`";
660             }
661 0         0 return '"' . ($v =~ s/(["'`])/\\$1/rg) . '"';
662             }
663              
664             # Just a string.
665 11         39 return $v;
666 17         118 };
667              
668             # Format an array value.
669 4     4   7 my $pr_array = sub ( $rv, $level=0, $props = {} ) {
  4         8  
  4         8  
  4         10  
  4         7  
670 4 50       11 return "[]" unless @$rv;
671              
672             # Gather list of formatted values.
673 4         10 my @v = map { $self->encode( %opts,
  12         75  
674             data => $_,
675             level => $level+1,
676             schema => $props,
677             ) } @$rv;
678              
679 4 100       22 return "[".join(",",@v)."]" unless $pretty;
680              
681             # If sufficiently short, put it on one line.
682 2 50 33     112 if ( $i + length("@v") < 72
683             && join("",@v) !~ /\s|$p_newlines/ ) {
684 2         16 return "[ @v ]";
685             }
686              
687             # Put the values on separate lines.
688 0         0 my $s = "[\n";
689 0         0 $s .= s/^/(" " x ($i+$indent))/gemr . "\n" for @v;
  0         0  
690 0         0 $s .= (" " x $i) . "]";
691              
692 0         0 return $s;
693 17         107 };
694              
695             # Format a hash value.
696 17     8   30 my $pr_hash; $pr_hash = sub ( $rv, $level=0, $props = {} ) {
  8         14  
  8         16  
  8         17  
  8         15  
  8         13  
697 8 50       25 return "{}" unless keys(%$rv);
698              
699 8         17 my $s = "";
700              
701             # Opening brace.
702 8 100 66     36 if ( $level || !$impoh ) {
703 3 100       10 $s .= $pretty ? "{\n" : "{";
704 3         8 $i += $indent;
705             }
706              
707             # If we have a key order, use this and delete.
708             my @ko = $rv->{" key order "}
709 8 100       47 ? @{ delete($rv->{" key order "}) }
  2         10  
710             : sort(keys(%$rv));
711              
712             # Dedup.
713 8         39 @ko = uniqstr(@ko);
714              
715 8         16 my $ll = 0;
716 8         19 for ( @ko ) {
717             # This may be wrong if \ escapes or combined keys are involved.
718 10 100       34 $ll = length($_) if length($_) > $ll;
719             }
720              
721 8         16 for ( @ko ) {
722 10         18 my $k = $_;
723              
724             # Gather comments, if available.
725 10         19 my $comment;
726 10 50       29 if ( $props->{$k} ) {
727 0         0 $comment = $comments->($props->{$k});
728 0 0       0 $s .= $comment if $comment;
729             }
730              
731 10         22 my $v = $rv->{$k};
732 10         17 my $key = $k; # final key
733             # Combine keys if allowed and possible.
734 10   66     72 while ( $ckeys && ref($v) eq 'HASH' && keys(%$v) == 1 ) {
      100        
735 6         18 my $k = (keys(%$v))[0];
736 6         13 $key .= ".$k"; # append to final key
737 6         32 $v = $v->{$k}; # step to next
738             }
739              
740 10 100       29 $s .= (" " x $i) if $pretty;
741              
742             # Format the key, try to align on length. NEEDS WORK
743 10         23 my $t = $pr_string->($key);
744 10         21 my $l = length($t);
745 10         21 $s .= $t;
746 10 50       48 my $in = $comment ? "" : " " x max( 0, $ll-length($t) );
747              
748             # Handle object serialisation.
749 10   66     58 my $r = UNIVERSAL::can( $v, "TO_JSON" ) // UNIVERSAL::can( $v, "FREEZE" );
750 10 100       64 $r = $r ? $v->$r : $v;
751              
752             # Format the value.
753 10 100       67 if ( ref($r) eq 'HASH' ) {
    100          
    50          
754             # Make up and recurse.
755 3 100       13 if ( $pretty ) {
    50          
756 1 50       4 $s .= $prpmode ? " " : " : ";
757             }
758             elsif ( !$prpmode ) {
759 0         0 $s .= ":";
760             }
761              
762 3         89 $s .= $pr_hash->( $r, $level+1, $props->{$k}->{properties} );
763             }
764              
765             elsif ( ref($r) eq 'ARRAY' ) {
766 4 100       12 $s .= $pretty ? "$in : " : ":";
767 4         20 $s .= $pr_array->( $r, $level+1, $props->{$k}->{items} );
768             }
769              
770             elsif ( $pretty ) {
771 0         0 my $t = $pr_string->($r);
772 0         0 $s .= "$in : ";
773              
774             # Break quoted strings that contain pseudo-newlines.
775 0 0       0 if ( $t =~ /^["'`].*\\n/ ) {
776             # Remove the quotes/
777 0         0 my $quote = substr( $t, 0, 1, '');
778 0         0 chop($t);
779              
780             # Determine current indent.
781 0         0 $s =~ /^(.*)\Z/m;
782 0         0 my $sep = " \\\n" . (" " x length($1));
783              
784             # Get string parts.
785 0         0 my @a = split( /\\n/, $t, -1 );
786 0         0 while ( @a ) {
787 0         0 $s .= $quote.shift(@a);
788 0 0       0 $s .= "\\n" if @a;
789 0         0 $s .= $quote;
790 0 0       0 $s .= $sep if @a;
791             }
792             }
793              
794             # Just a string.
795             else {
796 0         0 $s .= $t;
797             }
798             }
799             else {
800 3         8 $s .= ":" . $pr_string->($r) . ",";
801             }
802 10 100       47 $s .= "\n" if $pretty;
803             }
804              
805             # Strip final comma.
806 8 100       47 $s =~ s/,$// unless $pretty;
807              
808             # Closing brace,.
809 8 100 66     31 if ( $level || !$impoh ) {
810 3         6 $i -= $indent;
811 3 100       10 $s .= (" " x $i) if $pretty;
812 3         22 $s .= "}";
813             }
814             else {
815 5         21 $s =~ s/\n+$//;
816             }
817              
818 8         35 return $s;
819 17         287 };
820              
821             # Handle object serialisation.
822 17   33     124 my $r = UNIVERSAL::can( $rv, "TO_JSON" ) // UNIVERSAL::can( $rv, "FREEZE" );
823 17 50       59 $r = $r ? $rv->$r : $rv;
824              
825             # From here it is straight forward.
826 17 100       51 if ( ref($r) eq 'HASH' ) {
    50          
827 5         15 $s .= $pr_hash->( $r, $level, $props );
828             }
829             elsif ( ref($r) eq 'ARRAY' ) {
830 0         0 $s .= $pr_array->( $r, $level );
831             }
832             else {
833 12         29 $s .= $pr_string->($r);
834             }
835              
836             # Final make-up.
837 17         42 $s =~ s/^ +$//gm;
838 17 100 100     51 if ( $pretty && !$level ) {
839 1         5 $s =~ s/^\n*//s;
840 1 50       5 $s .= "\n" if $s !~ /\n$/;
841             }
842 17         118 return $s;
843             }
844              
845             ################ Subroutines ################
846              
847             # resolve processes $ref, allOf etc nodes.
848              
849 0     0 0 0 sub resolve( $d, $schema ) {
  0         0  
  0         0  
  0         0  
850              
851 0 0       0 if ( is_hash($d) ) {
    0          
852 0         0 while ( my ($k,$v) = each %$d ) {
853 0 0 0     0 if ( $k eq 'allOf' ) {
    0          
    0          
854 0         0 delete $d->{$k}; # yes, safe to do
855 0         0 $d = merge( resolve( $_, $schema ), $d ) for @$v;
856             }
857             elsif ( $k eq 'oneOf' || $k eq 'anyOf' ) {
858 0         0 delete $d->{$k}; # yes, safe to do
859 0         0 $d = merge( resolve( $v->[0], $schema ), $d );
860             }
861             elsif ( $k eq '$ref' ) {
862 0         0 delete $d->{$k}; # yes, safe to do
863 0 0       0 if ( $v =~ m;^#/definitions/(.*); ) {
864 0         0 $d = merge( resolve( $schema->{definitions}->{$1}, $schema ), $d );
865             }
866             else {
867 0         0 die("Invalid \$ref: $v\n");
868             }
869             }
870             else {
871 0         0 $d->{$k} = resolve( $v, $schema );
872             }
873             }
874             }
875             elsif ( is_array($d) ) {
876 0         0 $d = [ map { resolve( $_, $schema ) } @$d ];
  0         0  
877             }
878             else {
879             }
880              
881 0         0 return $d;
882             }
883              
884 0     0 0 0 sub is_hash($o) { UNIVERSAL::isa( $o, 'HASH' ) }
  0         0  
  0         0  
  0         0  
885 0     0 0 0 sub is_array($o) { UNIVERSAL::isa( $o, 'ARRAY' ) }
  0         0  
  0         0  
  0         0  
886              
887 0     0 0 0 sub merge ( $left, $right ) {
  0         0  
  0         0  
  0         0  
888              
889 0 0       0 return $left unless $right;
890              
891 0         0 my %merged = %$left;
892              
893 0         0 for my $key ( keys %$right ) {
894              
895 0         0 my ($hr, $hl) = map { is_hash($_->{$key}) } $right, $left;
  0         0  
896              
897 0 0 0     0 if ( $hr and $hl ) {
898 0         0 $merged{$key} = merge( $left->{$key}, $right->{$key} );
899             }
900             else {
901 0         0 $merged{$key} = $right->{$key};
902             }
903             }
904              
905 0         0 return \%merged;
906             }
907              
908             ################ Tokens ################
909              
910             class JSON::Relaxed::Token;
911              
912 123 50   123   245 field $parent :accessor :param;
  123         443  
913 1023 50   1023   1577 field $token :accessor :param;
  1023         3060  
914 0 0   0   0 field $type :accessor :param;
  0         0  
915 18 50   18   43 field $offset :accessor :param;
  18         121  
916              
917 648     648   823 method is_string() {
  648         1146  
  648         756  
918 648         2193 $type =~ /[QUN]/
919             }
920              
921 234     234   329 method is_list_opener() {
  234         355  
  234         269  
922 234 100       1428 $type eq 'C' && $token =~ /[{\[]/;
923             }
924              
925 0     0   0 method as_perl( %options ) { # for values
  0         0  
  0         0  
  0         0  
926 0         0 $token->as_perl(%options);
927             }
928              
929 0     0   0 method _data_printer( $ddp ) { # for DDP
  0         0  
  0         0  
  0         0  
930 0         0 my $res = "Token(";
931 0 0       0 if ( !defined $token ) {
    0          
932 0         0 $res .= "null";
933             }
934             elsif ( $self->is_string ) {
935 0         0 $res .= $token->_data_printer($ddp);
936             }
937             else {
938 0         0 $res .= "\"$token\"";
939             }
940 0         0 $res .= ", $type";
941 0         0 $res . ", $offset)";
942             }
943              
944             method as_string { # for messages
945             my $res = "";
946             if ( $self->is_string ) {
947             $res = '"' . ($self->content =~ s/"/\\"/gr) . '"';
948             }
949             else {
950             $res .= "\"$token\"";
951             }
952             $res;
953             }
954              
955             =begin heavily_optimized_alternative
956              
957             package JSON::Relaxed::XXToken;
958             our @ISA = qw(JSON::Relaxed::Parser);
959              
960             sub new {
961             my ( $pkg, %opts ) = @_;
962             my $self = bless [] => $pkg;
963             push( @$self,
964             delete(%opts{parent}),
965             delete(%opts{token}),
966             delete(%opts{type}),
967             delete(%opts{offset}),
968             );
969             $self;
970             }
971              
972             sub parent { $_[0]->[0] }
973             sub token { $_[0]->[1] }
974             sub type { $_[0]->[2] }
975             sub offset { $_[0]->[3] }
976              
977             sub is_string { $_[0]->[2] =~ /[QUN]/ }
978             sub is_list_opener { $_[0]->[2] eq 'C' && $_[0]->[1] =~ /[{\[]/ }
979             sub as_perl { # for values
980             return shift->[1]->as_perl(@_);
981             }
982              
983             sub _data_printer { # for DDP
984             my ( $self, $ddp ) = @_;
985             my $res = "Token(";
986             if ( $self->is_string ) {
987             $res .= $self->[1]->_data_printer($ddp);
988             }
989             else {
990             $res .= "\"".$self->[1]."\"";
991             }
992             $res .= ", " . $self->[2];
993             $res . ", " . $self->[3] . ")";
994             }
995              
996             sub as_string { # for messages
997             if ( $_[0]->is_string ) {
998             return '"' . ($_[0]->[1]->content =~ s/"/\\"/gr) . '"';
999             }
1000             "\"" . $_[0]->[1] . "\"";
1001             }
1002              
1003             =cut
1004              
1005             ################ Strings ################
1006              
1007             class JSON::Relaxed::String :isa(JSON::Relaxed::Token);
1008              
1009             field $content :param = undef;
1010             field $quote :accessor :param = undef;
1011              
1012             # Quoted strings are assembled from complete substrings, so escape
1013             # processing is done on the substrings. This prevents ugly things
1014             # when unicode escapes are split across substrings.
1015             # Unquotes strings are collected token by token, so escape processing
1016             # can only be done on the complete string (on output).
1017              
1018 0 0   0   0 ADJUST {
  0         0  
1019             $content = $self->unescape($content) if defined($quote);
1020             };
1021              
1022 25     25   43 method append ($str) {
  25         52  
  25         40  
  25         35  
1023 25 100       62 $str = $self->unescape($str) if defined $quote;
1024 25         61 $content .= $str;
1025             }
1026              
1027             method content {
1028             defined($quote) ? $content : $self->unescape($content);
1029             }
1030              
1031             # One regexp to match them all...
1032             my $esc_quoted = qr/
1033             \\([tnrfb]) # $1 : one char
1034             | \\u\{([[:xdigit:]]+)\} # $2 : \u{XX...}
1035             | \\u([Dd][89abAB][[:xdigit:]]{2}) # $3 : \uDXXX hi
1036             \\u([Dd][c-fC-F][[:xdigit:]]{2}) # $4 : \uDXXX lo
1037             | \\u([[:xdigit:]]{4}) # $5 : \uXXXX
1038             | \\?(.) # $6
1039             /xs;
1040              
1041             # Special escapes (quoted strings only).
1042             my %esc = (
1043             'b' => "\b", # Backspace
1044             'f' => "\f", # Form feed
1045             'n' => "\n", # New line
1046             'r' => "\r", # Carriage return
1047             't' => "\t", # Tab
1048             'v' => chr(11), # Vertical tab
1049             );
1050              
1051 365     365   485 method unescape ($str) {
  365         625  
  365         512  
  365         420  
1052 365 100       1433 return $str unless $str =~ /\\/;
1053              
1054             my $convert = sub {
1055             # Specials. Only for quoted strings.
1056 560 100   560   1009 if ( defined($1) ) {
1057 3 50       12 return defined($quote) ? $esc{$1} : $1;
1058             }
1059              
1060             # Extended \u{XXX} character.
1061 557 100       975 defined($2) and return chr(hex($2));
1062              
1063             # Pair of surrogates.
1064 550 100       973 defined($3) and return pack( 'U*',
1065             0x10000 + (hex($3) - 0xD800) * 0x400
1066             + (hex($4) - 0xDC00) );
1067              
1068             # Standard \uXXXX character.
1069 542 100       970 defined($5) and return chr(hex($5));
1070              
1071             # Anything else.
1072 528 50       1850 defined($6) and return $6;
1073              
1074 0         0 return '';
1075 58         302 };
1076              
1077 58         1019 while( $str =~ s/\G$esc_quoted/$convert->()/gxse) {
  560         812  
1078 58 50       217 last unless defined pos($str);
1079             }
1080              
1081 58         628 return $str;
1082             }
1083              
1084             ################ Quoted Strings ################
1085              
1086             class JSON::Relaxed::String::Quoted :isa(JSON::Relaxed::String);
1087              
1088 185     185   229 method as_perl( %options ) {
  185         392  
  185         250  
  185         207  
1089 185         486 $self->content;
1090             }
1091              
1092 0     0   0 method _data_printer( $ddp ) {
  0         0  
  0         0  
  0         0  
1093 0         0 "Token(" . $self->quote . $self->content . $self->quote . ", " .
1094             $self->type . ", " . $self->offset . ")";
1095             }
1096              
1097             ################ Unquoted Strings ################
1098              
1099             class JSON::Relaxed::String::Unquoted :isa(JSON::Relaxed::String);
1100              
1101             # If the option always_string is set, bypass the reserved strings.
1102             # This is used for hash keys.
1103 163     163   241 method as_perl( %options ) {
  163         287  
  163         258  
  163         251  
1104 163         492 my $content = $self->content;
1105              
1106             # If used as a key, always return a string.
1107 163 100       472 return $content if $options{always_string};
1108              
1109             # Return boolean specials if appropriate.
1110 113 100       394 if ( $content =~ /^(?:true|false)$/ ) {
1111 14 100       37 return $self->parent->booleans->[ $content eq 'true' ? 1 : 0 ];
1112             }
1113 99 100 66     256 if ( $self->parent->prp && $content =~ /^(?:on|off)$/ ) {
    100          
1114 4 100       10 return $self->parent->booleans->[ $content eq 'on' ? 1 : 0 ];
1115             }
1116              
1117             # null -> undef
1118             elsif ( $content eq "null" ) {
1119 4         9 return undef;
1120             }
1121              
1122             # Return as string.
1123 91         306 $content;
1124             }
1125              
1126 0     0     method _data_printer( $ddp ) {
  0            
  0            
  0            
1127 0           "Token(«" . $self->content . "», " .
1128             $self->type . ", " . $self->offset . ")";
1129             }
1130              
1131             ################ Booleans ################
1132              
1133             # This class distinguises booleans true and false from numeric 1 and 0.
1134              
1135 11     11   79656 use JSON::PP ();
  11         429986  
  11         6335  
1136              
1137             package JSON::Boolean {
1138              
1139 0     0     sub as_perl( $self, %options ) { $self }
  0            
  0            
  0            
  0            
1140              
1141 0     0     sub _data_printer( $self, $ddp ) { "Bool($self)" }
  0            
  0            
  0            
  0            
1142              
1143 8 100   8   1173 use overload '""' => sub { ${$_[0]} ? "true" : "false" },
  8         179  
1144 1     1   4 "0+" => sub { ${$_[0]} },
  1         8  
1145 4     4   1897 "bool" => sub { !!${$_[0]} },
  4         25  
1146 11     11   129 fallback => 1;
  11         29  
  11         165  
1147              
1148             # For JSON::PP export.
1149 0 0   0     sub TO_JSON { ${$_[0]} ? $JSON::PP::true : $JSON::PP::false }
  0            
1150              
1151             # Boolean values.
1152             our $true = do { bless \(my $dummy = 1) => __PACKAGE__ };
1153             our $false = do { bless \(my $dummy = 0) => __PACKAGE__ };
1154              
1155             }
1156              
1157             ################
1158              
1159             1;