File Coverage

blib/lib/ThaiSchema/JSON.pm
Criterion Covered Total %
statement 146 181 80.6
branch 71 104 68.2
condition 7 17 41.1
subroutine 16 20 80.0
pod 0 6 0.0
total 240 328 73.1


line stmt bran cond sub pod time code
1             package ThaiSchema::JSON;
2 2     2   101837 use strict;
  2         5  
  2         86  
3 2     2   11 use warnings;
  2         4  
  2         71  
4 2     2   11 use utf8;
  2         4  
  2         479  
5              
6 2     2   1251 use ThaiSchema;
  2         5  
  2         201  
7 2     2   3404 use Encode ();
  2         74408  
  2         7241  
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 40     40 0 16153 my $class = shift;
55 40 0       385 bless @_ ? @_ > 1 ? {@_} : { %{ $_[0] } } : {}, $class;
  0 50       0  
56             }
57              
58             sub error {
59 94 100   94 0 431 $_[0]->{error} = $_[1] if @_ > 1;
60 94         255 return $_[0]->{error};
61             }
62              
63             sub validate {
64 39     39 0 211 my ( $self, $bytes, $schema ) = @_;
65 39         106 $schema = _schema($schema);
66              
67 39         72 local $FAIL;
68 39         98 local @_ERRORS;
69 39         76 local $_NAME = '';
70              
71             # Cleanup
72 39         123 $self->error(undef);
73              
74             # Missing input
75 39 50 0     117 $self->error('Missing or empty input') and return undef
76             unless $bytes; ## no critic (undef)
77              
78             # Remove BOM
79 39         129 $bytes =~
80             s/^(?:\357\273\277|\377\376\0\0|\0\0\376\377|\376\377|\377\376)//g;
81              
82             # Wide characters
83 39 50 0     170 $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 39         61 my $encoding = 'UTF-8';
89 39   33     775 $bytes =~ $UTF_PATTERNS->{$_} and $encoding = $_ for keys %$UTF_PATTERNS;
90              
91 39         170 my $d_res = eval { $bytes = Encode::decode( $encoding, $bytes, 1 ); 1 };
  39         178  
  39         3508  
92 39 50       126 $bytes = undef unless $d_res;
93              
94             # Object or array
95 39         60 my $res = eval {
96 39         73 local $_ = $bytes;
97              
98             # Leading whitespace
99 39         300 m/\G$WHITESPACE_RE/gc;
100              
101             # Array
102 39         62 my $ref;
103 39 100       191 if (m/\G\[/gc) {
    50          
104 20 100       83 unless ($schema->is_array()) {
105 1         5 _exception2("Unexpected array found.");
106             }
107 19         63 $ref = _decode_array($schema->schema)
108             }
109              
110             # Object
111             elsif (m/\G\{/gc) {
112 19 100       240 unless ($schema->is_hash()) {
113 1         6 _exception2("Unexpected object found.");
114             }
115 18         61 $ref = _decode_object($schema)
116             }
117              
118             # Unexpected
119 0         0 else { _exception('Expected array or object') }
120              
121             # Leftover data
122 31 50       216 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 31         105 $ref;
128             };
129              
130             # Exception
131 39 100 66     258 if ( !$res && ( my $e = $@ ) ) {
132 8         21 chomp $e;
133 8         23 $self->error($e);
134             }
135              
136 39 100       102 if ($self->error) {
137 8         25 push @_ERRORS, $self->error;
138 8         15 $FAIL++;
139             }
140              
141             # return ($ok, \@errors);
142 39         1323 return (!$FAIL, \@_ERRORS);
143             }
144              
145             sub _fail {
146 16     16   34 my ($got, $schema) = @_;
147 16 50       97 _fail2(($_NAME ? "$_NAME: " : '') . $schema->name . " is expected, but $got is found");
148             }
149              
150             sub _fail2 {
151 17     17   30 my ($msg) = @_;
152 17         27 $FAIL++;
153 17         336 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   58 my $schema = _schema(shift);
161              
162 26         40 my @array;
163 26         38 my $i = 0;
164 26         217 until (m/\G$WHITESPACE_RE\]/gc) {
165 24         656 local $_NAME = $_NAME . "[$i]";
166              
167             # Value
168 24         88 push @array, _decode_value($schema);
169              
170 20         32 $i++;
171              
172             # Separator
173 20 50       121 redo if m/\G$WHITESPACE_RE,/gc;
174              
175             # End
176 20 50       286 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         77 return \@array;
184             }
185              
186             sub _decode_object {
187 22     22   55 my $schema = _schema(shift);
188              
189 22         44 my %hash;
190 22         30 my %schema = %{$schema->schema};
  22         79  
191 22         336 until (m/\G$WHITESPACE_RE\}/gc) {
192              
193             # Quote
194 25 50       191 m/\G$WHITESPACE_RE"/gc
195             or _exception('Expected string while parsing object');
196              
197             # Key
198 25         1220 my $key = _decode_string();
199              
200             # Colon
201 25 50       168 m/\G$WHITESPACE_RE:/gc
202             or _exception('Expected colon while parsing object');
203              
204             # Value
205 25         67 local $_NAME = $_NAME . ".$key";
206 25         73 my $cschema = delete $schema{$key};
207 25 100       94 if ($cschema) {
208 19         53 $hash{$key} = _decode_value($cschema);
209             } else {
210 6 100       15 if ($ThaiSchema::ALLOW_EXTRA) {
211 5         31 $hash{$key} = _decode_value(ThaiSchema::Extra->new());
212             } else {
213 1         6 _exception2("There is extra key: $key");
214             }
215             }
216              
217             # Separator
218 23 100       174 redo if m/\G$WHITESPACE_RE,/gc;
219              
220             # End
221 18 50       162 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 20 100       53 if (%schema) {
229 1         9 _fail2('There is missing keys: ' . join(', ', keys %schema));
230             }
231              
232 20         317 return \%hash;
233             }
234              
235             sub _decode_string {
236 31     31   112 my $pos = pos;
237              
238             # Extract string with escaped characters
239 31         162 m#\G(((?:[^\x00-\x1F\\"]|\\(?:["\\/bfnrt]|u[[:xdigit:]]{4})){0,32766})*)#gc;
240 31         90 my $str = $1;
241              
242             # Missing quote
243 31 50       118 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 31 50       144 if ( index( $str, '\\u' ) < 0 ) {
252 31         51 $str =~ s!\\(["\\/bfnrt])!$ESCAPE{$1}!gs;
253 31         96 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 137     137   226 my $schema = shift;
297 137 100       786 if (ref $schema eq 'HASH') {
    100          
298 7         34 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         10 return ThaiSchema::Array->new(schema => _schema($schema->[0]));
304             } else {
305 128         276 return $schema;
306             }
307             }
308              
309             sub _decode_value {
310 48     48   108 my $schema = _schema(shift);
311              
312             # Leading whitespace
313 48         313 m/\G$WHITESPACE_RE/gc;
314              
315             # String
316 48 100       311 if (m/\G"/gc) {
317 6 100       41 unless ($schema->is_string) {
318 4         13 _fail('string', $schema);
319             }
320 6         7349 return _decode_string();
321             }
322              
323             # Array
324 42 100       152 if (m/\G\[/gc) {
325 9 100       62 unless ($schema->is_array) {
326 2         8 _fail('array', $schema);
327 2         8 _exception2("Unexpected array.");
328             }
329 7         28 return _decode_array($schema->schema);
330             }
331              
332             # Object
333 33 100       97 if (m/\G\{/gc) {
334 7 100       38 unless ($schema->is_hash) {
335 3         10 _fail('object', $schema);
336 3         13 _exception2("Unexpected hash.");
337             }
338 4         18 return _decode_object($schema);
339             }
340              
341             # Number
342 26 100       145 if (m/\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc) {
343 15         55 my $number = 0+$1;
344 15 100       68 unless ($schema->is_number) {
345 2         8 _fail('number', $schema);
346             }
347 15 100 100     61 if ($schema->is_integer && int($number) != $number) {
348 1         14 push @_ERRORS, "integer is expected, but you got $number";
349 1         3 $FAIL++;
350             }
351 15         60 return $number;
352             }
353              
354             # True
355 11 100       53 if (m/\Gtrue/gc) {
356 6 100       33 unless ($schema->is_bool) {
357 2         8 _fail('true', $schema);
358             }
359 6         24 return $TRUE;
360             }
361              
362             # False
363 5 100       25 if (m/\Gfalse/gc) {
364 2 100       74 unless ($schema->is_bool) {
365 1         5 _fail('false', $schema);
366             }
367 2         8 return $FALSE;
368             }
369              
370             # Null
371 3 50       23 if (m/\Gnull/gc) {
372 3 100       24 unless ($schema->is_null) {
373 2         6 _fail('null', $schema);
374             }
375             ## no critic (return)
376 3         11 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   148 m/\G$WHITESPACE_RE/gc;
386              
387             # Context
388 8         12 my $context;
389 8 100       31 $context .= "$_NAME: " if $_NAME;
390 8         18 $context .= shift;
391 8 50       24 if (m/\G\z/gc) { $context .= ' before end of data' }
  0         0  
392             else {
393 8         57 my @lines = split /\n/, substr( $_, 0, pos );
394 8   50     51 $context .=
395             ' at line ' . @lines . ', offset ' . length( pop @lines || '' );
396             }
397              
398             # Throw
399 8         78 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__