Merge: modelize: ask that attributes in refinements are either noautoninit or have...
[nit.git] / lib / json / serialization.nit
index 7332b83..359acf5 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Handles serialization and deserialization of objects to/from Json.
+# Handles serialization and deserialization of objects to/from JSON
+#
+# ## Nity JSON
+#
+# `JsonSerializer` write Nit objects that subclass `Serializable` to JSON,
+# and `JsonDeserializer` can read them. They both use meta-data added to the
+# generated JSON to recreate the Nit instances with the exact original type.
+#
+# For more information on Nit serialization, see: ../serialization/README.md
+#
+# ## Plain JSON
+#
+# The attribute `JsonSerializer::plain_json` triggers generating plain and
+# clean JSON. This format is easier to read for an human and a non-Nit program,
+# but it cannot be fully deserialized. It can still be read by services from
+# `json::static` and `json::dynamic`.
+#
+# A shortcut to this service is provided by `Serializable::to_plain_json`.
+#
+# ### Usage Example
+#
+# ~~~nitish
+# import json::serialization
+#
+# class Person
+#     serialize
+#
+#     var name: String
+#     var year_of_birth: Int
+#     var next_of_kin: nullable Person
+# end
+#
+# var bob = new Person("Bob", 1986)
+# var alice = new Person("Alice", 1978, bob)
+#
+# assert bob.to_plain_json == """
+# {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}"""
+#
+# assert alice.to_plain_json == """
+# {"name": "Alice", "year_of_birth": 1978, "next_of_kin": {"name": "Bob", "year_of_birth": 1986, "next_of_kin": null}}"""
+# ~~~
 module serialization
 
-import ::serialization
+import ::serialization::caching
 private import ::serialization::engine_tools
-import static
+private import static
 
 # Serializer of Nit objects to Json string.
 class JsonSerializer
-       super Serializer
+       super CachingSerializer
 
        # Target writing stream
        var stream: Writer
 
+       # Write plain JSON? easier to read but does not support Nit deserialization
+       #
+       # If `false`, the default, serialize to support deserialization:
+       #
+       # * Write meta-data, including the types of the serialized objects so they can
+       #   be deserialized to their original form using `JsonDeserializer`.
+       # * Use references when an object has already been serialized so to not duplicate it.
+       # * Support cycles in references.
+       # * Preserve the Nit `Char` type as an object because it does not exist in JSON.
+       # * The generated JSON is standard and can be read by non-Nit programs.
+       #   However, some Nit types are not represented by the simplest possible JSON representation.
+       #   With the added meta-data, it can be complex to read.
+       #
+       # If `true`, serialize for other programs:
+       #
+       # * Nit objects are serialized to pure and standard JSON so they can
+       #   be easily read by non-Nit programs and humans.
+       # * Nit objects are serialized for every references, so they can be duplicated.
+       #   It is easier to read but it creates a larger output.
+       # * Does not support cycles, will replace the problematic references by `null`.
+       # * Does not serialize the meta-data needed to deserialize the objects
+       #   back to regular Nit objects.
+       # * Keys of Nit `HashMap` are converted to their string reprensentation using `to_s`.
+       var plain_json = false is writable
+
+       # List of the current open objects, the first is the main target of the serialization
+       #
+       # Used only when `plain_json == true` to detect cycles in serialization.
+       private var open_objects = new Array[Object]
+
+       # Has the first attribute of the current object already been serialized?
+       #
+       # Used only when `plain_json == true`.
+       private var first_attribute = false
+
        redef fun serialize(object)
        do
                if object == null then
                        stream.write "null"
-               else object.serialize_to_json(self)
+               else
+                       if plain_json then
+                               for o in open_objects do
+                                       if object.is_same_serialized(o) then
+                                               # Cycle detected
+                                               stream.write "null"
+                                               return
+                                       end
+                               end
+
+                               open_objects.add object
+                       end
+
+                       first_attribute = true
+                       object.serialize_to_json self
+                       first_attribute = false
+
+                       if plain_json then open_objects.pop
+               end
        end
 
        redef fun serialize_attribute(name, value)
        do
-               stream.write ", \""
+               if not plain_json or not first_attribute then
+                       stream.write ", "
+                       first_attribute = false
+               end
+
+               stream.write "\""
                stream.write name
                stream.write "\": "
                super
@@ -45,9 +143,9 @@ class JsonSerializer
 
        redef fun serialize_reference(object)
        do
-               if refs_map.has_key(object) then
+               if not plain_json and cache.has_object(object) then
                        # if already serialized, add local reference
-                       var id = ref_id_for(object)
+                       var id = cache.id_for(object)
                        stream.write "\{\"__kind\": \"ref\", \"__id\": "
                        stream.write id.to_s
                        stream.write "\}"
@@ -56,38 +154,20 @@ class JsonSerializer
                        serialize object
                end
        end
-
-       # Map of references to already serialized objects.
-       private var refs_map = new StrictHashMap[Serializable,Int]
-
-       # Get the internal serialized reference for this `object`.
-       private fun ref_id_for(object: Serializable): Int
-       do
-               if refs_map.has_key(object) then
-                       return refs_map[object]
-               else
-                       var id = refs_map.length
-                       refs_map[object] = id
-                       return id
-               end
-       end
 end
 
 # Deserializer from a Json string.
 class JsonDeserializer
-       super Deserializer
+       super CachingDeserializer
 
        # Json text to deserialize from.
        private var text: Text
 
        # Root json object parsed from input text.
-       var root: nullable Jsonable is noinit
+       private var root: nullable Jsonable is noinit
 
        # Depth-first path in the serialized object tree.
-       var path = new Array[JsonObject]
-
-       # Map of references to already deserialized objects.
-       private var id_to_object = new StrictHashMap[Int, Object]
+       private var path = new Array[JsonObject]
 
        # Last encountered object reference id.
        #
@@ -117,7 +197,7 @@ class JsonDeserializer
        do
                var id = just_opened_id
                if id == null then return # Register `new_object` only once
-               id_to_object[id] = new_object
+               cache[id] = new_object
        end
 
        # Convert from simple Json object to Nit object
@@ -133,8 +213,8 @@ class JsonDeserializer
                                var id = object["__id"]
                                assert id isa Int
 
-                               assert id_to_object.has_key(id)
-                               return id_to_object[id]
+                               assert cache.has_id(id)
+                               return cache.object_for(id)
                        end
 
                        # obj?
@@ -147,7 +227,7 @@ class JsonDeserializer
                                var class_name = object["__class"]
                                assert class_name isa String
 
-                               assert not id_to_object.has_key(id) else print "Error: Object with id '{id}' of {class_name} is deserialized twice."
+                               assert not cache.has_id(id) else print "Error: Object with id '{id}' of {class_name} is deserialized twice."
 
                                # advance on path
                                path.push object
@@ -193,15 +273,32 @@ end
 redef class Serializable
        private fun serialize_to_json(v: JsonSerializer)
        do
-               var id = v.ref_id_for(self)
-               v.stream.write "\{\"__kind\": \"obj\", \"__id\": "
-               v.stream.write id.to_s
-               v.stream.write ", \"__class\": \""
-               v.stream.write class_name
-               v.stream.write "\""
+               var id = v.cache.new_id_for(self)
+               v.stream.write "\{"
+               if not v.plain_json then
+                       v.stream.write "\"__kind\": \"obj\", \"__id\": "
+                       v.stream.write id.to_s
+                       v.stream.write ", \"__class\": \""
+                       v.stream.write class_name
+                       v.stream.write "\""
+               end
                core_serialize_to(v)
                v.stream.write "\}"
        end
+
+       # Serialize this object to plain JSON
+       #
+       # This is a shortcut using `JsonSerializer::plain_json`,
+       # see its documentation for more information.
+       fun to_plain_json: String
+       do
+               var stream = new StringWriter
+               var serializer = new JsonSerializer(stream)
+               serializer.plain_json = true
+               serializer.serialize self
+               stream.close
+               return stream.to_s
+       end
 end
 
 redef class Int
@@ -219,9 +316,13 @@ end
 redef class Char
        redef fun serialize_to_json(v)
        do
-               v.stream.write "\{\"__kind\": \"char\", \"__val\": "
-               v.stream.write to_s.to_json
-               v.stream.write "\}"
+               if v.plain_json then
+                       v.stream.write to_s.to_json
+               else
+                       v.stream.write "\{\"__kind\": \"char\", \"__val\": "
+                       v.stream.write to_s.to_json
+                       v.stream.write "\}"
+               end
        end
 end
 
@@ -256,20 +357,27 @@ redef class SimpleCollection[E]
        redef fun serialize_to_json(v)
        do
                # Register as pseudo object
-               var id = v.ref_id_for(self)
-               v.stream.write """{"__kind": "obj", "__id": """
-               v.stream.write id.to_s
-               v.stream.write """, "__class": """"
-               v.stream.write class_name
-               v.stream.write """", "__length": """
-               v.stream.write length.to_s
-               v.stream.write """, "__items": """
+               if not v.plain_json then
+                       var id = v.cache.new_id_for(self)
+                       v.stream.write """{"__kind": "obj", "__id": """
+                       v.stream.write id.to_s
+                       v.stream.write """, "__class": """"
+                       v.stream.write class_name
+                       v.stream.write """", "__length": """
+                       v.stream.write length.to_s
+                       v.stream.write """, "__items": """
+               end
+
                serialize_to_pure_json v
-               v.stream.write "\}"
+
+               if not v.plain_json then
+                       v.stream.write "\}"
+               end
        end
 
        redef init from_deserializer(v: Deserializer)
        do
+               super
                if v isa JsonDeserializer then
                        v.notify_of_creation self
                        init
@@ -287,7 +395,7 @@ end
 redef class Array[E]
        redef fun serialize_to_json(v)
        do
-               if class_name == "Array[nullable Serializable]" then
+               if v.plain_json or class_name == "Array[nullable Serializable]" then
                        # Using class_name to get the exact type,
                        # we do not want Array[Int] or anything else here.
 
@@ -300,30 +408,52 @@ redef class Map[K, V]
        redef fun serialize_to_json(v)
        do
                # Register as pseudo object
-               var id = v.ref_id_for(self)
+               var id = v.cache.new_id_for(self)
+
+               if v.plain_json then
+                       v.stream.write "\{"
+                       var first = true
+                       for key, val in self do
+                               if not first then
+                                       v.stream.write ", "
+                               else first = false
+
+                               if key == null then key = "null"
+
+                               v.stream.write key.to_s.to_json
+                               v.stream.write ": "
+                               if not v.try_to_serialize(val) then
+                                       v.warn("element of type {val.class_name} is not serializable.")
+                                       v.stream.write "null"
+                               end
+                       end
+                       v.stream.write "\}"
+               else
+                       v.stream.write """{"__kind": "obj", "__id": """
+                       v.stream.write id.to_s
+                       v.stream.write """, "__class": """"
+                       v.stream.write class_name
+                       v.stream.write """", "__length": """
+                       v.stream.write length.to_s
 
-               v.stream.write """{"__kind": "obj", "__id": """
-               v.stream.write id.to_s
-               v.stream.write """, "__class": """"
-               v.stream.write class_name
-               v.stream.write """", "__length": """
-               v.stream.write length.to_s
-               v.stream.write """, "__keys": """
+                       v.stream.write """, "__keys": """
+                       keys.serialize_to_pure_json v
 
-               keys.serialize_to_pure_json v
+                       v.stream.write """, "__values": """
+                       values.serialize_to_pure_json v
 
-               v.stream.write """, "__values": """
-               values.serialize_to_pure_json v
-               v.stream.write "\}"
+                       v.stream.write "\}"
+               end
        end
 
        # Instantiate a new `Array` from its serialized representation.
        redef init from_deserializer(v: Deserializer)
        do
-               init
+               super
 
                if v isa JsonDeserializer then
                        v.notify_of_creation self
+                       init
 
                        var length = v.deserialize_attribute("__length").as(Int)
                        var keys = v.path.last["__keys"].as(SequenceRead[nullable Object])