File Coverage

blib/lib/ThaiSchema/JSON.pm
Criterion Covered Total %
statement 147 182 80.7
branch 73 106 68.8
condition 7 17 41.1
subroutine 16 20 80.0
pod 0 6 0.0
total 243 331 73.4


line stmt bran cond sub pod time code
1             package ThaiSchema::JSON;
2 3     3   28913 use strict;
  3         8  
  3         73  
3 3     3   12 use warnings;
  3         6  
  3         60  
4 3     3   11 use utf8;
  3         7  
  3         12  
5              
6 3     3   653 use ThaiSchema;
  3         8  
  3         185  
7 3     3   1345 use Encode ();
  3         21278  
  3         5997  
8              
9             # Licensed under the Artistic 2.0 license.
10             # See http://www.perlfoundation.org/artistic_license_2_0.
11              
12             # This module is based on JSON::Tiny 0.22
13              
14             my $FALSE = \0;
15             my $TRUE = \1;
16              
17             sub ddf {
18 0     0 0 0 require Data::Dumper;
19 0         0 local $Data::Dumper::Terse = 1;
20 0         0 Data::Dumper::Dumper(@_);
21             }
22              
23             # Escaped special character map (with u2028 and u2029)
24             my %ESCAPE = (
25             '"' => '"',
26             '\\' => '\\',
27             '/' => '/',
28             'b' => "\x07",
29             'f' => "\x0C",
30             'n' => "\x0A",
31             'r' => "\x0D",
32             't' => "\x09",
33             'u2028' => "\x{2028}",
34             'u2029' => "\x{2029}"
35             );
36             my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE;
37             for ( 0x00 .. 0x1F, 0x7F ) { $REVERSE{ pack 'C', $_ } //= sprintf '\u%.4X', $_ }
38              
39             # Unicode encoding detection
40             my $UTF_PATTERNS = {
41             'UTF-32BE' => qr/^\0\0\0[^\0]/,
42             'UTF-16BE' => qr/^\0[^\0]\0[^\0]/,
43             'UTF-32LE' => qr/^[^\0]\0\0\0/,
44             'UTF-16LE' => qr/^[^\0]\0[^\0]\0/
45             };
46             my $WHITESPACE_RE = qr/[\x20\x09\x0a\x0d]*/;
47              
48             our $FAIL;
49              
50             our @_ERRORS;
51             our $_NAME = '';
52              
53             sub new {
54 41     41 0 2260 my $class = shift;
55 41 0       113 bless @_ ? @_ > 1 ? {@_} : { %{ $_[0] } } : {}, $class;
  0 50       0  
56             }
57              
58             sub error {
59 98 100   98 0 276 $_[0]->{error} = $_[1] if @_ > 1;
60 98         189 return $_[0]->{error};
61             }
62              
63             sub validate {
64 41     41 0 595 my ( $self, $bytes, $schema ) = @_;
65 41         88 $schema = _schema($schema);
66              
67 41         61 local $FAIL;
68 41         69 local @_ERRORS;
69 41         64 local $_NAME = '';
70              
71             # Cleanup
72 41         126 $self->error(undef);
73              
74             # Missing input
75 41 50 0     103 $self->error('Missing or empty input') and return undef
76             unless $bytes; ## no critic (undef)
77              
78             # Remove BOM
79 41         105 $bytes =~
80             s/^(?:\357\273\277|\377\376\0\0|\0\0\376\377|\376\377|\377\376)//g;
81              
82             # Wide characters
83 41 50 0     120 $self->error('Wide character in input')
84             and return undef ## no critic (undef)
85             unless utf8::downgrade( $bytes, 1 );
86              
87             # Detect and decode Unicode
88 41         69 my $encoding = 'UTF-8';
89 41   33     409 $bytes =~ $UTF_PATTERNS->{$_} and $encoding = $_ for keys %$UTF_PATTERNS;
90              
91 41         90 my $d_res = eval { $bytes = Encode::decode( $encoding, $bytes, 1 ); 1 };
  41         117  
  41         2305  
92 41 50       110 $bytes = undef unless $d_res;
93              
94             # Object or array
95 41         69 my $res = eval {
96 41         63 local $_ = $bytes;
97              
98             # Leading whitespace
99 41         269 m/\G$WHITESPACE_RE/gc;
100              
101             # Array
102 41         76 my $ref;
103 41 100       138 if (m/\G\[/gc) {
    50          
104 20 100       58 unless ($schema->is_array()) {
105 1         4 _exception2("Unexpected array found.");
106             }
107 19         47 $ref = _decode_array($schema->schema)
108             }
109              
110             # Object
111             elsif (m/\G\{/gc) {
112 21 100       62 unless ($schema->is_hash()) {
113 1         3 _exception2("Unexpected object found.");
114             }
115 20         47 $ref = _decode_object($schema)
116             }
117              
118             # Unexpected
119 0         0 else { _exception('Expected array or object') }
120              
121             # Leftover data
122 33 50       152 unless (m/\G$WHITESPACE_RE\z/gc) {
123 0 0       0 my $got = ref $ref eq 'ARRAY' ? 'array' : 'object';
124 0         0 _exception("Unexpected data after $got");
125             }
126              
127 33         90 $ref;
128             };
129              
130             # Exception
131 41 100 66     136 if ( !$res && ( my $e = $@ ) ) {
132 8         17 chomp $e;
133 8         17 $self->error($e);
134             }
135              
136 41 100       87 if ($self->error) {
137 8         17 push @_ERRORS, $self->error;
138 8         14 $FAIL++;
139             }
140              
141             # return ($ok, \@errors);
142 41         171 return (!$FAIL, \@_ERRORS);
143             }
144              
145             sub _fail {
146 16     16   34 my ($got, $schema) = @_;
147 16 50       59 _fail2(($_NAME ? "$_NAME: " : '') . $schema->name . " is expected, but $got is found");
148             }
149              
150             sub _fail2 {
151 17     17   34 my ($msg) = @_;
152 17         26 $FAIL++;
153 17         37 push @_ERRORS, $msg;
154             }
155              
156 0     0 0 0 sub false { $FALSE }
157 0     0 0 0 sub true { $TRUE }
158              
159             sub _decode_array {
160 26     26   49 my $schema = _schema(shift);
161              
162 26         34 my @array;
163 26         42 my $i = 0;
164 26         138 until (m/\G$WHITESPACE_RE\]/gc) {
165 24         61 local $_NAME = $_NAME . "[$i]";
166              
167             # Value
168 24         53 push @array, _decode_value($schema);
169              
170 20         36 $i++;
171              
172             # Separator
173 20 50       81 redo if m/\G$WHITESPACE_RE,/gc;
174              
175             # End
176 20 50       123 last if m/\G$WHITESPACE_RE\]/gc;
177              
178             # Invalid character
179 0         0 _exception(
180             'Expected comma or right square bracket while parsing array');
181             }
182              
183 22         60 return \@array;
184             }
185              
186             sub _decode_object {
187 25     25   47 my $schema = _schema(shift);
188              
189 25         35 my %hash;
190 25 100       120 my %schema = $schema->isa("ThaiSchema::Maybe") ? %{$schema->schema->schema} : %{$schema->schema};
  1         4  
  24         62  
191 25         187 until (m/\G$WHITESPACE_RE\}/gc) {
192              
193             # Quote
194 28 50       182 m/\G$WHITESPACE_RE"/gc
195             or _exception('Expected string while parsing object');
196              
197             # Key
198 28         62 my $key = _decode_string();
199              
200             # Colon
201 28 50       156 m/\G$WHITESPACE_RE:/gc
202             or _exception('Expected colon while parsing object');
203              
204             # Value
205 28         75 local $_NAME = $_NAME . ".$key";
206 28         69 my $cschema = delete $schema{$key};
207 28 100       75 if ($cschema) {
208 22         53 $hash{$key} = _decode_value($cschema);
209             } else {
210 6 100       14 if ($ThaiSchema::ALLOW_EXTRA) {
211 5         21 $hash{$key} = _decode_value(ThaiSchema::Extra->new());
212             } else {
213 1         5 _exception2("There is extra key: $key");
214             }
215             }
216              
217             # Separator
218 26 100       140 redo if m/\G$WHITESPACE_RE,/gc;
219              
220             # End
221 21 50       123 last if m/\G$WHITESPACE_RE\}/gc;
222              
223             # Invalid character
224 0         0 _exception(
225             'Expected comma or right curly bracket while parsing object');
226             }
227              
228 23 100       62 if (%schema) {
229 1         6 _fail2('There is missing keys: ' . join(', ', keys %schema));
230             }
231              
232 23         56 return \%hash;
233             }
234              
235             sub _decode_string {
236 34     34   73 my $pos = pos;
237              
238             # Extract string with escaped characters
239 34         149 m#\G(((?:[^\x00-\x1F\\"]|\\(?:["\\/bfnrt]|u[[:xdigit:]]{4})){0,32766})*)#gc;
240 34         90 my $str = $1;
241              
242             # Missing quote
243 34 50       94 unless (m/\G"/gc) {
244 0 0       0 _exception(
245             'Unexpected character or invalid escape while parsing string')
246             if m/\G[\x00-\x1F\\]/;
247 0         0 _exception('Unterminated string');
248             }
249              
250             # Unescape popular characters
251 34 50       118 if ( index( $str, '\\u' ) < 0 ) {
252 34         66 $str =~ s!\\(["\\/bfnrt])!$ESCAPE{$1}!gs;
253 34         78 return $str;
254             }
255              
256             # Unescape everything else
257 0         0 my $buffer = '';
258 0         0 while ( $str =~ m/\G([^\\]*)\\(?:([^u])|u(.{4}))/gc ) {
259 0         0 $buffer .= $1;
260              
261             # Popular character
262 0 0       0 if ($2) { $buffer .= $ESCAPE{$2} }
  0         0  
263              
264             # Escaped
265             else {
266 0         0 my $ord = hex $3;
267              
268             # Surrogate pair
269 0 0       0 if ( ( $ord & 0xF800 ) == 0xD800 ) {
270              
271             # High surrogate
272 0 0       0 ( $ord & 0xFC00 ) == 0xD800
273             or pos($_) = $pos + pos($str),
274             _exception('Missing high-surrogate');
275              
276             # Low surrogate
277 0 0       0 $str =~ m/\G\\u([Dd][C-Fc-f]..)/gc
278             or pos($_) = $pos + pos($str),
279             _exception('Missing low-surrogate');
280              
281             # Pair
282 0         0 $ord =
283             0x10000 + ( $ord - 0xD800 ) * 0x400 + ( hex($1) - 0xDC00 );
284             }
285              
286             # Character
287 0         0 $buffer .= pack 'U', $ord;
288             }
289             }
290              
291             # The rest
292 0         0 return $buffer . substr $str, pos($str), length($str);
293             }
294              
295             sub _schema {
296 145     145   213 my $schema = shift;
297 145 100       377 if (ref $schema eq 'HASH') {
    100          
298 9         32 return ThaiSchema::Hash->new(schema => $schema);
299             } elsif (ref $schema eq 'ARRAY') {
300 2 50       8 if (@$schema > 1) {
301 0         0 Carp::confess("Invalid schema: too many elements in arrayref: " . ddf($schema));
302             }
303 2         7 return ThaiSchema::Array->new(schema => _schema($schema->[0]));
304             } else {
305 134         255 return $schema;
306             }
307             }
308              
309             sub _decode_value {
310 51     51   92 my $schema = _schema(shift);
311              
312             # Leading whitespace
313 51         178 m/\G$WHITESPACE_RE/gc;
314              
315             # String
316 51 100       158 if (m/\G"/gc) {
317 6 100       36 unless ($schema->is_string) {
318 4         8 _fail('string', $schema);
319             }
320 6         17 return _decode_string();
321             }
322              
323             # Array
324 45 100       106 if (m/\G\[/gc) {
325 9 100       31 unless ($schema->is_array) {
326 2         10 _fail('array', $schema);
327 2         5 _exception2("Unexpected array.");
328             }
329 7         20 return _decode_array($schema->schema);
330             }
331              
332             # Object
333 36 100       85 if (m/\G\{/gc) {
334 8 100       53 unless ($schema->is_hash) {
335 3         8 _fail('object', $schema);
336 3         7 _exception2("Unexpected hash.");
337             }
338 5         20 return _decode_object($schema);
339             }
340              
341             # Number
342 28 100       98 if (m/\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc) {
343 16         51 my $number = 0+$1;
344 16 100       53 unless ($schema->is_number) {
345 2         6 _fail('number', $schema);
346             }
347 16 100 100     44 if ($schema->is_integer && int($number) != $number) {
348 1         12 push @_ERRORS, "integer is expected, but you got $number";
349 1         3 $FAIL++;
350             }
351 16         50 return $number;
352             }
353              
354             # True
355 12 100       36 if (m/\Gtrue/gc) {
356 6 100       22 unless ($schema->is_bool) {
357 2         10 _fail('true', $schema);
358             }
359 6         14 return $TRUE;
360             }
361              
362             # False
363 6 100       29 if (m/\Gfalse/gc) {
364 2 100       6 unless ($schema->is_bool) {
365 1         4 _fail('false', $schema);
366             }
367 2         5 return $FALSE;
368             }
369              
370             # Null
371 4 50       20 if (m/\Gnull/gc) {
372 4 100       21 unless ($schema->is_null) {
373 2         5 _fail('null', $schema);
374             }
375             ## no critic (return)
376 4         12 return undef;
377             }
378              
379             # Invalid data
380 0         0 _exception('Expected string, array, object, number, boolean or null');
381             }
382              
383             sub _exception2 {
384             # Leading whitespace
385 8     8   39 m/\G$WHITESPACE_RE/gc;
386              
387             # Context
388 8         15 my $context;
389 8 100       24 $context .= "$_NAME: " if $_NAME;
390 8         13 $context .= shift;
391 8 50       22 if (m/\G\z/gc) { $context .= ' before end of data' }
  0         0  
392             else {
393 8         35 my @lines = split /\n/, substr( $_, 0, pos );
394 8   50     33 $context .=
395             ' at line ' . @lines . ', offset ' . length( pop @lines || '' );
396             }
397              
398             # Throw
399 8         51 die "$context\n";
400             }
401              
402             sub _exception {
403              
404             # Leading whitespace
405 0     0     m/\G$WHITESPACE_RE/gc;
406              
407             # Context
408 0           my $context = 'Malformed JSON: ' . shift;
409 0 0         if (m/\G\z/gc) { $context .= ' before end of data' }
  0            
410             else {
411 0           my @lines = split /\n/, substr( $_, 0, pos );
412 0   0       $context .=
413             ' at line ' . @lines . ', offset ' . length( pop @lines || '' );
414             }
415              
416             # Throw
417 0           die "$context\n";
418             }
419              
420             1;
421             __END__