Merge: serialization: safe formal types, fix warnings in generated code and more
[nit.git] / lib / json / serialization_read.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 # Services to read JSON: `from_json_string` and `JsonDeserializer`
16 module serialization_read
17
18 import ::serialization::caching
19 private import ::serialization::engine_tools
20 private import static
21 private import string_parser
22 import poset
23
24 # Deserializer from a Json string.
25 class JsonDeserializer
26 super CachingDeserializer
27
28 # Json text to deserialize from.
29 private var text: Text
30
31 # Accepted parameterized classes to deserialize
32 #
33 # If `whitelist.empty`, all types are accepted.
34 #
35 # ~~~nitish
36 # import json::serialization
37 #
38 # class MyClass
39 # serialize
40 # end
41 #
42 # var json_string = """
43 # {"__class" = "MyClass"}
44 # """
45 #
46 # var deserializer = new JsonDeserializer(json_string)
47 # var obj = deserializer.deserialize
48 # assert deserializer.errors.is_empty
49 # assert obj isa MyClass
50 #
51 # deserializer = new JsonDeserializer(json_string)
52 # deserializer.whitelist.add "Array[String]"
53 # deserializer.whitelist.add "AnotherAcceptedClass"
54 # obj = deserializer.deserialize
55 # assert deserializer.errors.length == 1
56 # assert obj == null
57 # ~~~
58 var whitelist = new Array[Text]
59
60 # Should objects be checked if they a subtype of the static type before deserialization?
61 #
62 # Defaults to `true`, as it should always be activated.
63 # It can be turned off to implement the subtype check itself.
64 var check_subtypes = true is writable
65
66 # Root json object parsed from input text.
67 private var root: nullable Object is noinit
68
69 # Depth-first path in the serialized object tree.
70 private var path = new Array[Map[String, nullable Object]]
71
72 # Names of the attributes from the root to the object currently being deserialized
73 var attributes_path = new Array[String]
74
75 # Last encountered object reference id.
76 #
77 # See `id_to_object`.
78 var just_opened_id: nullable Int = null
79
80 init do
81 var root = text.parse_json
82 if root isa Map[String, nullable Object] then path.add(root)
83 self.root = root
84 end
85
86 redef fun deserialize_attribute(name, static_type)
87 do
88 if path.is_empty then
89 # The was a parsing error or the root is not an object
90 if not root isa Error then
91 errors.add new Error("Deserialization Error: parsed JSON value is not an object.")
92 end
93 deserialize_attribute_missing = false
94 return null
95 end
96
97 var current = path.last
98
99 if not current.keys.has(name) then
100 # Let the generated code / caller of `deserialize_attribute` raise the missing attribute error
101 deserialize_attribute_missing = true
102 return null
103 end
104
105 var value = current[name]
106
107 attributes_path.add name
108 var res = convert_object(value, static_type)
109 attributes_path.pop
110
111 deserialize_attribute_missing = false
112 return res
113 end
114
115 # This may be called multiple times by the same object from constructors
116 # in different nclassdef
117 redef fun notify_of_creation(new_object)
118 do
119 var id = just_opened_id
120 if id == null then return # Register `new_object` only once
121 cache[id] = new_object
122 end
123
124 # Convert the simple JSON `object` to a Nit object
125 private fun convert_object(object: nullable Object, static_type: nullable String): nullable Object
126 do
127 if object isa JsonParseError then
128 errors.add object
129 return null
130 end
131
132 if object isa Map[String, nullable Object] then
133 var kind = null
134 if object.keys.has("__kind") then
135 kind = object["__kind"]
136 end
137
138 # ref?
139 if kind == "ref" then
140 if not object.keys.has("__id") then
141 errors.add new Error("Deserialization Error: JSON object reference does not declare a `__id`.")
142 return object
143 end
144
145 var id = object["__id"]
146 if not id isa Int then
147 errors.add new Error("Deserialization Error: JSON object reference declares a non-integer `__id`.")
148 return object
149 end
150
151 if not cache.has_id(id) then
152 errors.add new Error("Deserialization Error: JSON object reference has an unknown `__id`.")
153 return object
154 end
155
156 return cache.object_for(id)
157 end
158
159 # obj?
160 if kind == "obj" or kind == null then
161 var id = null
162 if object.keys.has("__id") then
163 id = object["__id"]
164
165 if not id isa Int then
166 errors.add new Error("Deserialization Error: JSON object declaration declares a non-integer `__id`.")
167 return object
168 end
169
170 if cache.has_id(id) then
171 errors.add new Error("Deserialization Error: JSON object with `__id` {id} is deserialized twice.")
172 # Keep going
173 end
174 end
175
176 var class_name = object.get_or_null("__class")
177 if class_name == null then
178 # Fallback to custom heuristic
179 class_name = class_name_heuristic(object)
180
181 if class_name == null and static_type != null then
182 # Fallack to the static type, strip the `nullable` prefix
183 var prefix = "nullable "
184 if static_type.has_prefix(prefix) then
185 class_name = static_type.substring_from(prefix.length)
186 else class_name = static_type
187 end
188 end
189
190 if class_name == null then
191 errors.add new Error("Deserialization Error: JSON object declaration does not declare a `__class`.")
192 return object
193 end
194
195 if not class_name isa String then
196 errors.add new Error("Deserialization Error: JSON object declaration declares a non-string `__class`.")
197 return object
198 end
199
200 if whitelist.not_empty and not whitelist.has(class_name) then
201 errors.add new Error("Deserialization Error: '{class_name}' not in whitelist")
202 return null
203 end
204
205 if static_type != null and check_subtypes then
206 var static_class = static_type.strip_nullable_and_params
207 var dynamic_class = class_name.strip_nullable_and_params
208 if not class_inheritance_metamodel.has_edge(dynamic_class, static_class) then
209 errors.add new Error("Deserialization Error: `{class_name}` is not a subtype of the static type `{static_type}`")
210 return null
211 end
212 end
213
214 # advance on path
215 path.push object
216
217 just_opened_id = id
218 var value = deserialize_class(class_name)
219 just_opened_id = null
220
221 # revert on path
222 path.pop
223
224 return value
225 end
226
227 # char?
228 if kind == "char" then
229 if not object.keys.has("__val") then
230 errors.add new Error("Deserialization Error: JSON `char` object does not declare a `__val`.")
231 return object
232 end
233
234 var val = object["__val"]
235
236 if not val isa String or val.is_empty then
237 errors.add new Error("Deserialization Error: JSON `char` object does not declare a single char in `__val`.")
238 return object
239 end
240
241 return val.chars.first
242 end
243
244 errors.add new Error("Deserialization Error: JSON object has an unknown `__kind`.")
245 return object
246 end
247
248 # Simple JSON array without serialization metadata
249 if object isa Array[nullable Object] then
250 # Can we use the static type?
251 if static_type != null then
252 var prefix = "nullable "
253 var class_name = if static_type.has(prefix) then
254 static_type.substring_from(prefix.length)
255 else static_type
256
257 opened_array = object
258 var value = deserialize_class(class_name)
259 opened_array = null
260 return value
261 end
262
263 # This branch should rarely be used:
264 # when an array is the root object which is accepted but illegal in standard JSON,
265 # or in strange custom deserialization hacks.
266
267 var array = new Array[nullable Object]
268 var types = new HashSet[String]
269 var has_nullable = false
270 for e in object do
271 var res = convert_object(e)
272 array.add res
273
274 if res != null then
275 types.add res.class_name
276 else has_nullable = true
277 end
278
279 if types.length == 1 then
280 var array_type = types.first
281
282 var typed_array
283 if array_type == "ASCIIFlatString" or array_type == "UnicodeFlatString" then
284 if has_nullable then
285 typed_array = new Array[nullable FlatString]
286 else typed_array = new Array[FlatString]
287 else if array_type == "Int" then
288 if has_nullable then
289 typed_array = new Array[nullable Int]
290 else typed_array = new Array[Int]
291 else if array_type == "Float" then
292 if has_nullable then
293 typed_array = new Array[nullable Float]
294 else typed_array = new Array[Float]
295 else
296 # TODO support all array types when we separate the constructor
297 # `from_deserializer` from the filling of the items.
298
299 if not has_nullable then
300 typed_array = new Array[Object]
301 else
302 # Unsupported array type, return as `Array[nullable Object]`
303 return array
304 end
305 end
306
307 assert typed_array isa Array[nullable Object]
308
309 # Copy item to the new array
310 for e in array do typed_array.add e
311 return typed_array
312 end
313
314 # Uninferrable type, return as `Array[nullable Object]`
315 return array
316 end
317
318 return object
319 end
320
321 # Current array open for deserialization, used by `SimpleCollection::from_deserializer`
322 private var opened_array: nullable Array[nullable Object] = null
323
324 redef fun deserialize(static_type)
325 do
326 errors.clear
327 return convert_object(root, static_type)
328 end
329
330 # User customizable heuristic to infer the name of the Nit class to deserialize `json_object`
331 #
332 # This method is called only when deserializing an object without the metadata `__class`.
333 # Use the content of `json_object` to identify what Nit class it should be deserialized into.
334 # Or use `self.attributes_path` indicating where the deserialized object will be stored,
335 # is is less reliable as some objects don't have an associated attribute:
336 # the root/first deserialized object and collection elements.
337 #
338 # Return the class name as a `String` when it can be inferred,
339 # or `null` when the class name cannot be found.
340 #
341 # If a valid class name is returned, `json_object` will then be deserialized normally.
342 # So it must contain the attributes of the corresponding class, as usual.
343 #
344 # ~~~
345 # class MyData
346 # serialize
347 #
348 # var data: String
349 # end
350 #
351 # class MyError
352 # serialize
353 #
354 # var error: String
355 # var related_data: MyData
356 # end
357 #
358 # class MyJsonDeserializer
359 # super JsonDeserializer
360 #
361 # redef fun class_name_heuristic(json_object)
362 # do
363 # # Infer the Nit class from the content of the JSON object.
364 # if json_object.keys.has("error") then return "MyError"
365 # if json_object.keys.has("data") then return "MyData"
366 #
367 # # Infer the Nit class from the attribute where it will be stored.
368 # # This line duplicates a previous line, and would only apply when
369 # # `MyData` is within a `MyError`.
370 # if attributes_path.not_empty and attributes_path.last == "related_data" then return "MyData"
371 #
372 # return null
373 # end
374 # end
375 #
376 # var json = """{"data": "some data"}"""
377 # var deserializer = new MyJsonDeserializer(json)
378 # var deserialized = deserializer.deserialize
379 # assert deserializer.errors.is_empty
380 # assert deserialized isa MyData
381 #
382 # json = """{"error": "some error message",
383 # "related_data": {"data": "some other data"}}"""
384 # deserializer = new MyJsonDeserializer(json)
385 # deserialized = deserializer.deserialize
386 # assert deserializer.errors.is_empty
387 # assert deserialized isa MyError
388 # ~~~
389 protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String
390 do
391 return null
392 end
393 end
394
395 redef class Text
396
397 # Deserialize a `nullable Object` from this JSON formatted string
398 #
399 # Warning: Deserialization errors are reported with `print_error` and
400 # may be returned as a partial object or as `null`.
401 #
402 # This method is not appropriate when errors need to be handled programmatically,
403 # manually use a `JsonDeserializer` in such cases.
404 fun from_json_string: nullable Object
405 do
406 var deserializer = new JsonDeserializer(self)
407 var res = deserializer.deserialize
408 if deserializer.errors.not_empty then
409 print_error "Deserialization Errors: {deserializer.errors.join(", ")}"
410 end
411 return res
412 end
413
414 # Strip the `nullable` prefix and the params from the class name `self`
415 #
416 # ~~~nitish
417 # assert "String".strip_nullable_and_params == "String"
418 # assert "Array[Int]".strip_nullable_and_params == "Array"
419 # assert "Map[Set[String], Set[Int]]".strip_nullable_and_params == "Map"
420 # ~~~
421 private fun strip_nullable_and_params: String
422 do
423 var class_name = to_s
424
425 var prefix = "nullable "
426 if class_name.has_prefix(prefix) then class_name = class_name.substring_from(prefix.length)
427
428 var bracket_index = class_name.index_of('[')
429 if bracket_index == -1 then return class_name
430 return class_name.substring(0, bracket_index)
431 end
432 end
433
434 redef class SimpleCollection[E]
435 redef init from_deserializer(v)
436 do
437 super
438 if v isa JsonDeserializer then
439 v.notify_of_creation self
440 init
441
442 var open_array: nullable SequenceRead[nullable Object] = v.opened_array
443 if open_array == null then
444 # With metadata
445 var arr = v.path.last.get_or_null("__items")
446 if not arr isa SequenceRead[nullable Object] then
447 # If there is nothing, we consider that it is an empty collection.
448 if arr != null then v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
449 return
450 end
451 open_array = arr
452 end
453
454 # Name of the dynamic name of E
455 var items_type_name = (new GetName[E]).to_s
456
457 # Fill array
458 for o in open_array do
459 var obj = v.convert_object(o, items_type_name)
460 if obj isa E then
461 add obj
462 else v.errors.add new AttributeTypeError(self, "items", obj, items_type_name)
463 end
464 end
465 end
466 end
467
468 redef class Map[K, V]
469 redef init from_deserializer(v)
470 do
471 super
472
473 if v isa JsonDeserializer then
474 v.notify_of_creation self
475 init
476
477 var keys_type_name = (new GetName[K]).to_s
478 var values_type_name = (new GetName[V]).to_s
479
480 var length = v.deserialize_attribute("__length")
481 var keys = v.path.last.get_or_null("__keys")
482 var values = v.path.last.get_or_null("__values")
483
484 if keys == null and values == null then
485 # Fallback to a plain object
486 for key, value_src in v.path.last do
487
488 var value = v.convert_object(value_src, values_type_name)
489
490 if not key isa K then
491 v.errors.add new AttributeTypeError(self, "keys", key, keys_type_name)
492 continue
493 end
494
495 if not value isa V then
496 v.errors.add new AttributeTypeError(self, "values", value, values_type_name)
497 continue
498 end
499
500 self[key] = value
501 end
502 return
503 end
504
505 # Length is optional
506 if length == null and keys isa SequenceRead[nullable Object] then length = keys.length
507
508 # Consistency check
509 if not length isa Int or length < 0 or
510 not keys isa SequenceRead[nullable Object] or
511 not values isa SequenceRead[nullable Object] or
512 keys.length != values.length or length != keys.length then
513
514 # If there is nothing or length == 0, we consider that it is an empty Map.
515 if (length != null and length != 0) or keys != null or values != null then
516 v.errors.add new Error("Deserialization Error: invalid format in {self.class_name}")
517 end
518 return
519 end
520
521 # First, convert all keys to follow the order of the serialization
522 var converted_keys = new Array[K]
523 for i in length.times do
524 var key = v.convert_object(keys[i], keys_type_name)
525
526 if not key isa K then
527 v.errors.add new AttributeTypeError(self, "keys", key, keys_type_name)
528 continue
529 end
530
531 converted_keys.add key
532 end
533
534 # Then convert the values and build the map
535 for i in length.times do
536 var key = converted_keys[i]
537 var value = v.convert_object(values[i], values_type_name)
538
539 if not value isa V then
540 v.errors.add new AttributeTypeError(self, "values", value, values_type_name)
541 continue
542 end
543
544 if has_key(key) then
545 v.errors.add new Error("Deserialization Error: duplicated key '{key or else "null"}' in {self.class_name}, previous value overwritten")
546 end
547
548 self[key] = value
549 end
550 end
551 end
552 end
553
554 # ---
555 # Metamodel
556
557 # Class inheritance graph as a `POSet[String]` serialized to JSON
558 private fun class_inheritance_metamodel_json: CString is intern
559
560 redef class Sys
561 # Class inheritance graph
562 #
563 # ~~~
564 # var hierarchy = class_inheritance_metamodel
565 # assert hierarchy.has_edge("String", "Object")
566 # assert not hierarchy.has_edge("Object", "String")
567 # ~~~
568 var class_inheritance_metamodel: POSet[String] is lazy do
569 var engine = new JsonDeserializer(class_inheritance_metamodel_json.to_s)
570 engine.check_subtypes = false
571 engine.whitelist.add_all(
572 ["String", "POSet[String]", "POSetElement[String]", "HashSet[String]", "HashMap[String, POSetElement[String]]"])
573 var poset = engine.deserialize
574 if engine.errors.not_empty then
575 print_error engine.errors.join("\n")
576 return new POSet[String]
577 end
578 if poset isa POSet[String] then return poset
579 return new POSet[String]
580 end
581 end
582
583 redef class Deserializer
584 redef fun deserialize_class(name)
585 do
586 if name == "POSet[String]" then return new POSet[String].from_deserializer(self)
587 if name == "POSetElement[String]" then return new POSetElement[String].from_deserializer(self)
588 if name == "HashSet[String]" then return new HashSet[String].from_deserializer(self)
589 if name == "HashMap[String, POSetElement[String]]" then return new HashMap[String, POSetElement[String]].from_deserializer(self)
590
591 return super
592 end
593 end