File Coverage

lib/mb/JSON.pm
Criterion Covered Total %
statement 129 135 95.5
branch 62 68 91.1
condition 3 6 50.0
subroutine 26 26 100.0
pod 6 6 100.0
total 226 241 93.7


line stmt bran cond sub pod time code
1             package mb::JSON;
2             ######################################################################
3             #
4             # mb::JSON - JSON encode/decode for multibyte (UTF-8) strings
5             #
6             # https://metacpan.org/dist/mb-JSON
7             #
8             # Copyright (c) 2021, 2022, 2026 INABA Hitoshi
9             ######################################################################
10             #
11             # Compatible: Perl 5.005_03 and later
12             # Platform: Windows and UNIX/Linux
13             #
14             ######################################################################
15              
16 5     5   186784 use 5.00503;
  5         19  
17 5     5   31 use strict;
  5         8  
  5         495  
18 5 50 33 5   156 BEGIN { if ($] < 5.006 && !defined(&warnings::import)) {
19 0         0 $INC{'warnings.pm'} = 'stub';
20 0         0 eval 'package warnings; sub import {}' } }
21 5     5   27 use warnings; local $^W = 1;
  5         10  
  5         421  
22 5 50   5   177 BEGIN { pop @INC if $INC[-1] eq '.' }
23 5     5   32 use vars qw($VERSION);
  5         14  
  5         383  
24             $VERSION = '0.06';
25             $VERSION = $VERSION;
26              
27 5     5   30 use Carp qw(croak);
  5         7  
  5         418  
28              
29             ######################################################################
30             # Boolean type objects
31             ######################################################################
32              
33             package mb::JSON::Boolean;
34 5     5   44 use vars qw($VERSION);
  5         9  
  5         690  
35             $VERSION = '0.06';
36             $VERSION = $VERSION;
37              
38             use overload
39 14     14   50 '0+' => sub { ${ $_[0] } },
  14         46  
40 4 100   4   6 '""' => sub { ${ $_[0] } ? 'true' : 'false' },
  4         33  
41 4     4   92 'bool' => sub { ${ $_[0] } },
  4         10  
42 5     5   2742 fallback => 1;
  5         7785  
  5         73  
43              
44             package mb::JSON;
45              
46 5     5   622 use vars qw($true $false);
  5         13  
  5         12505  
47             {
48             my $_t = 1; $true = bless \$_t, 'mb::JSON::Boolean';
49             my $_f = 0; $false = bless \$_f, 'mb::JSON::Boolean';
50             }
51              
52 19     19 1 191287 sub true { $true }
53 15     15 1 48 sub false { $false }
54              
55             ######################################################################
56             # UTF-8 multibyte pattern
57             ######################################################################
58              
59             my $utf8_pat = join '|', (
60             '[\x00-\x7F\x80-\xBF\xC0-\xC1\xF5-\xFF]',
61             '[\xC2-\xDF][\x80-\xBF]',
62             '[\xE0][\xA0-\xBF][\x80-\xBF]',
63             '[\xE1-\xEC][\x80-\xBF][\x80-\xBF]',
64             '[\xED][\x80-\x9F][\x80-\xBF]',
65             '[\xEE-\xEF][\x80-\xBF][\x80-\xBF]',
66             '[\xF0][\x90-\xBF][\x80-\xBF][\x80-\xBF]',
67             '[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]',
68             '[\xF4][\x80-\x8F][\x80-\xBF][\x80-\xBF]',
69             '[\x00-\xFF]',
70             );
71              
72             ######################################################################
73             # decode -- JSON text -> Perl data
74             ######################################################################
75              
76             sub decode {
77 42 100   42 1 125663 my $json = defined $_[0] ? $_[0] : $_;
78 42         115 my $r = \$json;
79 42         85 my $val = _parse_value($r);
80 35         55 $$r =~ s/\A\s+//s;
81 35 100       189 croak "mb::JSON::decode: trailing garbage: " . substr($$r, 0, 20)
82             if length $$r;
83 34         100 return $val;
84             }
85              
86             sub parse { # alias for decode()
87 5 100   5 1 58 my $json = defined $_[0] ? $_[0] : $_;
88 5         13 return decode($json);
89             }
90              
91             sub _parse_value {
92 87     87   131 my ($r) = @_;
93 87         169 $$r =~ s/\A\s+//s;
94 87 100       327 croak "mb::JSON::decode: unexpected end of input" unless length $$r;
95              
96 86         136 my $c = substr($$r, 0, 1);
97              
98 86 100       367 if ($c eq '{') { return _parse_object($r) }
  17 100       33  
    100          
    100          
    100          
    100          
    50          
99 8         26 elsif ($c eq '[') { return _parse_array($r) }
100 21         82 elsif ($c eq '"') { return _parse_string($r) }
101 5         31 elsif ($$r =~ s/\Anull(?=[^a-zA-Z0-9_]|$)//s) { return undef }
102 9         22 elsif ($$r =~ s/\Atrue(?=[^a-zA-Z0-9_]|$)//s) { return $true }
103 7         31 elsif ($$r =~ s/\Afalse(?=[^a-zA-Z0-9_]|$)//s) { return $false }
104             elsif ($$r =~ s/\A(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)//s) {
105 19         64 return $1 + 0;
106             }
107             else {
108 0         0 croak "mb::JSON::decode: unexpected token: " . substr($$r, 0, 20);
109             }
110             }
111              
112             sub _parse_object {
113 17     17   44 my ($r) = @_;
114 17         55 $$r =~ s/\A\{//s;
115 17         28 my %obj;
116 17         46 $$r =~ s/\A\s+//s;
117 17 100       47 if ($$r =~ s/\A\}//s) { return { %obj } }
  1         5  
118 16         45 while (1) {
119 27         46 $$r =~ s/\A\s+//s;
120 27 100       424 croak "mb::JSON::decode: expected string key in object"
121             unless $$r =~ /\A"/;
122 25         41 my $key = _parse_string($r);
123 25         51 $$r =~ s/\A\s+//s;
124 25 100       184 $$r =~ s/\A://s
125             or croak "mb::JSON::decode: expected ':' after key '$key'";
126 24         48 my $val = _parse_value($r);
127 24         79 $obj{$key} = $val;
128 24         55 $$r =~ s/\A\s+//s;
129 24 100       76 if ($$r =~ s/\A,//s) { next }
  11 100       18  
130 12         19 elsif ($$r =~ s/\A\}//s) { last }
131 1         115 else { croak "mb::JSON::decode: expected ',' or '}' in object" }
132             }
133 12         67 return { %obj };
134             }
135              
136             sub _parse_array {
137 8     8   13 my ($r) = @_;
138 8         25 $$r =~ s/\A\[//s;
139 8         12 my @arr;
140 8         16 $$r =~ s/\A\s+//s;
141 8 100       23 if ($$r =~ s/\A\]//s) { return [ @arr ] }
  1         3  
142 7         14 while (1) {
143 21         33 push @arr, _parse_value($r);
144 21         36 $$r =~ s/\A\s+//s;
145 21 100       81 if ($$r =~ s/\A,//s) { next }
  14 100       21  
146 6         13 elsif ($$r =~ s/\A\]//s) { last }
147 1         128 else { croak "mb::JSON::decode: expected ',' or ']' in array" }
148             }
149 6         24 return [ @arr ];
150             }
151              
152             my %UNESC = (
153             '"' => '"', '\\' => '\\', '/' => '/',
154             'b' => "\x08", 'f' => "\x0C",
155             'n' => "\n", 'r' => "\r", 't' => "\t",
156             );
157              
158             sub _parse_string {
159 46     46   72 my ($r) = @_;
160 46         127 $$r =~ s/\A"//s;
161 46         63 my $s = '';
162 46         68 while (1) {
163 213 100       1506 if ($$r =~ s/\A"//s) { last }
  45 100       60  
    100          
    100          
164 2         6 elsif ($$r =~ s/\A\\(["\\\/bfnrt])//s) { $s .= $UNESC{$1} }
165             elsif ($$r =~ s/\A\\u([0-9a-fA-F]{4})//s) {
166 1         6 $s .= _cp_to_utf8(hex($1));
167             }
168 164         330 elsif ($$r =~ s/\A($utf8_pat)//s) { $s .= $1 }
169 1         137 else { croak "mb::JSON::decode: unterminated string" }
170             }
171 45         99 return $s;
172             }
173              
174             sub _cp_to_utf8 {
175 1     1   2 my ($cp) = @_;
176 1 50       6 return chr($cp) if $cp <= 0x7F;
177 0 0       0 if ($cp <= 0x7FF) {
178 0         0 return chr(0xC0|($cp>>6)) . chr(0x80|($cp&0x3F));
179             }
180 0         0 return chr(0xE0|($cp>>12))
181             . chr(0x80|(($cp>>6)&0x3F))
182             . chr(0x80|($cp&0x3F));
183             }
184              
185             ######################################################################
186             # decode -- JSON text -> Perl data
187             # parse -- alias for decode()
188             ######################################################################
189              
190             ######################################################################
191             # encode -- Perl data -> JSON text
192             #
193             # Encoding rules:
194             # undef -> null
195             # mb::JSON::true -> true
196             # mb::JSON::false -> false
197             # number-like scalar -> number (no quotes)
198             # other scalar -> "string" (UTF-8 kept as-is)
199             # ARRAY ref -> [...]
200             # HASH ref -> {...} (keys sorted alphabetically)
201             ######################################################################
202              
203             sub encode {
204 83     83 1 189521 my ($data) = @_;
205 83         141 return _enc_value($data);
206             }
207              
208             sub stringify { # alias for encode()
209 39     39 1 194943 my ($data) = @_;
210 39         74 return encode($data);
211             }
212              
213             sub _enc_value {
214 179     179   251 my ($v) = @_;
215 179 100       364 return 'null' unless defined $v;
216 167 100       331 if (ref $v eq 'mb::JSON::Boolean') { return $$v ? 'true' : 'false' }
  30 100       218  
217 137 100       255 if (ref $v eq 'ARRAY') { return '[' . join(',', map { _enc_value($_) } @$v) . ']' }
  22         62  
  60         96  
218 115 100       202 if (ref $v eq 'HASH') {
219 20         61 my @pairs = map { _enc_string($_) . ':' . _enc_value($v->{$_}) }
  36         58  
220             sort keys %$v;
221 20         179 return '{' . join(',', @pairs) . '}';
222             }
223             # number: matches JSON number pattern exactly
224 95 100 66     518 if (!ref $v && $v =~ /\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/s) {
225 55         228 return $v;
226             }
227 40         71 return _enc_string($v);
228             }
229              
230             sub _enc_string {
231 76     76   116 my ($s) = @_;
232 76         121 $s =~ s/\\/\\\\/g;
233 76         102 $s =~ s/"/\\"/g;
234 76         107 $s =~ s/\x08/\\b/g;
235 76         103 $s =~ s/\x0C/\\f/g;
236 76         101 $s =~ s/\n/\\n/g;
237 76         100 $s =~ s/\r/\\r/g;
238 76         97 $s =~ s/\t/\\t/g;
239 76         142 $s =~ s/([\x00-\x1F])/sprintf('\\u%04X', ord($1))/ge;
  2         19  
240 76         236 return '"' . $s . '"';
241             }
242              
243             1;
244              
245             =head1 NAME
246              
247             mb::JSON - JSON encode/decode for multibyte (UTF-8) strings
248              
249             =head1 VERSION
250              
251             Version 0.06
252              
253             =head1 SYNOPSIS
254              
255             use mb::JSON;
256              
257             # decode: JSON text -> Perl data
258             my $data = mb::JSON::decode("{\"name\":\"\\u7530\\u4e2d\",\"age\":30}");
259             my $data = mb::JSON::decode("{\"name\":\"Tanaka\",\"age\":30}");
260              
261             # parse: alias for decode()
262             my $data = mb::JSON::parse("{\"key\":\"value\"}");
263              
264             # encode: Perl data -> JSON text
265             my $json = mb::JSON::encode({ name => 'Tanaka', age => 30 });
266             # -> '{"age":30,"name":"Tanaka"}'
267              
268             # stringify: alias for encode()
269             my $json = mb::JSON::stringify({ name => 'Tanaka', age => 30 });
270             # -> '{"age":30,"name":"Tanaka"}'
271              
272             # Boolean values
273             my $json = mb::JSON::encode({
274             active => mb::JSON::true,
275             locked => mb::JSON::false,
276             });
277             # -> '{"active":true,"locked":false}'
278              
279             # null
280             my $json = mb::JSON::encode({ value => undef });
281             # -> '{"value":null}'
282              
283             =head1 TABLE OF CONTENTS
284              
285             =over 4
286              
287             =item * L
288              
289             =item * L
290              
291             =item * L
292              
293             =item * L
294              
295             =item * L
296              
297             =item * L
298              
299             =item * L
300              
301             =item * L
302              
303             =item * L
304              
305             =back
306              
307             =head1 DESCRIPTION
308              
309             C is a simple, dependency-free JSON encoder and decoder
310             designed for Perl 5.005_03 and later. It handles UTF-8 multibyte
311             strings correctly, making it suitable for environments where standard
312             JSON modules requiring Perl 5.8+ are unavailable.
313              
314             C provides two pairs of symmetric functions:
315             C/C convert JSON text to Perl data, and
316             C/C convert Perl data to JSON text.
317             Within each pair, both names are aliases and produce identical output.
318             Version 0.06 adds C as an alias for C,
319             mirror of C which is an alias for C.
320              
321             =head1 FUNCTIONS
322              
323             =head2 decode( $json_text )
324              
325             Converts a JSON text string to a Perl data structure.
326             If no argument is given, C<$_> is used.
327              
328             my $data = mb::JSON::decode($json_text);
329              
330             =head2 parse( $json_text )
331              
332             Alias for C. Both names are interchangeable.
333              
334             my $data = mb::JSON::parse($json_text);
335              
336             =head2 encode( $data )
337              
338             Converts a Perl data structure to a JSON text string.
339             Returns a byte string encoded in UTF-8.
340              
341             my $json = mb::JSON::encode($data);
342              
343             =head2 stringify( $data )
344              
345             Alias for C. Both names are interchangeable.
346             Mirrors the C function in JavaScript.
347              
348             my $json = mb::JSON::stringify($data);
349              
350             =head2 true
351              
352             Returns the singleton C object representing JSON
353             C. Numifies to C<1>, stringifies to C<"true">.
354              
355             =head2 false
356              
357             Returns the singleton C object representing JSON
358             C. Numifies to C<0>, stringifies to C<"false">.
359              
360             =head1 BOOLEAN VALUES
361              
362             Perl has no native boolean type. To represent JSON C and
363             C unambiguously, C provides two singleton objects:
364              
365             mb::JSON::true -- stringifies as "true", numifies as 1
366             mb::JSON::false -- stringifies as "false", numifies as 0
367              
368             Use these when encoding a boolean value:
369              
370             my $json = mb::JSON::encode({ flag => mb::JSON::true });
371             # -> '{"flag":true}'
372              
373             A plain C<1> or C<0> encodes as a JSON number, not a boolean:
374              
375             my $json = mb::JSON::encode({ count => 1 });
376             # -> '{"count":1}'
377              
378             When decoding, JSON C and C are returned as
379             C objects, which behave as C<1> and C<0>
380             in numeric and boolean context.
381              
382             C behaves identically to C for boolean values.
383              
384             =head1 ENCODING RULES
385              
386             Applies to both C and C.
387              
388             =over 4
389              
390             =item undef -> null
391              
392             =item mb::JSON::true -> true, mb::JSON::false -> false
393              
394             =item Number
395              
396             A scalar matching the JSON number pattern is encoded as a bare number.
397              
398             =item String
399              
400             Encoded as a double-quoted JSON string. UTF-8 multibyte bytes are
401             output as-is (not C<\uXXXX>-escaped). Control characters U+0000-U+001F
402             are escaped.
403              
404             =item ARRAY reference -> JSON array C<[...]>
405              
406             =item HASH reference -> JSON object C<{...}>
407              
408             Hash keys are sorted alphabetically for deterministic output.
409              
410             =back
411              
412             =head1 DECODING RULES
413              
414             Applies to both C and C.
415              
416             =over 4
417              
418             =item null -> undef
419              
420             =item true -> mb::JSON::Boolean (numifies to 1)
421              
422             =item false -> mb::JSON::Boolean (numifies to 0)
423              
424             =item Number -> Perl number
425              
426             =item String -> Perl string (\uXXXX converted to UTF-8)
427              
428             =item Object -> hash reference
429              
430             =item Array -> array reference
431              
432             =back
433              
434             =head1 LIMITATIONS
435              
436             =over 4
437              
438             =item *
439              
440             Surrogate pairs (C<\uD800>-C<\uDFFF>) in C<\uXXXX> sequences are not
441             supported.
442              
443             =item *
444              
445             Circular references in C and C are not detected and will cause
446             infinite recursion.
447              
448             =item *
449              
450             References other than ARRAY and HASH (e.g. code references, blessed
451             objects other than C) are stringified rather than
452             raising an error.
453              
454             =item *
455              
456             A scalar that matches the JSON number pattern (e.g. C<"1.0">, C<"007">)
457             is encoded as a number if it looks like one, and as a string otherwise.
458             Leading-zero strings such as C<"007"> are preserved as strings because
459             they do not match the JSON number pattern.
460              
461             =back
462              
463             =head1 DIAGNOSTICS
464              
465             =over 4
466              
467             =item C
468              
469             The JSON text ended before a complete value was parsed.
470              
471             =item CtextE>
472              
473             An unrecognised token was encountered while parsing.
474              
475             =item C
476              
477             An object key was not a quoted string.
478              
479             =item CkeyE'>
480              
481             The colon separator was missing after an object key.
482              
483             =item C
484              
485             A JSON object was not properly terminated or separated.
486              
487             =item C
488              
489             A JSON array was not properly terminated or separated.
490              
491             =item C
492              
493             A JSON string was not closed with a double-quote.
494              
495             =item CtextE>
496              
497             Extra text was found after a successfully parsed top-level value.
498              
499             =back
500              
501             =head1 BUGS AND LIMITATIONS
502              
503             Please report bugs to C.
504              
505             =head1 SEE ALSO
506              
507             L, L
508              
509             =head1 AUTHOR
510              
511             INABA Hitoshi Eina@cpan.orgE
512              
513             =head1 COPYRIGHT AND LICENSE
514              
515             Copyright (c) 2021, 2022, 2026 INABA Hitoshi Eina@cpan.orgE
516              
517             This software is free software; you can redistribute it and/or modify
518             it under the same terms as Perl itself.
519             See L.
520              
521             =cut