File Coverage

blib/lib/MojoX/AlmostJSON.pm
Criterion Covered Total %
statement 45 118 38.1
branch 13 80 16.2
condition 6 17 35.2
subroutine 15 25 60.0
pod 0 7 0.0
total 79 247 31.9


line stmt bran cond sub pod time code
1             package MojoX::AlmostJSON;
2             $MojoX::AlmostJSON::VERSION = '1.151250';
3             # ABSTRACT: Almost JSON but allow javascript function
4             # Code stolen from Mojo::JSON
5 3     3   29777 use Mojo::Base -strict;
  3         20644  
  3         22  
6            
7 3     3   728 use B;
  3         5  
  3         130  
8 3     3   11 use Carp 'croak';
  3         9  
  3         122  
9 3     3   11 use Exporter 'import';
  3         5  
  3         68  
10 3     3   1708 use Mojo::Util;
  3         106553  
  3         194  
11 3     3   24 use Scalar::Util 'blessed';
  3         6  
  3         5152  
12            
13             our @EXPORT_OK = qw(decode_json encode_json false from_json j to_json true);
14            
15             # Booleans
16             my ($FALSE, $TRUE) = map { bless \(my $dummy = $_), 'MojoX::AlmostJSON::_Bool' } 0, 1;
17            
18             # Escaped special character map (with u2028 and u2029)
19             my %ESCAPE = (
20             '"' => '"',
21             '\\' => '\\',
22             '/' => '/',
23             'b' => "\x08",
24             'f' => "\x0c",
25             'n' => "\x0a",
26             'r' => "\x0d",
27             't' => "\x09",
28             'u2028' => "\x{2028}",
29             'u2029' => "\x{2029}"
30             );
31             my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE;
32             for (0x00 .. 0x1f) { $REVERSE{pack 'C', $_} //= sprintf '\u%.4X', $_ }
33            
34             sub decode_json {
35 0     0 0 0 my $err = _decode(\my $value, shift);
36 0 0       0 return defined $err ? croak $err : $value;
37             }
38            
39 2     2 0 340 sub encode_json { Mojo::Util::encode 'UTF-8', _encode_value(shift) }
40            
41 1     1 0 2 sub false () {$FALSE}
42            
43             sub from_json {
44 0     0 0 0 my $err = _decode(\my $value, shift, 1);
45 0 0       0 return defined $err ? croak $err : $value;
46             }
47            
48             sub j {
49 0 0 0 0 0 0 return encode_json($_[0]) if ref $_[0] eq 'ARRAY' || ref $_[0] eq 'HASH';
50 0         0 return eval { decode_json($_[0]) };
  0         0  
51             }
52            
53 0     0 0 0 sub to_json { _encode_value(shift) }
54            
55 1     1 0 9 sub true () {$TRUE}
56            
57             sub _decode {
58 0     0   0 my $valueref = shift;
59            
60 0 0       0 eval {
61            
62             # Missing input
63 0 0       0 die "Missing or empty input\n" unless length(local $_ = shift);
64            
65             # UTF-8
66 0 0       0 $_ = Mojo::Util::decode 'UTF-8', $_ unless shift;
67 0 0       0 die "Input is not UTF-8 encoded\n" unless defined;
68            
69             # Value
70 0         0 $$valueref = _decode_value();
71            
72             # Leftover data
73 0 0       0 /\G[\x20\x09\x0a\x0d]*\z/gc or _throw('Unexpected data');
74             } ? return undef : chomp $@;
75            
76 0         0 return $@;
77             }
78            
79             sub _decode_array {
80 0     0   0 my @array;
81 0         0 until (m/\G[\x20\x09\x0a\x0d]*\]/gc) {
82            
83             # Value
84 0         0 push @array, _decode_value();
85            
86             # Separator
87 0 0       0 redo if /\G[\x20\x09\x0a\x0d]*,/gc;
88            
89             # End
90 0 0       0 last if /\G[\x20\x09\x0a\x0d]*\]/gc;
91            
92             # Invalid character
93 0         0 _throw('Expected comma or right square bracket while parsing array');
94             }
95            
96 0         0 return \@array;
97             }
98            
99             sub _decode_object {
100 0     0   0 my %hash;
101 0         0 until (m/\G[\x20\x09\x0a\x0d]*\}/gc) {
102            
103             # Quote
104 0 0       0 /\G[\x20\x09\x0a\x0d]*"/gc
105             or _throw('Expected string while parsing object');
106            
107             # Key
108 0         0 my $key = _decode_string();
109            
110             # Colon
111 0 0       0 /\G[\x20\x09\x0a\x0d]*:/gc
112             or _throw('Expected colon while parsing object');
113            
114             # Value
115 0         0 $hash{$key} = _decode_value();
116            
117             # Separator
118 0 0       0 redo if /\G[\x20\x09\x0a\x0d]*,/gc;
119            
120             # End
121 0 0       0 last if /\G[\x20\x09\x0a\x0d]*\}/gc;
122            
123             # Invalid character
124 0         0 _throw('Expected comma or right curly bracket while parsing object');
125             }
126            
127 0         0 return \%hash;
128             }
129            
130             sub _decode_string {
131 0     0   0 my $pos = pos;
132            
133             # Extract string with escaped characters
134 0         0 m!\G((?:(?:[^\x00-\x1f\\"]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc;
135 0         0 my $str = $1;
136            
137             # Invalid character
138 0 0       0 unless (m/\G"/gc) {
139 0 0       0 _throw('Unexpected character or invalid escape while parsing string')
140             if /\G[\x00-\x1f\\]/;
141 0         0 _throw('Unterminated string');
142             }
143            
144             # Unescape popular characters
145 0 0       0 if (index($str, '\\u') < 0) {
146 0         0 $str =~ s!\\(["\\/bfnrt])!$ESCAPE{$1}!gs;
147 0         0 return $str;
148             }
149            
150             # Unescape everything else
151 0         0 my $buffer = '';
152 0         0 while ($str =~ /\G([^\\]*)\\(?:([^u])|u(.{4}))/gc) {
153 0         0 $buffer .= $1;
154            
155             # Popular character
156 0 0       0 if ($2) { $buffer .= $ESCAPE{$2} }
  0         0  
157            
158             # Escaped
159             else {
160 0         0 my $ord = hex $3;
161            
162             # Surrogate pair
163 0 0       0 if (($ord & 0xf800) == 0xd800) {
164            
165             # High surrogate
166 0 0       0 ($ord & 0xfc00) == 0xd800
167             or pos($_) = $pos + pos($str), _throw('Missing high-surrogate');
168            
169             # Low surrogate
170 0 0       0 $str =~ /\G\\u([Dd][C-Fc-f]..)/gc
171             or pos($_) = $pos + pos($str), _throw('Missing low-surrogate');
172            
173 0         0 $ord = 0x10000 + ($ord - 0xd800) * 0x400 + (hex($1) - 0xdc00);
174             }
175            
176             # Character
177 0         0 $buffer .= pack 'U', $ord;
178             }
179             }
180            
181             # The rest
182 0         0 return $buffer . substr $str, pos($str), length($str);
183             }
184            
185             sub _decode_value {
186            
187             # Leading whitespace
188 0     0   0 /\G[\x20\x09\x0a\x0d]*/gc;
189            
190             # String
191 0 0       0 return _decode_string() if /\G"/gc;
192            
193             # Object
194 0 0       0 return _decode_object() if /\G\{/gc;
195            
196             # Array
197 0 0       0 return _decode_array() if /\G\[/gc;
198            
199             # Number
200 0 0       0 return 0 + $1
201             if /\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc;
202            
203             # True
204 0 0       0 return $TRUE if /\Gtrue/gc;
205            
206             # False
207 0 0       0 return $FALSE if /\Gfalse/gc;
208            
209             # Null
210 0 0       0 return undef if /\Gnull/gc;
211            
212             # Invalid character
213 0         0 _throw('Expected string, array, object, number, boolean or null');
214             }
215            
216             sub _encode_array {
217 3     3   3 '[' . join(',', map { _encode_value($_) } @{$_[0]}) . ']';
  9         17  
  3         5  
218             }
219            
220             sub _encode_object {
221 4     4   2 my $object = shift;
222 4         8 my @pairs = map { _encode_string($_) . ':' . _encode_value($object->{$_}) }
  4         5  
223             keys %$object;
224 4         13 return '{' . join(',', @pairs) . '}';
225             }
226            
227             sub _encode_string {
228 5     5   3 my $str = shift;
229 5         7 $str =~ s!([\x00-\x1f\x{2028}\x{2029}\\"/])!$REVERSE{$1}!gs;
230 5         12 return "\"$str\"";
231             }
232            
233             sub _encode_value {
234 15     15   14 my $value = shift;
235            
236             # Reference
237 15 100       23 if (my $ref = ref $value) {
238            
239             # Object
240 10 100       21 return _encode_object($value) if $ref eq 'HASH';
241            
242             # Array
243 6 100       16 return _encode_array($value) if $ref eq 'ARRAY';
244              
245             #Function
246 3 100 66     13 return $$value if $ref eq 'SCALAR' and $$value !~/^[01]$/;
247              
248             # True or false
249 2 100       34 return $value ? 'true' : 'false' if $ref =~ /^(?:MojoX::Almost|Mojo::)JSON::_Bool$/;
    50          
250 0 0       0 return $$value ? 'true' : 'false' if $ref eq 'SCALAR';
    0          
251            
252             # Blessed reference with TO_JSON method
253 0 0 0     0 if (blessed $value && (my $sub = $value->can('TO_JSON'))) {
254 0         0 return _encode_value($value->$sub);
255             }
256             }
257            
258             # Null
259 5 50       7 return 'null' unless defined $value;
260            
261             # Number
262 5 50 66     51 return $value
      66        
263             if B::svref_2object(\$value)->FLAGS & (B::SVp_IOK | B::SVp_NOK)
264             && 0 + $value eq $value
265             && $value * 0 == 0;
266            
267             # String
268 1         11 return _encode_string($value);
269             }
270            
271             sub _throw {
272            
273             # Leading whitespace
274 0     0     /\G[\x20\x09\x0a\x0d]*/gc;
275            
276             # Context
277 0           my $context = 'Malformed JSON: ' . shift;
278 0 0         if (m/\G\z/gc) { $context .= ' before end of data' }
  0            
279             else {
280 0           my @lines = split "\n", substr($_, 0, pos);
281 0   0       $context .= ' at line ' . @lines . ', offset ' . length(pop @lines || '');
282             }
283            
284 0           die "$context\n";
285             }
286            
287             # Emulate boolean type
288             package MojoX::AlmostJSON::_Bool;
289             $MojoX::AlmostJSON::_Bool::VERSION = '1.151250';
290 3     3   13 use overload '""' => sub { ${$_[0]} }, fallback => 1;
  3     4   4  
  3         30  
  4         3  
  4         17  
291            
292             1;
293              
294             __END__