X-Git-Url: http://nitlanguage.org diff --git a/lib/json/serialization.nit b/lib/json/serialization.nit index 78bca4a..359acf5 100644 --- a/lib/json/serialization.nit +++ b/lib/json/serialization.nit @@ -14,76 +14,160 @@ # 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 ", \"{name}\": " + if not plain_json or not first_attribute then + stream.write ", " + first_attribute = false + end + + stream.write "\"" + stream.write name + stream.write "\": " super end 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) - stream.write "\{\"__kind\": \"ref\", \"__id\": {id}\}" + var id = cache.id_for(object) + stream.write "\{\"__kind\": \"ref\", \"__id\": " + stream.write id.to_s + stream.write "\}" else # serialize here 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. # @@ -113,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 @@ -129,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? @@ -143,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 @@ -189,11 +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\": {id}, \"__class\": \"{class_name}\"" + 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 @@ -209,7 +314,16 @@ redef class Bool end redef class Char - redef fun serialize_to_json(v) do v.stream.write "\{\"__kind\": \"char\", \"__val\": {to_s.to_json}\}" + redef fun serialize_to_json(v) + do + 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 redef class String @@ -243,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 @@ -274,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. @@ -287,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])