lib/json: Added ad-hoc string parser for 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 # Parses and returns a Nit string from a JSON String
235 fun parse_json_string: Jsonable do
236 var ln = src.length
237 var p = pos
238 p += 1
239 if p > ln then return make_parse_error("Malformed JSON String")
240 var c = src[p]
241 var st = p
242 while c != '"' do
243 if c == '\\' then
244 if p + 1 >= ln then return make_parse_error("Malformed Escape sequence in JSON string")
245 p += 1
246 c = src[p]
247 if c == 'u' then
248 p += 1
249 if p + 3 >= ln then return make_parse_error("Bad Unicode escape sequence in string")
250 for i in [0 .. 4[ do if not src[p + i].is_hexdigit then return make_parse_error("Bad Unicode escape sequence in string")
251 p += 3
252 end
253 end
254 p += 1
255 if p >= ln then return make_parse_error("Malformed JSON String")
256 c = src[p]
257 end
258 pos = p + 1
259 return src.substring(st, p - st).unescape_json
260 end
261
262 # Ignores any character until a JSON separator is encountered
263 fun ignore_until_separator do
264 var max = len
265 while pos < max do
266 if not src[pos].is_json_separator then return
267 end
268 end
269 end
270
271 redef class Text
272 redef fun parse_json do return (new JSONStringParser(self.to_s)).parse_entity
273 end
274
275 redef class JsonParseError
276
277 # Location of the error in source
278 var location: nullable Location = null
279
280 # Get the JSON representation of `self`.
281 #
282 # ~~~
283 # var err = new JsonParseError("foo", new Position(1, 2, 3, 4, 5, 6))
284 # assert err.to_json == "Parsing error: foo"
285 # ~~~
286 redef fun to_json do
287 var l = location
288 var m = message
289 return if l == null then "Parsing error: {m}" else "Parsing error at {l}: {m}"
290 end
291
292 redef fun to_s do return to_json
293 end