File Coverage

lib/ChordPro/lib/JSON/Relaxed/Parser.pm
Criterion Covered Total %
statement 188 526 35.7
branch 58 244 23.7
condition 28 142 19.7
subroutine 30 68 44.1
pod 0 35 0.0
total 304 1015 29.9


line stmt bran cond sub pod time code
1             #! perl
2              
3 90     90   1351 use v5.26;
  90         427  
4 90     90   593 use Object::Pad;
  90         220  
  90         966  
5 90     90   15554 use utf8;
  90         219  
  90         754  
6              
7             package JSON::Relaxed::Parser;
8              
9             our $VERSION = "0.098";
10              
11             class JSON::Relaxed::Parser;
12              
13             # Instance data.
14 0     0 0 0 field $data :mutator; # RJSON string being parser
  0         0  
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 0     0 0 0 field $strict :mutator :param = 0;
24              
25             # Allow extra stuff after the JSON structure.
26             # Strict mode only.
27 0     0 0 0 field $extra_tokens_ok :mutator :param = 0;
  0         0  
28              
29             # Define the values to be used for true and false.
30 0     126 0 0 field $booleans :mutator :param = 1;
  126         355  
31              
32             # Signal error with exceptions.
33 126     0 0 1128 field $croak_on_error :mutator :param = 1;
  0         0  
34 0         0 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     133300 0 0 field $prp :mutator :param = 1;
  133300         263318  
49              
50             # Formatted output.
51 133300     0 0 581195 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 0   0 0 0 field $err_id :accessor;
  0         0  
  0         0  
59 0 0   0 0 0 field $err_msg :accessor;
  0         0  
60 0 0   0 0 0 field $err_pos :accessor;
  0         0  
61              
62 133     133 0 375 method decode( $str ) {
  133         782  
  133         312  
  133         269  
63 133         291 $croak_on_error_internal = $croak_on_error;
64 133         741 $self->_decode($str);
65             }
66              
67             # Legacy.
68 0     0 0 0 method parse( $str ) {
  0         0  
  0         0  
  0         0  
69 0         0 $croak_on_error_internal = 0;
70 0         0 $self->_decode($str);
71             }
72              
73 133     133   309 method _decode( $str ) {
  133         424  
  133         288  
  133         284  
74              
75 133         469 $data = $str;
76 133 50 33     982 return $self->error('missing-input')
77             unless defined $data && length $data;
78              
79 133         345 undef $err_id;
80 133         293 $err_pos = -1;
81 133         262 undef $err_msg;
82              
83 133         1262 $self->pretokenize;
84 133 50       1695 return if $self->is_error;
85              
86 133         5250 $self->tokenize;
87 133 50       542 return $self->error('empty-input') unless @tokens;
88              
89 133         836 $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 510940     510940 0 810644 method is_reserved ($c) {
  510940         1110926  
  510940         835365  
  510940         732348  
105 510940         4382289 $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 121400     121400 0 248491 method is_quote ($c) {
  121400         242280  
  121400         204209  
  121400         184136  
121 121400         364175 $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 592150     592150 0 909455 method addtok( $tok, $typ, $off, $quote=undef ) {
  592150         1225653  
  592150         990724  
  592150         993808  
  592150         889853  
  592150         941764  
  592150         847728  
285              
286 592150 100 100     6858078 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 69314     69314 0 116478 method structure( %opts ) {
  69314         159175  
  69314         107956  
  69314         99032  
308              
309 69314 50       170865 @tokens = @{$opts{tokens}} if $opts{tokens}; # for debugging
  0         0  
310              
311 69314 50 33     246658 if ( $implied_outer_hash && !$strict ) {
312             # Note that = can only occur with $prp.
313 69314 100 66     238089 if ( @tokens > 2 && $tokens[0]->is_string
      66        
314             && $tokens[1]->token =~ /[:={]/ ) {
315 132         842 $self->addtok( '}', 'C', $tokens[-1]->offset );
316 132         617 $self->addtok( '{', 'C', $tokens[0]->offset );
317 132         4907 unshift( @tokens, pop(@tokens ));
318             }
319             }
320              
321 69314   50     189223 my $this = shift(@tokens) // return;
322 69314         110950 my $rv;
323              
324 69314 50       141393 if ( $this->is_string ) { # (un)quoted string
325 0         0 $rv = $this->as_perl;
326             }
327             else {
328 69314         160794 my $t = $this->token;
329 69314 100       177195 if ( $t eq '{' ) {
    50          
330 50139         124032 $rv = $self->build_hash;
331             }
332             elsif ( $t eq '[' ) {
333 19175         50441 $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 69314 0 66     208548 if ( $opts{top}
      0        
      33        
      0        
343             && @tokens
344             && ( $strict || !$extra_tokens_ok )
345             && !$self->is_error
346             ) {
347 0         0 return $self->error( 'multiple-structures', $tokens[0] );
348             }
349              
350 69314         204811 return $rv;
351             }
352              
353              
354 0     0 0 0 method error( $id, $aux = undef ) {
  0         0  
  0         0  
  0         0  
  0         0  
355 0         0 require JSON::Relaxed::ErrorCodes;
356 0         0 $err_id = $id;
357 0 0       0 $err_pos = $aux ? $aux->offset : -1;
358 0         0 $err_msg = JSON::Relaxed::ErrorCodes->message( $id, $aux );
359              
360 0 0       0 die( $err_msg, "\n" ) if $croak_on_error_internal;
361 0         0 return; # undef
362             }
363              
364 119973     119973 0 194173 method is_error() {
  119973         237125  
  119973         171903  
365 119973         319133 $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 50139     50139 0 77229 method build_hash() {
  50139         107242  
  50139         73692  
381              
382 50139         92610 my $rv = {};
383 50139         77128 my @ko; # order of keys
384              
385 50139         104544 while ( @tokens ) {
386 169846         307146 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 169846         386884 my $t = $this->token;
394 169846 100       396818 if ( $t eq '}' ) {
395 50139 0 33     151525 $rv->{" key order "} = \@ko
      33        
396             if $key_order && !$strict && @ko > 1;
397 50139         179974 return $rv;
398             }
399              
400             # If comma, do nothing.
401 119707 50       265677 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 119707 50       252390 if ( $this->is_string ) {
407 119707         203558 my ( $key, $value );
408              
409             # Set key using string.
410 119707         290883 $key = $this->as_perl( always_string => 1 );
411 119707         368799 $self->set_value( $rv, $key );
412 119707 50       267017 if ( $key_order ) {
413 0 0 0     0 if ( $combined_keys && !$strict ) {
414 0         0 push( @ko, $key =~ s/\..*//r );
415             }
416             else {
417 0         0 push( @ko, $key );
418             }
419             }
420              
421 119707         200197 my $next = $tokens[0];
422             # If anything follows the string.
423 119707 50       241053 last unless defined $next;
424              
425             # A comma or closing brace is acceptable after a string.
426 119707 50 33     268912 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 119707 100 33     282409 if ( $next->token =~ /^[:=]$/ ) {
    50          
431             # Step past the colon.
432 119443         203369 shift(@tokens);
433              
434             # If at end of token array, exit loop.
435 119443 50       259979 last unless @tokens;
436              
437             # Get hash value.
438 119443         270935 $value = $self->get_value;
439              
440             # If there is a global error, return undef.
441 119443 50       333812 return undef if $self->is_error;
442             }
443              
444             # Extension (prp): Implied colon.
445             elsif ( $prp && $next->token eq '{' ) {
446             # Get hash value.
447 264         1032 $value = $self->get_value;
448              
449             # If there is a global error, return undef.
450 264 50       736 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 119707         307224 $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 119707     119707 0 177791 method get_value() {
  119707         256154  
  119707         169842  
474              
475             # Get token.
476 119707         196918 my $this = shift(@tokens);
477              
478             # Token must be string, array, or hash.
479              
480             # String.
481 119707 100       288370 if ( $this->is_string ) {
    50          
482 100377         233834 return $this->as_perl;
483             }
484              
485             # Token opens a hash or array.
486             elsif ( $this->is_list_opener ) {
487 19330         39237 unshift( @tokens, $this );
488 19330         58226 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 239414     239414 0 348157 method set_value ( $rv, $key, $value = undef ) {
  239414         480366  
  239414         356072  
  239414         374056  
  239414         401305  
  239414         335850  
496 239414 100 33     1834285 return $rv->{$key} = $value
      66        
497             unless $combined_keys && !$strict && $key =~ /\./s;
498              
499 238         1378 my @keys = split(/\./, $key, -1 );
500 238         826 my $c = \$rv;
501 238         1056 for ( @keys ) {
502 714 50       3244 if ( /^[+-]?\d+$/ ) {
503 0         0 $c = \( $$c->[$_] );
504             }
505             else {
506 714         2754 $c = \( $$c->{$_} );
507             }
508             }
509 238         1343 $$c = $value;
510             }
511              
512 19175     19175 0 29369 method build_array() {
  19175         40677  
  19175         28363  
513              
514 19175         33989 my $rv = [];
515              
516             # Build array. Work through tokens until closing brace.
517 19175         42866 while ( @tokens ) {
518 183021         310630 my $this = shift(@tokens);
519              
520 183021         386298 my $t = $this->token;
521             # Closing brace: we're done building this array.
522 183021 100       496523 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 163846 50 0     562233 if ( $t eq ',' && (!$strict || @$rv) ) {
    100 33        
    50          
527             }
528              
529             # Opening brace of hash or array.
530             elsif ( $this->is_list_opener ) {
531 49851         110325 unshift( @tokens, $this );
532 49851         124292 my $object = $self->structure;
533 49851 50       127881 defined($object) or return undef;
534 49851         312776 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 113995         268074 push( @$rv, $this->as_perl );
541              
542             # Check following token.
543 113995 50       263637 if ( @tokens ) {
544 113995   50     266015 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 113995 50 100     241887 unless ( $next->token =~ /^[,\]]$/
      66        
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 0         0 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 90     90   1056370 use List::Util qw( min max uniqstr );
  90         244  
  90         696273  
574              
575 0     0 0 0 method encode(%opts) {
  0         0  
  0         0  
  0         0  
576 0         0 my $schema = $opts{schema};
577 0   0     0 my $level = $opts{level} // 0;
578 0         0 my $rv = $opts{data}; # allow undef
579 0   0     0 my $indent = $opts{indent} // 2;
580 0   0     0 my $impoh = $opts{implied_outer_hash} // $implied_outer_hash;
581 0   0     0 my $ckeys = $opts{combined_keys} // $combined_keys;
582 0   0     0 my $prpmode = $opts{prp} // $prp;
583 0   0     0 my $pretty = $opts{pretty} // $pretty;
584 0   0     0 my $strict = $opts{strict} // $strict;
585 0   0     0 my $nouesc = $opts{nounicodeescapes} // 0;
586              
587 0 0       0 if ( $strict ) {
588 0         0 $ckeys = $prpmode = $impoh = 0;
589             }
590              
591 0 0       0 $schema = resolve( $schema, $schema ) if $schema;
592              
593 0         0 my $s = "";
594 0         0 my $i = 0;
595 0         0 my $props = $schema->{properties};
596             #warn("L$level - ", join(" ", sort keys(%$props)),"\n");
597              
598             # Add comments from schema, if any.
599 0     0   0 my $comments = sub( $p ) {
  0         0  
  0         0  
600 0         0 my $s = "";
601 0         0 my $did = 0;#$level;
602 0         0 for my $topic ( qw( title description ) ) {
603 0 0       0 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 0         0 return $s;
609 0         0 };
610              
611 0 0       0 if ( !$level ) {
612 0         0 $s .= $comments->($schema);
613             }
614              
615             # Format a string value.
616 0     0   0 my $pr_string = sub ( $str, $force = 0 ) {
  0         0  
  0         0  
  0         0  
617              
618             # Reserved strings.
619 0 0       0 if ( !defined($str) ) {
620 0         0 return "null";
621             }
622              
623 0 0 0     0 if ( UNIVERSAL::isa( $str, 'JSON::Boolean' )
624             || UNIVERSAL::isa( $str, 'JSON::PP::Boolean' ) ) {
625 0         0 return (qw(false true))[$str]; # force string result
626             }
627              
628 0         0 my $v = $str;
629              
630             # Escapes.
631 0         0 $v =~ s/\\/\\\\/g;
632 0         0 $v =~ s/\n/\\n/g;
633 0         0 $v =~ s/\r/\\r/g;
634 0         0 $v =~ s/\f/\\f/g;
635 0         0 $v =~ s/\013/\\v/g;
636 0         0 $v =~ s/\010/\\b/g;
637 0         0 $v =~ s/\t/\\t/g;
638 0 0       0 $v =~ s/([^ -ÿ])/sprintf( ord($1) < 0xffff ? "\\u%04x" : "\\u{%x}", ord($1))/ge unless $nouesc;
  0 0       0  
639              
640             # Force quotes unless the string can be represented as unquoted.
641 0 0 0     0 if ( # contains escapes
      0        
      0        
      0        
      0        
      0        
      0        
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 0 0       0 if ( $v !~ /\"/ ) {
653 0         0 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 0         0 return $v;
666 0         0 };
667              
668             # Format an array value.
669 0     0   0 my $pr_array = sub ( $rv, $level=0, $props = {} ) {
  0         0  
  0         0  
  0         0  
  0         0  
670 0 0       0 return "[]" unless @$rv;
671              
672             # Gather list of formatted values.
673 0         0 my @v = map { $self->encode( %opts,
  0         0  
674             data => $_,
675             level => $level+1,
676             schema => $props,
677             ) } @$rv;
678              
679 0 0       0 return "[".join(",",@v)."]" unless $pretty;
680              
681             # If sufficiently short, put it on one line.
682 0 0 0     0 if ( $i + length("@v") < 72
683             && join("",@v) !~ /\s|$p_newlines/ ) {
684 0         0 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 0         0 };
694              
695             # Format a hash value.
696 0     0   0 my $pr_hash; $pr_hash = sub ( $rv, $level=0, $props = {} ) {
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
697 0 0       0 return "{}" unless keys(%$rv);
698              
699 0         0 my $s = "";
700              
701             # Opening brace.
702 0 0 0     0 if ( $level || !$impoh ) {
703 0 0       0 $s .= $pretty ? "{\n" : "{";
704 0         0 $i += $indent;
705             }
706              
707             # If we have a key order, use this and delete.
708             my @ko = $rv->{" key order "}
709 0 0       0 ? @{ delete($rv->{" key order "}) }
  0         0  
710             : sort(keys(%$rv));
711              
712             # Dedup.
713 0         0 @ko = uniqstr(@ko);
714              
715 0         0 my $ll = 0;
716 0         0 for ( @ko ) {
717             # This may be wrong if \ escapes or combined keys are involved.
718 0 0       0 $ll = length($_) if length($_) > $ll;
719             }
720              
721 0         0 for ( @ko ) {
722 0         0 my $k = $_;
723              
724             # Gather comments, if available.
725 0         0 my $comment;
726 0 0       0 if ( $props->{$k} ) {
727 0         0 $comment = $comments->($props->{$k});
728 0 0       0 $s .= $comment if $comment;
729             }
730              
731 0         0 my $v = $rv->{$k};
732 0         0 my $key = $k; # final key
733             # Combine keys if allowed and possible.
734 0   0     0 while ( $ckeys && ref($v) eq 'HASH' && keys(%$v) == 1 ) {
      0        
735 0         0 my $k = (keys(%$v))[0];
736 0         0 $key .= ".$k"; # append to final key
737 0         0 $v = $v->{$k}; # step to next
738             }
739              
740 0 0       0 $s .= (" " x $i) if $pretty;
741              
742             # Format the key, try to align on length. NEEDS WORK
743 0         0 my $t = $pr_string->($key);
744 0         0 my $l = length($t);
745 0         0 $s .= $t;
746 0 0       0 my $in = $comment ? "" : " " x max( 0, $ll-length($t) );
747              
748             # Handle object serialisation.
749 0   0     0 my $r = UNIVERSAL::can( $v, "TO_JSON" ) // UNIVERSAL::can( $v, "FREEZE" );
750 0 0       0 $r = $r ? $v->$r : $v;
751              
752             # Format the value.
753 0 0       0 if ( ref($r) eq 'HASH' ) {
    0          
    0          
754             # Make up and recurse.
755 0 0       0 if ( $pretty ) {
    0          
756 0 0       0 $s .= $prpmode ? " " : " : ";
757             }
758             elsif ( !$prpmode ) {
759 0         0 $s .= ":";
760             }
761              
762 0         0 $s .= $pr_hash->( $r, $level+1, $props->{$k}->{properties} );
763             }
764              
765             elsif ( ref($r) eq 'ARRAY' ) {
766 0 0       0 $s .= $pretty ? "$in : " : ":";
767 0         0 $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 0         0 $s .= ":" . $pr_string->($r) . ",";
801             }
802 0 0       0 $s .= "\n" if $pretty;
803             }
804              
805             # Strip final comma.
806 0 0       0 $s =~ s/,$// unless $pretty;
807              
808             # Closing brace,.
809 0 0 0     0 if ( $level || !$impoh ) {
810 0         0 $i -= $indent;
811 0 0       0 $s .= (" " x $i) if $pretty;
812 0         0 $s .= "}";
813             }
814             else {
815 0         0 $s =~ s/\n+$//;
816             }
817              
818 0         0 return $s;
819 0         0 };
820              
821             # Handle object serialisation.
822 0   0     0 my $r = UNIVERSAL::can( $rv, "TO_JSON" ) // UNIVERSAL::can( $rv, "FREEZE" );
823 0 0       0 $r = $r ? $rv->$r : $rv;
824              
825             # From here it is straight forward.
826 0 0       0 if ( ref($r) eq 'HASH' ) {
    0          
827 0         0 $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 0         0 $s .= $pr_string->($r);
834             }
835              
836             # Final make-up.
837 0         0 $s =~ s/^ +$//gm;
838 0 0 0     0 if ( $pretty && !$level ) {
839 0         0 $s =~ s/^\n*//s;
840 0 0       0 $s .= "\n" if $s !~ /\n$/;
841             }
842 0         0 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 133426 50   133426   270084 field $parent :accessor :param;
  133426         445340  
913 895693 50   895693   1792969 field $token :accessor :param;
  895693         3188978  
914 0 0   0   0 field $type :accessor :param;
  0         0  
915 264 50   264   723 field $offset :accessor :param;
  264         1400  
916              
917 586976     586976   851950 method is_string() {
  586976         1114755  
  586976         805812  
918 586976         2207763 $type =~ /[QUN]/
919             }
920              
921 183285     183285   280068 method is_list_opener() {
  183285         344719  
  183285         259492  
922 183285 100       772233 $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 0     0   0 method append ($str) {
  0         0  
  0         0  
  0         0  
1023 0 0       0 $str = $self->unescape($str) if defined $quote;
1024 0         0 $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 334079     334079   530270 method unescape ($str) {
  334079         636575  
  334079         567964  
  334079         475935  
1052 334079 50       1482797 return $str unless $str =~ /\\/;
1053              
1054             my $convert = sub {
1055             # Specials. Only for quoted strings.
1056 0 0   0   0 if ( defined($1) ) {
1057 0 0       0 return defined($quote) ? $esc{$1} : $1;
1058             }
1059              
1060             # Extended \u{XXX} character.
1061 0 0       0 defined($2) and return chr(hex($2));
1062              
1063             # Pair of surrogates.
1064 0 0       0 defined($3) and return pack( 'U*',
1065             0x10000 + (hex($3) - 0xD800) * 0x400
1066             + (hex($4) - 0xDC00) );
1067              
1068             # Standard \uXXXX character.
1069 0 0       0 defined($5) and return chr(hex($5));
1070              
1071             # Anything else.
1072 0 0       0 defined($6) and return $6;
1073              
1074 0         0 return '';
1075 0         0 };
1076              
1077 0         0 while( $str =~ s/\G$esc_quoted/$convert->()/gxse) {
  0         0  
1078 0 0       0 last unless defined pos($str);
1079             }
1080              
1081 0         0 return $str;
1082             }
1083              
1084             ################ Quoted Strings ################
1085              
1086             class JSON::Relaxed::String::Quoted :isa(JSON::Relaxed::String);
1087              
1088 80946     80946   144204 method as_perl( %options ) {
  80946         170341  
  80946         122477  
  80946         115986  
1089 80946         229233 $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 253133     253133   384946 method as_perl( %options ) {
  253133         482729  
  253133         473595  
  253133         357334  
1104 253133         686758 my $content = $self->content;
1105              
1106             # If used as a key, always return a string.
1107 253133 100       759987 return $content if $options{always_string};
1108              
1109             # Return boolean specials if appropriate.
1110 133426 100       278189 if ( $content =~ /^(?:true|false)$/ ) {
1111 126 50       548 return $self->parent->booleans->[ $content eq 'true' ? 1 : 0 ];
1112             }
1113 133300 50 33     304234 if ( $self->parent->prp && $content =~ /^(?:on|off)$/ ) {
    50          
1114 0 0       0 return $self->parent->booleans->[ $content eq 'on' ? 1 : 0 ];
1115             }
1116              
1117             # null -> undef
1118             elsif ( $content eq "null" ) {
1119 0         0 return undef;
1120             }
1121              
1122             # Return as string.
1123 133300         399035 $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 90     90   567631 use JSON::PP ();
  90         2372424  
  90         57005  
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 51705 100   51705   75773 use overload '""' => sub { ${$_[0]} ? "true" : "false" },
  51705         222554  
1144 24     24   48 "0+" => sub { ${$_[0]} },
  24         99  
1145 197221     197221   309119 "bool" => sub { !!${$_[0]} },
  197221         582669  
1146 90     90   1162 fallback => 1;
  90         222  
  90         2789  
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;