*: update redefs of `to_json`
[nit.git] / lib / json / string_parser.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Simple ad-hoc implementation of a JSON parser for String inputs
16 module string_parser
17
18 import parser_base
19 import static
20
21 redef class Char
22 # Is `self` a valid number start ?
23 private fun is_json_num_start: Bool do
24 if self == '-' then return true
25 if self.is_numeric then return true
26 return false
27 end
28
29 # Is `self` a valid JSON separator ?
30 private fun is_json_separator: Bool do
31 if self == ':' then return true
32 if self == ',' then return true
33 if self == '{' then return true
34 if self == '}' then return true
35 if self == '[' then return true
36 if self == ']' then return true
37 if self == '"' then return true
38 if self.is_whitespace then return true
39 return false
40 end
41 end
42
43 # A simple ad-hoc JSON parser
44 #
45 # To parse a simple JSON document, read it as a String and give it to `parse_entity`
46 # NOTE: if your document contains several non-nested entities, use `parse_entity` for each
47 # JSON entity to parse
48 class JSONStringParser
49 super StringProcessor
50
51 # Parses a JSON Entity
52 #
53 # ~~~nit
54 # var p = new JSONStringParser("""{"numbers": [1,23,3], "string": "string"}""")
55 # assert p.parse_entity isa JsonObject
56 # ~~~
57 fun parse_entity: nullable Jsonable do
58 var srclen = len
59 ignore_whitespaces
60 if pos >= srclen then return make_parse_error("Empty JSON")
61 var c = src[pos]
62 if c == '[' then
63 pos += 1
64 return parse_json_array
65 else if c == '"' then
66 var s = parse_json_string
67 return s
68 else if c == '{' then
69 pos += 1
70 return parse_json_object
71 else if c == 'f' then
72 if pos + 4 >= srclen then make_parse_error("Error: bad JSON entity")
73 if src[pos + 1] == 'a' and src[pos + 2] == 'l' and src[pos + 3] == 's' and src[pos + 4] == 'e' then
74 pos += 5
75 return false
76 end
77 return make_parse_error("Error: bad JSON entity")
78 else if c == 't' then
79 if pos + 3 >= srclen then make_parse_error("Error: bad JSON entity")
80 if src[pos + 1] == 'r' and src[pos + 2] == 'u' and src[pos + 3] == 'e' then
81 pos += 4
82 return true
83 end
84 return make_parse_error("Error: bad JSON entity")
85 else if c == 'n' then
86 if pos + 3 >= srclen then make_parse_error("Error: bad JSON entity")
87 if src[pos + 1] == 'u' and src[pos + 2] == 'l' and src[pos + 3] == 'l' then
88 pos += 4
89 return null
90 end
91 return make_parse_error("Error: bad JSON entity")
92 end
93 if not c.is_json_num_start then return make_parse_error("Bad JSON character")
94 return parse_json_number
95 end
96
97 # Parses a JSON Array
98 fun parse_json_array: Jsonable do
99 var max = len
100 if pos >= max then return make_parse_error("Incomplete JSON array")
101 var arr = new JsonArray
102 var c = src[pos]
103 while not c == ']' do
104 ignore_whitespaces
105 if pos >= max then return make_parse_error("Incomplete JSON array")
106 if src[pos] == ']' then break
107 var ent = parse_entity
108 #print "Parsed an entity {ent} for a JSON array"
109 if ent isa JsonParseError then return ent
110 arr.add ent
111 ignore_whitespaces
112 if pos >= max then return make_parse_error("Incomplete JSON array")
113 c = src[pos]
114 if c == ']' then break
115 if c != ',' then return make_parse_error("Bad array separator {c}")
116 pos += 1
117 end
118 pos += 1
119 return arr
120 end
121
122 # Parses a JSON Object
123 fun parse_json_object: Jsonable do
124 var max = len
125 if pos >= max then return make_parse_error("Incomplete JSON object")
126 var obj = new JsonObject
127 var c = src[pos]
128 while not c == '}' do
129 ignore_whitespaces
130 if pos >= max then return make_parse_error("Malformed JSON object")
131 if src[pos] == '}' then break
132 var key = parse_entity
133 #print "Parsed key {key} for JSON object"
134 if not key isa String then return make_parse_error("Bad key format {key or else "null"}")
135 ignore_whitespaces
136 if pos >= max then return make_parse_error("Incomplete JSON object")
137 if not src[pos] == ':' then return make_parse_error("Bad key/value separator {src[pos]}")
138 pos += 1
139 ignore_whitespaces
140 var value = parse_entity
141 #print "Parsed value {value} for JSON object"
142 if value isa JsonParseError then return value
143 obj[key] = value
144 ignore_whitespaces
145 if pos >= max then return make_parse_error("Incomplete JSON object")
146 c = src[pos]
147 if c == '}' then break
148 if c != ',' then return make_parse_error("Bad object separator {src[pos]}")
149 pos += 1
150 end
151 pos += 1
152 return obj
153 end
154
155 # Creates a `JsonParseError` with the right message and location
156 protected fun make_parse_error(message: String): JsonParseError do
157 var err = new JsonParseError(message)
158 err.location = hot_location
159 return err
160 end
161
162 # Parses an Int or Float
163 fun parse_json_number: Jsonable do
164 var max = len
165 var p = pos
166 var c = src[p]
167 var is_neg = false
168 if c == '-' then
169 is_neg = true
170 p += 1
171 if p >= max then return make_parse_error("Bad JSON number")
172 c = src[p]
173 end
174 var val = 0
175 while c.is_numeric do
176 val *= 10
177 val += c.to_i
178 p += 1
179 if p >= max then break
180 c = src[p]
181 end
182 if c == '.' then
183 p += 1
184 if p >= max then return make_parse_error("Bad JSON number")
185 c = src[p]
186 var fl = val.to_f
187 var frac = 0.1
188 while c.is_numeric do
189 fl += c.to_i.to_f * frac
190 frac /= 10.0
191 p += 1
192 if p >= max then break
193 c = src[p]
194 end
195 if c == 'e' or c == 'E' then
196 p += 1
197 var exp = 0
198 if p >= max then return make_parse_error("Malformed JSON number")
199 c = src[p]
200 while c.is_numeric do
201 exp *= 10
202 exp += c.to_i
203 p += 1
204 if p >= max then break
205 c = src[p]
206 end
207 fl *= (10 ** exp).to_f
208 end
209 if p < max and not c.is_json_separator then return make_parse_error("Malformed JSON number")
210 pos = p
211 if is_neg then return -fl
212 return fl
213 end
214 if c == 'e' or c == 'E' then
215 p += 1
216 if p >= max then return make_parse_error("Bad JSON number")
217 var exp = src[p].to_i
218 c = src[p]
219 while c.is_numeric do
220 exp *= 10
221 exp += c.to_i
222 p += 1
223 if p >= max then break
224 c = src[p]
225 end
226 val *= (10 ** exp)
227 end
228 if p < max and not src[p].is_json_separator then return make_parse_error("Malformed JSON number")
229 pos = p
230 if is_neg then return -val
231 return val
232 end
233
234 private var parse_str_buf = new FlatBuffer
235
236 # Parses and returns a Nit string from a JSON String
237 fun parse_json_string: Jsonable do
238 var src = src
239 var ln = src.length
240 var p = pos
241 p += 1
242 if p > ln then return make_parse_error("Malformed JSON String")
243 var c = src[p]
244 var ret = parse_str_buf
245 var chunk_st = p
246 while c != '"' do
247 if c != '\\' then
248 p += 1
249 if p >= ln then return make_parse_error("Malformed JSON string")
250 c = src[p]
251 continue
252 end
253 ret.append_substring_impl(src, chunk_st, p - chunk_st)
254 p += 1
255 if p >= ln then return make_parse_error("Malformed Escape sequence in JSON string")
256 c = src[p]
257 if c == 'r' then
258 ret.add '\r'
259 p += 1
260 else if c == 'n' then
261 ret.add '\n'
262 p += 1
263 else if c == 't' then
264 ret.add '\t'
265 p += 1
266 else if c == 'u' then
267 var cp = 0
268 p += 1
269 for i in [0 .. 4[ do
270 cp <<= 4
271 if p >= ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
272 c = src[p]
273 if c >= '0' and c <= '9' then
274 cp += c.code_point - '0'.code_point
275 else if c >= 'a' and c <= 'f' then
276 cp += c.code_point - 'a'.code_point + 10
277 else if c >= 'A' and c <= 'F' then
278 cp += c.code_point - 'A'.code_point + 10
279 else
280 make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
281 end
282 p += 1
283 end
284 c = cp.code_point
285 if cp >= 0xD800 and cp <= 0xDBFF then
286 if p >= ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
287 c = src[p]
288 if c != '\\' then make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
289 p += 1
290 c = src[p]
291 if c != 'u' then make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
292 var locp = 0
293 p += 1
294 for i in [0 .. 4[ do
295 locp <<= 4
296 if p > ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
297 c = src[p]
298 if c >= '0' and c <= '9' then
299 locp += c.code_point - '0'.code_point
300 else if c >= 'a' and c <= 'f' then
301 locp += c.code_point - 'a'.code_point + 10
302 else if c >= 'A' and c <= 'F' then
303 locp += c.code_point - 'A'.code_point + 10
304 else
305 make_parse_error("Malformed \uXXXX Escape sequence in JSON string")
306 end
307 p += 1
308 end
309 c = (((locp & 0x3FF) | ((cp & 0x3FF) << 10)) + 0x10000).code_point
310 end
311 ret.add c
312 else if c == 'b' then
313 ret.add 8.code_point
314 p += 1
315 else if c == 'f' then
316 ret.add '\f'
317 p += 1
318 else
319 p += 1
320 ret.add c
321 end
322 chunk_st = p
323 c = src[p]
324 end
325 pos = p + 1
326 if ret.is_empty then return src.substring(chunk_st, p - chunk_st)
327 ret.append_substring_impl(src, chunk_st, p - chunk_st)
328 var rets = ret.to_s
329 ret.clear
330 return rets
331 end
332
333 # Ignores any character until a JSON separator is encountered
334 fun ignore_until_separator do
335 var max = len
336 while pos < max do
337 if not src[pos].is_json_separator then return
338 end
339 end
340 end
341
342 redef class Text
343 redef fun parse_json do return (new JSONStringParser(self.to_s)).parse_entity
344 end
345
346 redef class JsonParseError
347 serialize
348
349 # Location of the error in source
350 var location: nullable Location = null
351 end