Merge: serialization: safe formal types, fix warnings in generated code and more
authorJean Privat <jean@pryen.org>
Wed, 2 Aug 2017 18:17:06 +0000 (14:17 -0400)
committerJean Privat <jean@pryen.org>
Wed, 2 Aug 2017 18:17:06 +0000 (14:17 -0400)
* Use `GetName` to better restrict the accepted static type for attributes with formal types. This should complete the protection against the injection of unexpected types through deserialization (reported by @ppepos).
* Update `nitserial` to intrude import modules with private serializable classes, instead of skipping them. Serializing private classes from the lib should probably be avoided, but at least now we support them and we can see them.
* Fix the deserialization of maps from JSON with metadata and cycles. A difference in the deserialization order could cause a reference to be read before the referenced object. This issue may still be caused by versionning, so a foolproof solution should probably be added to the JSON deserializer.
* Fix a warning in generated code, it was generated for each `serialize` class (quite a lot). It caused 25 warnings just in lib/github/api.nit.
* Implement serialization for the private classes of `core::queue` and don't crash on deserialization errors in `a_star`.

Pull-Request: #2531
Reviewed-by: Jean Privat <jean@pryen.org>

15 files changed:
lib/a_star.nit
lib/gen_nit.nit
lib/json/serialization_read.nit
lib/serialization/serialization.nit
src/frontend/serialization_code_gen_phase.nit
src/nitserial.nit
tests/sav/nitce/test_json_deserialization_safe.res [new file with mode: 0644]
tests/sav/niti/test_json_deserialization_plain_alt2.res
tests/sav/nitserial_args1.res
tests/sav/test_json_deserialization_plain.res
tests/sav/test_json_deserialization_plain_alt2.res
tests/sav/test_json_deserialization_safe.res
tests/sav/test_json_deserialization_safe_alt1.res [deleted file]
tests/test_deserialization.nit
tests/test_json_deserialization_safe.nit

index db80519..9ea68e8 100644 (file)
@@ -211,8 +211,8 @@ class Node
        do
                deserializer.notify_of_creation self
 
-               var graph = deserializer.deserialize_attribute("graph")
-               assert graph isa Graph[N, Link]
+               var graph = deserializer.deserialize_attribute("graph", (new GetName[Graph[N, Link]]).to_s)
+               if not graph isa Graph[N, Link] then graph = new Graph[N, Link]
                self.graph = graph
        end
 end
@@ -247,10 +247,10 @@ class Graph[N: Node, L: Link]
        super Serializable
 
        # Nodes in this graph
-       var nodes: Set[N] = new HashSet[N]
+       var nodes = new Set[N]
 
        # Links in this graph
-       var links: Set[L] = new HashSet[L]
+       var links = new Set[L]
 
        # Add a `node` to this graph
        fun add_node(node: N): N
@@ -283,13 +283,17 @@ class Graph[N: Node, L: Link]
        do
                deserializer.notify_of_creation self
 
-               var nodes = deserializer.deserialize_attribute("nodes")
-               assert nodes isa HashSet[N]
-               self.nodes = nodes
+               var nodes = deserializer.deserialize_attribute("nodes", (new GetName[Set[N]]).to_s)
+               if deserializer.deserialize_attribute_missing then
+                       deserializer.errors.add new AttributeMissingError(self, "nodes")
+               end
+               if nodes isa Set[N] then self.nodes = nodes
 
-               var links = deserializer.deserialize_attribute("links")
-               assert links isa HashSet[L]
-               for link in links do add_link link
+               var links = deserializer.deserialize_attribute("links", (new GetName[Set[L]]).to_s)
+               if deserializer.deserialize_attribute_missing then
+                       deserializer.errors.add new AttributeMissingError(self, "links")
+               end
+               if links isa Set[L] then for link in links do add_link link
        end
 end
 
index 7af2044..c80229b 100644 (file)
@@ -55,8 +55,12 @@ class NitModule
        # Annotations on the module declaration
        var annotations = new Array[Writable]
 
-       # Imports from this module
-       var imports = new Array[Writable]
+       # Importation declarations
+       #
+       # Accepts two formats:
+       # * Module name only, short or qualified: `json`, `gamnit::flat`, etc.
+       # * Full importation declaration: `import json`, `private import gamnit::flat`, etc.
+       var imports = new Set[Writable]
 
        # Main content of this module
        var content = new Array[Writable]
@@ -75,7 +79,15 @@ class NitModule
                        add "end\n\n"
                end
 
-               for i in imports do add "import {i}\n"
+               for i in imports do
+                       if i.to_s.has("import ") then
+                               add i
+                       else
+                               add "import "
+                               add i
+                       end
+                       add "\n"
+               end
                add "\n"
 
                for l in content do
index a9ebcfd..0ac2bea 100644 (file)
@@ -138,18 +138,18 @@ class JsonDeserializer
                        # ref?
                        if kind == "ref" then
                                if not object.keys.has("__id") then
-                                       errors.add new Error("Serialization Error: JSON object reference does not declare a `__id`.")
+                                       errors.add new Error("Deserialization Error: JSON object reference does not declare a `__id`.")
                                        return object
                                end
 
                                var id = object["__id"]
                                if not id isa Int then
-                                       errors.add new Error("Serialization Error: JSON object reference declares a non-integer `__id`.")
+                                       errors.add new Error("Deserialization Error: JSON object reference declares a non-integer `__id`.")
                                        return object
                                end
 
                                if not cache.has_id(id) then
-                                       errors.add new Error("Serialization Error: JSON object reference has an unknown `__id`.")
+                                       errors.add new Error("Deserialization Error: JSON object reference has an unknown `__id`.")
                                        return object
                                end
 
@@ -163,12 +163,12 @@ class JsonDeserializer
                                        id = object["__id"]
 
                                        if not id isa Int then
-                                               errors.add new Error("Serialization Error: JSON object declaration declares a non-integer `__id`.")
+                                               errors.add new Error("Deserialization Error: JSON object declaration declares a non-integer `__id`.")
                                                return object
                                        end
 
                                        if cache.has_id(id) then
-                                               errors.add new Error("Serialization Error: JSON object with `__id` {id} is deserialized twice.")
+                                               errors.add new Error("Deserialization Error: JSON object with `__id` {id} is deserialized twice.")
                                                # Keep going
                                        end
                                end
@@ -188,12 +188,12 @@ class JsonDeserializer
                                end
 
                                if class_name == null then
-                                       errors.add new Error("Serialization Error: JSON object declaration does not declare a `__class`.")
+                                       errors.add new Error("Deserialization Error: JSON object declaration does not declare a `__class`.")
                                        return object
                                end
 
                                if not class_name isa String then
-                                       errors.add new Error("Serialization Error: JSON object declaration declares a non-string `__class`.")
+                                       errors.add new Error("Deserialization Error: JSON object declaration declares a non-string `__class`.")
                                        return object
                                end
 
@@ -227,21 +227,21 @@ class JsonDeserializer
                        # char?
                        if kind == "char" then
                                if not object.keys.has("__val") then
-                                       errors.add new Error("Serialization Error: JSON `char` object does not declare a `__val`.")
+                                       errors.add new Error("Deserialization Error: JSON `char` object does not declare a `__val`.")
                                        return object
                                end
 
                                var val = object["__val"]
 
                                if not val isa String or val.is_empty then
-                                       errors.add new Error("Serialization Error: JSON `char` object does not declare a single char in `__val`.")
+                                       errors.add new Error("Deserialization Error: JSON `char` object does not declare a single char in `__val`.")
                                        return object
                                end
 
                                return val.chars.first
                        end
 
-                       errors.add new Error("Serialization Error: JSON object has an unknown `__kind`.")
+                       errors.add new Error("Deserialization Error: JSON object has an unknown `__kind`.")
                        return object
                end
 
@@ -451,25 +451,15 @@ redef class SimpleCollection[E]
                                open_array = arr
                        end
 
-                       # Try to get the name of the single parameter type assuming it is E.
-                       # This does not work in non-generic subclasses,
-                       # when the first parameter is not E, or
-                       # when there is more than one parameter. (The last one could be fixed)
-                       var class_name = class_name
-                       var items_type = null
-                       var bracket_index = class_name.index_of('[')
-                       if bracket_index != -1 then
-                               var start = bracket_index + 1
-                               var ending = class_name.last_index_of(']')
-                               items_type = class_name.substring(start, ending-start)
-                       end
+                       # Name of the dynamic name of E
+                       var items_type_name = (new GetName[E]).to_s
 
                        # Fill array
                        for o in open_array do
-                               var obj = v.convert_object(o, items_type)
+                               var obj = v.convert_object(o, items_type_name)
                                if obj isa E then
                                        add obj
-                               else v.errors.add new AttributeTypeError(self, "items", obj, "E")
+                               else v.errors.add new AttributeTypeError(self, "items", obj, items_type_name)
                        end
                end
        end
@@ -484,6 +474,9 @@ redef class Map[K, V]
                        v.notify_of_creation self
                        init
 
+                       var keys_type_name = (new GetName[K]).to_s
+                       var values_type_name = (new GetName[V]).to_s
+
                        var length = v.deserialize_attribute("__length")
                        var keys = v.path.last.get_or_null("__keys")
                        var values = v.path.last.get_or_null("__values")
@@ -491,15 +484,16 @@ redef class Map[K, V]
                        if keys == null and values == null then
                                # Fallback to a plain object
                                for key, value_src in v.path.last do
-                                       var value = v.convert_object(value_src)
+
+                                       var value = v.convert_object(value_src, values_type_name)
 
                                        if not key isa K then
-                                               v.errors.add new AttributeTypeError(self, "keys", key, "K")
+                                               v.errors.add new AttributeTypeError(self, "keys", key, keys_type_name)
                                                continue
                                        end
 
                                        if not value isa V then
-                                               v.errors.add new AttributeTypeError(self, "values", value, "V")
+                                               v.errors.add new AttributeTypeError(self, "values", value, values_type_name)
                                                continue
                                        end
 
@@ -524,17 +518,26 @@ redef class Map[K, V]
                                return
                        end
 
+                       # First, convert all keys to follow the order of the serialization
+                       var converted_keys = new Array[K]
                        for i in length.times do
-                               var key = v.convert_object(keys[i])
-                               var value = v.convert_object(values[i])
+                               var key = v.convert_object(keys[i], keys_type_name)
 
                                if not key isa K then
-                                       v.errors.add new AttributeTypeError(self, "keys", key, "K")
+                                       v.errors.add new AttributeTypeError(self, "keys", key, keys_type_name)
                                        continue
                                end
 
+                               converted_keys.add key
+                       end
+
+                       # Then convert the values and build the map
+                       for i in length.times do
+                               var key = converted_keys[i]
+                               var value = v.convert_object(values[i], values_type_name)
+
                                if not value isa V then
-                                       v.errors.add new AttributeTypeError(self, "values", value, "V")
+                                       v.errors.add new AttributeTypeError(self, "values", value, values_type_name)
                                        continue
                                end
 
index b0d5d6c..597ae98 100644 (file)
@@ -49,6 +49,9 @@ module serialization is
        new_annotation serialize_as
 end
 
+intrude import core::queue
+import meta
+
 # Abstract serialization service to be sub-classed by specialized services.
 interface Serializer
        # Entry point method of this service, serialize the `object`
@@ -113,8 +116,7 @@ abstract class Deserializer
 
        # Deserialize the attribute with `name` from the object open for deserialization
        #
-       # The `static_type` can be used as last resort if the deserialized object
-       # desn't have any metadata declaring the dynamic type.
+       # The `static_type` restricts what kind of object can be deserialized.
        #
        # Return the deserialized value or null on error, and set
        # `deserialize_attribute_missing` to whether the attribute was missing.
@@ -170,17 +172,22 @@ abstract class Deserializer
        var errors = new Array[Error]
 end
 
-# Error on invalid dynamic type for a deserialized attribute
-class AttributeTypeError
+# Deserialization error related to an attribute of `receiver`
+abstract class AttributeError
        super Error
 
-       autoinit receiver, attribute_name, attribute, expected_type
-
        # Parent object of the problematic attribute
        var receiver: Object
 
        # Name of the problematic attribute in `receiver`
        var attribute_name: String
+end
+
+# Invalid dynamic type for a deserialized attribute
+class AttributeTypeError
+       super AttributeError
+
+       autoinit receiver, attribute_name, attribute, expected_type
 
        # Deserialized object that isn't of the `expected_type`
        var attribute: nullable Object
@@ -197,6 +204,17 @@ class AttributeTypeError
        end
 end
 
+# Missing attribute at deserialization
+class AttributeMissingError
+       super AttributeError
+
+       autoinit receiver, attribute_name
+
+       redef var message is lazy do
+               return "Deserialization Error: Missing attribute `{receiver.class_name}::{attribute_name}`"
+       end
+end
+
 # Instances of this class can be passed to `Serializer::serialize`
 interface Serializable
        # Serialize `self` to `serializer`
@@ -313,3 +331,71 @@ redef class Error
                v.serialize_attribute("cause", cause)
        end
 end
+
+# ---
+# core::queue classes
+
+redef abstract class ProxyQueue[E]
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+
+               var seq = v.deserialize_attribute("seq", (new GetName[Sequence[E]]).to_s)
+               if not seq isa Sequence[E] then seq = new Array[E]
+               if v.deserialize_attribute_missing then
+                       v.errors.add new AttributeMissingError(self, "seq")
+               end
+
+               init seq
+       end
+
+       redef fun core_serialize_to(v) do v.serialize_attribute("seq", seq)
+end
+
+redef class RandQueue[E]
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+
+               var seq = v.deserialize_attribute("seq", (new GetName[SimpleCollection[E]]).to_s)
+               if not seq isa SimpleCollection[E] then seq = new Array[E]
+               if v.deserialize_attribute_missing then
+                       v.errors.add new AttributeMissingError(self, "seq")
+               end
+
+               init seq
+       end
+
+       redef fun core_serialize_to(v) do v.serialize_attribute("seq", seq)
+end
+
+redef class MinHeap[E]
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+
+               var items = v.deserialize_attribute("items", (new GetName[SimpleCollection[E]]).to_s)
+               if not items isa Array[E] then items = new Array[E]
+               if v.deserialize_attribute_missing then
+                       v.errors.add new AttributeMissingError(self, "items")
+               end
+
+               var comparator = v.deserialize_attribute("comparator", "Comparator")
+               if not comparator isa Comparator then comparator = default_comparator
+               if v.deserialize_attribute_missing then
+                       v.errors.add new AttributeMissingError(self, "comparator")
+               end
+
+               init comparator
+               self.items.add_all items
+       end
+
+       redef fun core_serialize_to(v)
+       do
+               v.serialize_attribute("items", items)
+               v.serialize_attribute("comparator", comparator)
+       end
+end
index 57f46c5..3e4e38d 100644 (file)
@@ -53,7 +53,7 @@ private class SerializationPhasePostModel
        do
                var code = new Array[String]
                code.add """
-redef init from_deserializer(v: Deserializer)
+redef init from_deserializer(v)
 do
        super
        v.notify_of_creation self
@@ -71,42 +71,50 @@ do
                        var type_name = mtype.to_s
                        var name = attribute.name
 
-                       var resolved_type_name = type_name
-                       var mclassdef = nclassdef.mclassdef
-                       if mclassdef != null then
-                               var bound_mtype = mclassdef.bound_mtype
-                               var resolved_mtype = mtype.resolve_for(bound_mtype, bound_mtype, mclassdef.mmodule, true)
-                               resolved_type_name = resolved_mtype.name
-
-                               # TODO Use something like `V.class_name` to get the precise runtime type of virtual types.
-                               # We currently use the upper bound of virtual types as static type in generated code
-                               # for type suggestion and to prevent loading unexected types.
-                               # This leaves a security issue when, for example, `DefaultMap::default_value`
-                               # is bound to `nullable Object` and would allow any object to be loaded.
+                       if mtype.need_anchor then
+                               # Check dynamic type of virtual params
+                               code.add """
+       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", (new GetName[{{{mtype}}}]).to_s)
+"""
+                       else
+                               # No param to check
+                               code.add """
+       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{type_name}}}")
+"""
                        end
 
                        if type_name == "nullable Object" then
                                # Don't type check
                                code.add """
-       self.{{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{resolved_type_name}}}")
+       self.{{{name}}} = {{{name}}}
 """
                        else
+                               # Needs type check
                                code.add """
-       var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{resolved_type_name}}}")
        if v.deserialize_attribute_missing then
 """
                                # What to do when an attribute is missing?
                                if attribute.has_value then
                                        # Leave it to the default value
                                else if mtype isa MNullableType then
+                                       # This is always a nullable type
                                        code.add """
                self.{{{name}}} = null"""
+                               else if mtype isa MFormalType then
+                                       # This type nullability may change in subclasses
+                                       code.add """
+               var n___{{{name}}} = null
+               if n___{{{name}}} isa {{{mtype}}} then
+                       self.{{{name}}} = n___{{{name}}}
+               else
+                       v.errors.add new AttributeMissingError(self, "{{{name}}}")
+               end"""
                                else code.add """
-               v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")"""
+               v.errors.add new AttributeMissingError(self, "{{{name}}}")"""
 
                                code.add """
        else if not {{{name}}} isa {{{type_name}}} then
-               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{resolved_type_name}}}")
+               v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{type_name}}}")
                if v.keep_going == false then return
        else
                self.{{{name}}} = {{{name}}}
index 574bc5d..2b347f1 100644 (file)
@@ -57,25 +57,24 @@ redef class MModule
 end
 
 redef class MType
-       # Is this type fully visible from `mmodule`?
-       fun is_visible_from(mmodule: MModule): Bool is abstract
+       # List classes composing this type
+       private fun related_mclasses(mmodule: MModule): Array[MClass] is abstract
 end
 
 redef class MClassType
-       redef fun is_visible_from(mmodule) do
-               return mmodule.is_visible(mclass.intro_mmodule, mclass.visibility)
-       end
+       redef fun related_mclasses(mmodule) do return [mclass]
 end
 
-redef class MNullableType
-       redef fun is_visible_from(mmodule) do return mtype.is_visible_from(mmodule)
+redef class MProxyType
+       redef fun related_mclasses(mmodule) do return undecorate.related_mclasses(mmodule)
 end
 
 redef class MGenericType
-       redef fun is_visible_from(mmodule)
+       redef fun related_mclasses(mmodule)
        do
-               for arg_mtype in arguments do if not arg_mtype.is_visible_from(mmodule) then return false
-               return super
+               var mods = super
+               for arg_mtype in arguments do mods.add_all(arg_mtype.related_mclasses(mmodule))
+               return mods
        end
 end
 
@@ -189,10 +188,25 @@ redef class Deserializer
                        # and which are visible.
                        if mtype isa MGenericType and
                           mtype.is_subtype(m, null, serializable_type) and
-                          mtype.is_visible_from(mmodule) and
                           mtype.mclass.kind == concrete_kind and
                           not compiled_types.has(mtype) then
 
+                               # Intrude import the modules declaring private classes
+                               var related_mclasses = mtype.related_mclasses(mmodule)
+                               for mclass in related_mclasses do
+                                       if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then
+                                               var intro_mmodule = mclass.intro_mmodule
+                                               var intro_mgroup = intro_mmodule.mgroup
+
+                                               var to_import = intro_mmodule.full_name
+                                               if intro_mgroup == null or intro_mgroup.default_mmodule == intro_mmodule then
+                                                       to_import = intro_mmodule.name
+                                               end
+
+                                               nit_module.imports.add "intrude import {to_import}"
+                                       end
+                               end
+
                                compiled_types.add mtype
                                nit_module.content.add """
                if name == \"{{{mtype}}}\" then return new {{{mtype}}}.from_deserializer(self)"""
diff --git a/tests/sav/nitce/test_json_deserialization_safe.res b/tests/sav/nitce/test_json_deserialization_safe.res
new file mode 100644 (file)
index 0000000..fc7aae9
--- /dev/null
@@ -0,0 +1,23 @@
+Deserialization Error: `DangerSub` is not a subtype of the static type `nullable A`
+Deserialization Error: `DangerSub` is not a subtype of the static type ``
+<A other:null next:null>
+---
+DANGER 'My text 1'
+DANGER 'My text 2'
+Deserialization Error: Wrong type on `A::other` expected `nullable A`, got `DangerSub`
+Deserialization Error: Wrong type on `A::next` expected `C`, got `DangerSub`
+<A other:null next:null>
+---
+Deserialization Error: `B` is not a subtype of the static type ``
+<B other:null next:null>
+---
+Deserialization Error: `A` is not a subtype of the static type ``
+<B other:null next:null>
+---
+Deserialization Error: `A` is not a subtype of the static type ``
+Deserialization Error: Wrong type on `G::e` expected `E`, got `null`
+<G e:not-set>
+---
+Deserialization Error: `A` is not a subtype of the static type ``
+Deserialization Error: Wrong type on `G::e` expected `E`, got `null`
+<G e:not-set>
index a6ae660..c5bbcb5 100644 (file)
@@ -1,3 +1,3 @@
 Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:27)
 # JSON: {"__class": "MyClass", "i": 123, "o": null}
-# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
+# Errors: 'Deserialization Error: Missing attribute `MyClass::s`', 'Deserialization Error: Missing attribute `MyClass::f`', 'Deserialization Error: Missing attribute `MyClass::a`'
index 3e2cf64..8f3e651 100644 (file)
@@ -7,6 +7,7 @@ end
 
 import test_serialization
 import serialization
+intrude import serialization::engine_tools
 
 redef class Deserializer
        redef fun deserialize_class(name)
@@ -15,6 +16,7 @@ redef class Deserializer
                if name == "Array[Text]" then return new Array[Text].from_deserializer(self)
                if name == "Array[Map[String, nullable Object]]" then return new Array[Map[String, nullable Object]].from_deserializer(self)
                if name == "Array[String]" then return new Array[String].from_deserializer(self)
+               if name == "StrictHashMap[Int, Object]" then return new StrictHashMap[Int, Object].from_deserializer(self)
                if name == "Array[Error]" then return new Array[Error].from_deserializer(self)
                if name == "POSet[String]" then return new POSet[String].from_deserializer(self)
                if name == "Array[Int]" then return new Array[Int].from_deserializer(self)
@@ -27,6 +29,7 @@ redef class Deserializer
                if name == "Array[Float]" then return new Array[Float].from_deserializer(self)
                if name == "Array[Object]" then return new Array[Object].from_deserializer(self)
                if name == "Array[Serializable]" then return new Array[Serializable].from_deserializer(self)
+               if name == "StrictHashMap[Serializable, Int]" then return new StrictHashMap[Serializable, Int].from_deserializer(self)
                if name == "POSetElement[String]" then return new POSetElement[String].from_deserializer(self)
                if name == "HashMap[String, POSetElement[String]]" then return new HashMap[String, POSetElement[String]].from_deserializer(self)
                if name == "Array[Match]" then return new Array[Match].from_deserializer(self)
index c8b7dd2..8487e43 100644 (file)
@@ -18,7 +18,7 @@
 # Nit: <MyClass i:123 s:hello f:123.456 a:[one, two] o:<MyClass i:456 s:world f:654.321 a:[1, 2] o:<null>>>
 
 # JSON: {"i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o": null}
-# Errors: 'Serialization Error: JSON object declaration does not declare a `__class`.'
+# Errors: 'Deserialization Error: JSON object declaration does not declare a `__class`.'
 # Nit: <JsonObject i:123, s:hello, f:123.456, a:[one,two], o:<null>>
 
 # JSON: {"__class": "MyClass", "i": 123, "s": "hello", "f": 123.456, "a": ["one", "two"], "o": "Not the right type"}
index 4b53f0f..190b5f4 100644 (file)
@@ -1,3 +1,3 @@
 Runtime error: Uninitialized attribute _s (alt/test_json_deserialization_plain_alt2.nit:22)
 # JSON: {"__class": "MyClass", "i": 123, "o": null}
-# Errors: 'Deserialization Error: attribute `MyClass::s` missing from JSON object', 'Deserialization Error: attribute `MyClass::f` missing from JSON object', 'Deserialization Error: attribute `MyClass::a` missing from JSON object'
+# Errors: 'Deserialization Error: Missing attribute `MyClass::s`', 'Deserialization Error: Missing attribute `MyClass::f`', 'Deserialization Error: Missing attribute `MyClass::a`'
index e429d54..7d64cc0 100644 (file)
@@ -1,4 +1,22 @@
-Deserialization Error: `DangerSub` is not a subtype of the static type `MyClass`
-Deserialization Error: Wrong type on `MyClass::other` expected `MyClass`, got `null`
-Deserialization Error: `DangerSub` is not a subtype of the static type `MyClass`
-Deserialization Error: Wrong type on `MyClass::next` expected `MyClass`, got `null`
+Deserialization Error: `DangerSub` is not a subtype of the static type `nullable A`
+Deserialization Error: `DangerSub` is not a subtype of the static type `nullable A`
+<A other:null next:null>
+---
+DANGER 'My text 1'
+DANGER 'My text 2'
+Deserialization Error: Wrong type on `A::other` expected `nullable A`, got `DangerSub`
+Deserialization Error: Wrong type on `A::next` expected `C`, got `DangerSub`
+<A other:null next:null>
+---
+
+<B other:null next:<B other:null next:null>>
+---
+Deserialization Error: `A` is not a subtype of the static type `nullable B`
+<B other:null next:null>
+---
+
+<G[A] e:<A other:null next:null>>
+---
+Deserialization Error: `A` is not a subtype of the static type `B`
+Deserialization Error: Wrong type on `G[B]::e` expected `E`, got `null`
+<G[B] e:not-set>
diff --git a/tests/sav/test_json_deserialization_safe_alt1.res b/tests/sav/test_json_deserialization_safe_alt1.res
deleted file mode 100644 (file)
index 66757ac..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-DANGER 'My text 1'
-DANGER 'My text 2'
-Deserialization Error: Wrong type on `MyClass::other` expected `MyClass`, got `DangerSub`
-Deserialization Error: Wrong type on `MyClass::next` expected `MyClass`, got `DangerSub`
index 478f9d0..527821b 100644 (file)
@@ -125,5 +125,5 @@ class TestEntities
        var with_generics: Array[Serializable] = [a, b, c, d, e, fi, ff, g: Serializable]
 end
 
-# We instanciate it here so that `nitserial` detects generic types as being alive
+# We instantiate it here so that `nitserial` detects generic types as being alive
 var entities = new TestEntities
index 22eaf87..65bf393 100644 (file)
 
 import json
 
-class MyClass
+class A
     serialize
 
-       var other: MyClass
+       var other: nullable A
 
-       type C: MyClass
+       type C: nullable A
        var next: C
+
+       redef fun to_s do return "<{class_name} other:{other or else "null"} next:{next or else "null"}>"
+end
+
+class B
+       super A
+       serialize
+
+       redef type C: nullable B
 end
 
 class DangerSup
@@ -40,13 +49,75 @@ class DangerSub
        end
 end
 
-var json_with_metadata = """{
-    "__class": "MyClass",
+class G[E: A]
+       serialize
+       var e: E
+
+       redef fun to_s do return "<{class_name} e:{if isset _e then e.to_s else "not-set"}>"
+end
+
+redef class Deserializer
+       redef fun deserialize_class(name)
+       do
+               if name == "G[A]" then return new G[A].from_deserializer(self)
+               if name == "G[B]" then return new G[B].from_deserializer(self)
+               return super
+       end
+end
+
+var json = """{
+    "__class": "A",
     "other": {"__class": "DangerSub", "dangerous_setter": "My text 1"},
     "next": {"__class": "DangerSub", "dangerous_setter": "My text 2"}
 }"""
 
-var deserializer = new JsonDeserializer(json_with_metadata)
-#alt1#deserializer.check_subtypes = false
-deserializer.deserialize
+# OK, accept only valid types, so don't try to deserialize DangerSub
+var deserializer = new JsonDeserializer(json)
+var res = deserializer.deserialize
+print deserializer.errors.join("\n")
+print res or else "null"
+
+print "---"
+
+# Accept any types, so deserialize the dangerous classes
+deserializer = new JsonDeserializer(json)
+deserializer.check_subtypes = false
+res = deserializer.deserialize
+print deserializer.errors.join("\n")
+print res or else "null"
+
+print "---"
+
+# Valid virtual type for `next: B`
+json = """{"__class": "B", "next": {"__class": "B"}}"""
+deserializer = new JsonDeserializer(json)
+res = deserializer.deserialize
+print deserializer.errors.join("\n")
+print res or else "null"
+
+print "---"
+
+# Virtual type error for `next: B`
+json = """{"__class": "B", "next": {"__class": "A"}}"""
+deserializer = new JsonDeserializer(json)
+res = deserializer.deserialize
+print deserializer.errors.join("\n")
+print res or else "null"
+
+print "---"
+
+# Valid parameter type for `e: A`
+json = """{"__class": "G[A]", "e": {"__class": "A"}}"""
+deserializer = new JsonDeserializer(json)
+res = deserializer.deserialize
+print deserializer.errors.join("\n")
+print res or else "null"
+
+print "---"
+
+# Parameter type error for `e: B`
+json = """{"__class": "G[B]", "e": {"__class": "A"}}"""
+deserializer = new JsonDeserializer(json)
+res = deserializer.deserialize
 print deserializer.errors.join("\n")
+print res or else "null"