lib/json: document how to refine `accept_json_serializer` and make it protected
[nit.git] / lib / json / serialization.nit
index d5324e8..65d5cbf 100644 (file)
 
 # Handles serialization and deserialization of objects to/from JSON
 #
-# ## Nity JSON
+# ## Writing JSON with metadata
 #
 # `JsonSerializer` write Nit objects that subclass `Serializable` to JSON,
-# and `JsonDeserializer` can read them. They both use meta-data added to the
+# and `JsonDeserializer` can read them. They both use metadata 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
+# ## Writing 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`.
+# A shortcut to these writing services is provided by `Serializable::serialize_to_json`.
 #
 # ### Usage Example
 #
 # 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 bob.serialize_to_json(pretty=true, plain=true) == """
+#{
+#      "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}}"""
+# assert alice.serialize_to_json(pretty=true, plain=true) == """
+#{
+#      "name": "Alice",
+#      "year_of_birth": 1978,
+#      "next_of_kin": {
+#              "name": "Bob",
+#              "year_of_birth": 1986,
+#              "next_of_kin": null
+#      }
+#}"""
 # ~~~
 #
 # ## JSON to Nit objects
 #
-# The `JsonDeserializer` support reading JSON code with minimal meta-data
+# The `JsonDeserializer` support reading JSON code with minimal metadata
 # to easily create Nit object from client-side code or configuration files.
 # Each JSON object must define the `__class` attribute with the corresponding
 # Nit class and the expected attributes with its name in Nit followed by its value.
@@ -90,7 +102,7 @@ module serialization
 
 import ::serialization::caching
 private import ::serialization::engine_tools
-import static
+private import static
 
 # Serializer of Nit objects to Json string.
 class JsonSerializer
@@ -103,14 +115,14 @@ class JsonSerializer
        #
        # If `false`, the default, serialize to support deserialization:
        #
-       # * Write meta-data, including the types of the serialized objects so they can
+       # * Write metadata, 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.
+       #   With the added metadata, it can be complex to read.
        #
        # If `true`, serialize for other programs:
        #
@@ -119,7 +131,7 @@ class JsonSerializer
        # * 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
+       # * Does not serialize the metadata needed to deserialize the objects
        #   back to regular Nit objects.
        # * Keys of Nit `HashMap` are converted to their string representation using `to_s`.
        var plain_json = false is writable
@@ -162,7 +174,7 @@ class JsonSerializer
                        end
 
                        first_attribute = true
-                       object.serialize_to_json self
+                       object.accept_json_serializer self
                        first_attribute = false
 
                        if plain_json then open_objects.pop
@@ -220,10 +232,10 @@ class JsonDeserializer
        private var text: Text
 
        # Root json object parsed from input text.
-       private var root: nullable Jsonable is noinit
+       private var root: nullable Object is noinit
 
        # Depth-first path in the serialized object tree.
-       private var path = new Array[JsonObject]
+       private var path = new Array[Map[String, nullable Object]]
 
        # Last encountered object reference id.
        #
@@ -232,7 +244,7 @@ class JsonDeserializer
 
        init do
                var root = text.parse_json
-               if root isa JsonObject then path.add(root)
+               if root isa Map[String, nullable Object] then path.add(root)
                self.root = root
        end
 
@@ -268,7 +280,7 @@ class JsonDeserializer
                        return null
                end
 
-               if object isa JsonObject then
+               if object isa Map[String, nullable Object] then
                        var kind = null
                        if object.keys.has("__kind") then
                                kind = object["__kind"]
@@ -380,7 +392,7 @@ class JsonDeserializer
                                var array_type = types.first
 
                                var typed_array
-                               if array_type == "FlatString" then
+                               if array_type == "ASCIIFlatString" or array_type == "UnicodeFlatString" then
                                        if has_nullable then
                                                typed_array = new Array[nullable FlatString]
                                        else typed_array = new Array[FlatString]
@@ -467,7 +479,7 @@ class JsonDeserializer
        # deserialized = deserializer.deserialize
        # assert deserialized isa MyError
        # ~~~
-       protected fun class_name_heuristic(json_object: JsonObject): nullable String
+       protected fun class_name_heuristic(json_object: Map[String, nullable Object]): nullable String
        do
                return null
        end
@@ -492,11 +504,44 @@ redef class Text
                return res
        end
 
-       redef fun serialize_to_json(v) do v.stream.write(to_json)
+       redef fun accept_json_serializer(v) do v.stream.write(to_json)
 end
 
 redef class Serializable
-       private fun serialize_to_json(v: JsonSerializer)
+
+       # Serialize `self` to JSON
+       #
+       # Set `plain = true` to generate standard JSON, without deserialization metadata.
+       # Use this option if the generated JSON will be read by other programs or humans.
+       # Use the default, `plain = false`, if the JSON is to be deserialized by a Nit program.
+       #
+       # Set `pretty = true` to generate pretty JSON for human eyes.
+       # Use the default, `pretty = false`, to generate minified JSON.
+       #
+       # This method should not be refined by subclasses,
+       # instead `accept_json_serializer` can customize the serialization of an object.
+       #
+       # See: `JsonSerializer`
+       fun serialize_to_json(plain, pretty: nullable Bool): String
+       do
+               var stream = new StringWriter
+               var serializer = new JsonSerializer(stream)
+               serializer.plain_json = plain or else false
+               serializer.pretty_json = pretty or else false
+               serializer.serialize self
+               stream.close
+               return stream.to_s
+       end
+
+       # Refinable service to customize the serialization of this class to JSON
+       #
+       # This method can be refined to customize the serialization by either
+       # writing pure JSON directly on the stream `v.stream` or
+       # by using other services of `JsonSerializer`.
+       #
+       # Most of the time, it is preferable to refine the method `core_serialize_to`
+       # which is used by all the serialization engines, not just JSON.
+       protected fun accept_json_serializer(v: JsonSerializer)
        do
                var id = v.cache.new_id_for(self)
                v.stream.write "\{"
@@ -515,59 +560,35 @@ redef class Serializable
                v.new_line_and_indent
                v.stream.write "\}"
        end
-
-       # Serialize this object to a JSON string with metadata for deserialization
-       fun to_json_string: String
-       do
-               var stream = new StringWriter
-               var serializer = new JsonSerializer(stream)
-               serializer.serialize self
-               stream.close
-               return stream.to_s
-       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
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Float
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Bool
-       redef fun serialize_to_json(v) do v.stream.write(to_s)
+       redef fun accept_json_serializer(v) do v.stream.write to_s
 end
 
 redef class Char
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                if v.plain_json then
-                       v.stream.write to_s.to_json
+                       to_s.accept_json_serializer v
                else
                        v.stream.write "\{\"__kind\": \"char\", \"__val\": "
-                       v.stream.write to_s.to_json
+                       to_s.accept_json_serializer v
                        v.stream.write "\}"
                end
        end
 end
 
 redef class NativeString
-       redef fun serialize_to_json(v) do to_s.serialize_to_json(v)
+       redef fun accept_json_serializer(v) do to_s.accept_json_serializer(v)
 end
 
 redef class Collection[E]
@@ -595,7 +616,7 @@ redef class Collection[E]
 end
 
 redef class SimpleCollection[E]
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                # Register as pseudo object
                if not v.plain_json then
@@ -638,7 +659,7 @@ redef class SimpleCollection[E]
 end
 
 redef class Map[K, V]
-       redef fun serialize_to_json(v)
+       redef fun accept_json_serializer(v)
        do
                # Register as pseudo object
                var id = v.cache.new_id_for(self)
@@ -654,7 +675,7 @@ redef class Map[K, V]
                                v.new_line_and_indent
 
                                var k = key or else "null"
-                               v.stream.write k.to_s.to_json
+                               k.to_s.accept_json_serializer v
                                v.stream.write ": "
                                if not v.try_to_serialize(val) then
                                        assert val != null # null would have been serialized