Merge: serialization: redef inspect for an output useful to humans
authorJean Privat <jean@pryen.org>
Mon, 18 Sep 2017 19:00:47 +0000 (15:00 -0400)
committerJean Privat <jean@pryen.org>
Mon, 18 Sep 2017 19:00:47 +0000 (15:00 -0400)
Intro a new serialization engine to pretty print the attributes of the receiver on calls to `inspect`. The format is designed to be helpful, reproducible and compact.

Note that, asides from direct calls to `inspect`, this will also affect the default behavior of `Serializable::to_s`. And this will not affect all Nit programs, but it will affect all those importing the serialization services, so most larger projects using either `json`, `nitcorn`, `gamnit`, `more_collections`, etc.

Simple immutable data are inspected as they would be written in Nit code:
~~~
assert 123.inspect == "123"
assert 1.5.inspect == "1.5"
assert 0xa1u8.inspect == "0xa1u8"
assert 'c'.inspect == "'c'"
assert "asdf\n".inspect == "\"asdf\\n\""
~~~

Inspections of mutable object show their dynamic type and an id unique to each call to `inspect`. A change from using their `object_id`.

Items of collections are flattened:

~~~
assert [1, 2, 3].inspect == "<Array[Int]#0 [1, 2, 3]>"

var set = new HashSet[Object].from([1, 1.5, "two": Object])
assert set.inspect == """<HashSet[Object]#0 [1, 1.5, "two"]>"""

var map = new Map[Int, String]
map[1] = "one"
map[2] = "two"
assert map.inspect == """<HashMap[Int, String]#0 {1:"one", 2:"two"}>"""
~~~

Inspecting other `Serializable` classes shows the first level attributes only:

~~~
class MyClass
    serialize

    var i: Int
    var o: nullable Object
end

var class_with_null = new MyClass(123)
assert class_with_null.to_s == class_with_null.inspect
assert class_with_null.to_s == "<MyClass#0 i:123, o:null>"

var class_with_other = new MyClass(456, class_with_null)
assert class_with_other.to_s == "<MyClass#0 i:456, o:<MyClass#1>>"

var class_with_cycle = new MyClass(789)
class_with_cycle.o = class_with_cycle
assert class_with_cycle.to_s == "<MyClass#0 i:789, o:<MyClass#0>>"
~~~

Inspections producing over 80 characters are cut short:

~~~
var long_class = new MyClass(123456789, "Some " + "very "*8 + "long string")
assert long_class.to_s == "<MyClass#0 i:123456789, o:\"Some very very very very very very very very long s…>"
~~~

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

lib/poset.nit
lib/serialization/caching.nit
lib/serialization/engine_tools.nit
lib/serialization/inspect.nit [new file with mode: 0644]
lib/serialization/serialization.nit
lib/serialization/serialization_core.nit [new file with mode: 0644]
tests/sav/nitce/test_inspect_serialization.res [new file with mode: 0644]
tests/sav/nitserial_args1.res
tests/sav/test_inspect_serialization.res [new file with mode: 0644]
tests/test_inspect_serialization.nit [new file with mode: 0644]

index 767b27d..3cd64db 100644 (file)
@@ -17,7 +17,7 @@
 # Pre order sets and partial order set (ie hierarchies)
 module poset
 
-import serialization
+import serialization::serialization_core
 
 # Pre-order set graph.
 # This class models an incremental pre-order graph where new nodes and edges can be added (but not removed).
index 3554d22..e1b9a16 100644 (file)
@@ -15,7 +15,7 @@
 # Services for caching serialization engines
 module caching
 
-import serialization
+import serialization_core
 private import engine_tools
 
 # A `Serializer` with a `cache`
index d43da6d..94f6fbf 100644 (file)
@@ -15,7 +15,7 @@
 # Advanced services for serialization engines
 module engine_tools
 
-import serialization
+import serialization_core
 intrude import core::collection::hash_collection
 
 # Maps instances to a value, uses `is_same_serialized` and `serialization_hash`.
diff --git a/lib/serialization/inspect.nit b/lib/serialization/inspect.nit
new file mode 100644 (file)
index 0000000..ec5d466
--- /dev/null
@@ -0,0 +1,294 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Refine `Serializable::inspect` to show more useful information
+module inspect
+
+import serialization_core
+private import caching
+
+private fun inspect_testing: Bool do return "NIT_TESTING".environ == "true"
+
+# Serialization engine writing the object attributes to strings
+private class InspectSerializer
+       super CachingSerializer
+
+       # Target writing stream
+       var stream: Writer
+
+       redef var current_object = null
+
+       var first_object: nullable Object = null
+
+       redef fun serialize(object)
+       do
+               if object == null then
+                       stream.write "null"
+               else
+                       if current_object == null then
+                               first_object = object
+                       end
+
+                       var last_object = current_object
+                       current_object = object
+                       object.accept_inspect_serializer self
+                       current_object = last_object
+               end
+       end
+
+       var first_attribute_serialized = false
+
+       redef fun serialize_attribute(name, value)
+       do
+               if first_attribute_serialized then
+                       stream.write ", "
+               else
+                       stream.write " "
+                       first_attribute_serialized = true
+               end
+
+               stream.write name
+               stream.write ":"
+
+               super
+       end
+
+       redef fun serialize_reference(object)
+       do
+               if cache.has_object(object) then
+                       # Cycle
+                       var id = object.object_id
+                       if inspect_testing then id = cache.id_for(object)
+
+                       stream.write "<"
+                       stream.write object.class_name
+                       stream.write "#"
+                       stream.write id.to_s
+                       stream.write ">"
+               else if object != first_object and (not object isa DirectSerializable) then
+                       # Another object, print class and id only
+                       var id = object.object_id
+                       if inspect_testing then id = cache.new_id_for(object)
+
+                       stream.write "<"
+                       stream.write object.class_name
+                       stream.write "#"
+                       stream.write id.to_s
+                       stream.write ">"
+               else
+                       # Main object
+                       serialize object
+               end
+       end
+end
+
+redef class Serializable
+
+       # Improve the default inspection reading serializable attributes
+       #
+       # Simple immutable data are inspected as they would be written in Nit code.
+       #
+       # ~~~
+       # assert 123.inspect == "123"
+       # assert 1.5.inspect == "1.5"
+       # assert 0xa1u8.inspect == "0xa1u8"
+       # assert 'c'.inspect == "'c'"
+       # assert "asdf\n".inspect == "\"asdf\\n\""
+       # ~~~
+       #
+       # Inspections of mutable serializable objects show their dynamic type,
+       # their `object_id` and their first level attributes. When testing,
+       # the `object_id` is replaced by an id unique to each call to `inspect`.
+       #
+       # ~~~
+       # class MyClass
+       #     serialize
+       #
+       #     var i: Int
+       #     var o: nullable Object
+       # end
+       #
+       # var class_with_null = new MyClass(123)
+       # assert class_with_null.to_s == class_with_null.inspect
+       # assert class_with_null.to_s == "<MyClass#0 i:123, o:null>"
+       #
+       # var class_with_other = new MyClass(456, class_with_null)
+       # assert class_with_other.to_s == "<MyClass#0 i:456, o:<MyClass#1>>"
+       #
+       # var class_with_cycle = new MyClass(789)
+       # class_with_cycle.o = class_with_cycle
+       # assert class_with_cycle.to_s == "<MyClass#0 i:789, o:<MyClass#0>>"
+       # ~~~
+       #
+       # Items of collections are flattened and appended to the output.
+       #
+       # ~~~
+       # assert [1, 2, 3].inspect == "<Array[Int]#0 [1, 2, 3]>"
+       #
+       # var set = new HashSet[Object].from([1, 1.5, "two": Object])
+       # assert set.inspect == """<HashSet[Object]#0 [1, 1.5, "two"]>"""
+       #
+       # var map = new Map[Int, String]
+       # map[1] = "one"
+       # map[2] = "two"
+       # assert map.inspect == """<HashMap[Int, String]#0 {1:"one", 2:"two"}>"""
+       # ~~~
+       #
+       # Inspections producing over 80 characters are cut short.
+       #
+       # ~~~
+       # var long_class = new MyClass(123456789, "Some " + "very "*8 + "long string")
+       # assert long_class.to_s == "<MyClass#0 i:123456789, o:\"Some very very very very very very very very long s…>"
+       # ~~~
+       redef fun inspect
+       do
+               var stream = new StringWriter
+               var serializer = new InspectSerializer(stream)
+               serializer.serialize self
+               stream.close
+               var str = stream.to_s
+
+               # Cut long inspects
+               var max_length = 80
+               if str.length > max_length then
+                       str = str.substring(0, max_length-2) + "…>"
+               end
+
+               return str
+       end
+
+       private fun accept_inspect_serializer(v: InspectSerializer)
+       do
+               v.stream.write "<"
+
+               v.stream.write class_name
+               v.stream.write "#"
+
+               var id = object_id
+               if inspect_testing then id = v.cache.new_id_for(self)
+               v.stream.write id.to_s
+
+               accept_inspect_serializer_core v
+
+               v.stream.write ">"
+       end
+
+       private fun accept_inspect_serializer_core(v: InspectSerializer)
+       do v.serialize_core(self)
+end
+
+redef class Int
+       redef fun accept_inspect_serializer(v) do v.stream.write to_s
+end
+
+redef class Float
+       redef fun accept_inspect_serializer(v) do v.stream.write to_s
+end
+
+redef class Bool
+       redef fun accept_inspect_serializer(v) do v.stream.write to_s
+end
+
+redef class Char
+       redef fun accept_inspect_serializer(v)
+       do
+               v.stream.write "'"
+               v.stream.write to_s.escape_to_nit
+               v.stream.write "'"
+       end
+end
+
+redef class Byte
+       redef fun accept_inspect_serializer(v)
+       do
+               v.stream.write to_s
+               v.stream.write "u8"
+       end
+end
+
+redef class CString
+       redef fun accept_inspect_serializer_core(v)
+       do
+               v.stream.write " \""
+               v.stream.write to_s.escape_to_nit
+               v.stream.write_char '"'
+       end
+end
+
+redef class Text
+
+       redef fun accept_inspect_serializer(v)
+       do
+               v.stream.write "\""
+               v.stream.write escape_to_nit
+               v.stream.write "\""
+       end
+end
+
+redef class Collection[E]
+       private fun serialize_as_inspect(v: InspectSerializer)
+       do
+               v.stream.write "["
+               var is_first = true
+               for e in self do
+                       if is_first then
+                               is_first = false
+                       else
+                               v.stream.write ", "
+                       end
+
+                       if not v.try_to_serialize(e) then
+                               assert e != null
+                               v.stream.write e.inspect
+                       end
+               end
+               v.stream.write "]"
+       end
+end
+
+redef class SimpleCollection[E]
+       redef fun accept_inspect_serializer_core(v)
+       do
+               v.stream.write " "
+               serialize_as_inspect v
+       end
+end
+
+redef class Map[K, V]
+       redef fun accept_inspect_serializer_core(v)
+       do
+               v.stream.write " \{"
+
+               var first = true
+               for key, val in self do
+                       if not first then
+                               v.stream.write ", "
+                       else first = false
+
+                       if not v.try_to_serialize(key) then
+                               assert key != null
+                               v.stream.write key.inspect
+                       end
+
+                       v.stream.write ":"
+
+                       if not v.try_to_serialize(val) then
+                               assert val != null
+                               v.stream.write val.inspect
+                       end
+               end
+
+               v.stream.write "\}"
+       end
+end
index 3feb4ee..cdbaed4 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Abstract services to serialize Nit objects to different formats
-#
-# This module declares the `serialize` annotation to mark Nit classes as serializable.
-# For an introduction to this service, refer to the documentation of the `serialization` group.
-# This documentation provides more technical information on interesting entitie of this module.
-#
-# Interesting entities for end users of serializable classes:
-#
-# * Serialize an instance subclass of `Serializable` with either
-#   `Serializer::serializable` and `Serializable::serialize`.
-# * Deserialize an object using `Deserializer::deserialize`.
-#   The object type must the be checked with an `assert` or otherwise.
-#
-# Interesting entities to create custom serializable classes:
-#
-# * Subclass `Serializable` to declare a class as serializable and to customize
-#   the serialization and deserialization behavior.
-# * Redefine `Serializable::core_serialize_to` to customize the serialization
-#   of the receiver class.
-# * Redefine `Deserializer::deserialize_class` to customize the deserialization
-#   of a specific class by name.
-#
-# Interesting entities for serialization format:
-#
-# * Subclass `Serializer` and `Deserializer` with custom serices.
-# * In `Serializer`, `serialize` and `serialize_reference` must be redefined.
-# * In `Deserializer`; `deserialize`, `deserialize_attribute and
-#   `notify_of_creation` must be redefined.
-module serialization is
-       new_annotation auto_serializable
-       new_annotation serialize
-       new_annotation noserialize
-       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`
-       #
-       # This method, and refinements, should handle `null` and probably
-       # use double dispatch to customize the bahavior per serializable objects.
-       fun serialize(object: nullable Serializable) is abstract
-
-       # The object currently serialized by `serialized`
-       #
-       # Can be used by a custom serializer to add domain-specific serialization behavior.
-       protected fun current_object: nullable Object is abstract
-
-       # Serialize an object, with full serialization or a simple reference
-       protected fun serialize_reference(object: Serializable) is abstract
-
-       # Serialize an attribute to compose a serializable object
-       #
-       # This method should be called from `Serializable::core_serialize_to`.
-       fun serialize_attribute(name: String, value: nullable Object)
-       do
-               if not try_to_serialize(value) then
-                       assert value != null # null would have been serialized
-                       warn("argument {name} of type {value.class_name} is not serializable.")
-               end
-       end
-
-       # Serialize `value` is possible, i.e. it is `Serializable` or `null`
-       fun try_to_serialize(value: nullable Object): Bool
-       do
-               if value isa Serializable then
-                       value.serialize_to_or_delay(self)
-               else if value == null then
-                       serialize value
-               else return false
-               return true
-       end
-
-       # The method is called when a standard `value` is serialized
-       #
-       # The default behavior is to call `value.core_serialize_to(self)` but it
-       # can be redefined by a custom serializer to add domain-specific serialization behavior.
-       fun serialize_core(value: Serializable)
-       do
-               value.core_serialize_to(self)
-       end
-
-       # Warn of problems and potential errors (such as if an attribute
-       # is not serializable)
-       fun warn(msg: String) do print "Serialization warning: {msg}"
-end
-
-# Abstract deserialization service
-#
-# The main service is `deserialize`.
-abstract class Deserializer
-       # Deserialize and return an object, storing errors in the attribute `errors`
-       #
-       # If a `static_type` is given, only subtypes of the `static_type` are accepted.
-       #
-       # This method behavior varies according to the implementation engines.
-       fun deserialize(static_type: nullable String): nullable Object is abstract
-
-       # Deserialize the attribute with `name` from the object open for deserialization
-       #
-       # 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.
-       #
-       # Internal method to be implemented by the engines.
-       fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract
-
-       # Was the attribute queried by the last call to `deserialize_attribute` missing?
-       var deserialize_attribute_missing = false
-
-       # Register a newly allocated object (even if not completely built)
-       #
-       # Internal method called by objects in creation, to be implemented by the engines.
-       fun notify_of_creation(new_object: Object) is abstract
-
-       # Deserialize the next available object as an instance of `class_name`
-       #
-       # Return the deserialized object on success and
-       # record in `errors` if `class_name` is unknown.
-       #
-       # This method should be redefined for each custom subclass of `Serializable`.
-       # All refinement should look for a precise `class_name` and call super
-       # on unsupported classes.
-       protected fun deserialize_class(class_name: Text): nullable Object do
-               if class_name == "Error" then return new Error.from_deserializer(self)
-               return deserialize_class_intern(class_name)
-       end
-
-       # Generated service to deserialize the next available object as an instance of `class_name`
-       #
-       # Refinements to this method will be generated by the serialization phase.
-       # To avoid conflicts, there should not be any other refinements to this method.
-       # You can instead use `deserialize_class`.
-       protected fun deserialize_class_intern(class_name: Text): nullable Object do
-               errors.add new Error("Deserialization Error: Doesn't know how to deserialize class \"{class_name}\"")
-               return null
-       end
-
-       # Should `self` keep trying to deserialize an object after an error?
-       #
-       # This behavior takes effect after each attribute deserialization with
-       # errors such as a missing attribute or the value is of the wrong type.
-       # If `keep_going`, the attribute will be skipped but the engine will
-       # deserialize the next attribute.
-       # If `not keep_going`, the engine stops deserializing right away.
-       #
-       # When at `true`, this may cause the accumulation of a lot of entries in `errors`.
-       #
-       # Default at `true`.
-       var keep_going: nullable Bool = null is writable
-
-       # Errors encountered in the last call to `deserialize`
-       var errors = new Array[Error]
-end
-
-# Deserialization error related to an attribute of `receiver`
-abstract class AttributeError
-       super Error
-
-       # 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
-
-       # Name of the type expected for `attribute`
-       var expected_type: String
-
-       redef var message is lazy do
-               var attribute = attribute
-               var found_type = if attribute != null then attribute.class_name else "null"
-
-               return "Deserialization Error: {
-               }Wrong type on `{receiver.class_name}::{attribute_name}` expected `{expected_type}`, got `{found_type}`"
-       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`
-       #
-       # This is a shortcut to `Serializer::serialize`.
-       fun serialize_to(serializer: Serializer) do serializer.serialize(self)
-
-       # Actual serialization of `self` to `serializer`
-       #
-       # This writes the full data of `self` to `serializer`.
-       #
-       # This method can be redefined in sub classes and refinements.
-       # It should use `Serializer::serialize_attribute` to to register real or
-       # logical attributes.
-       #
-       # Any refinement should have its equivalent refinement of
-       # `Deserializer::deserialize_class` to support this custom deserialization.
-       fun core_serialize_to(serializer: Serializer) do end
-
-       # Accept references or force direct serialization (using `serialize_to`)
-       #
-       # The subclass change the default behavior, which will accept references,
-       # to force to always serialize copies of `self`.
-       private fun serialize_to_or_delay(v: Serializer) do v.serialize_reference(self)
-
-       # Create an instance of this class from the `deserializer`
-       #
-       # This constructor is refined by subclasses to correctly build their instances.
-       init from_deserializer(deserializer: Deserializer) is nosuper do end
-end
-
-# Instances of this class are not delayed and instead serialized immediately
-# This applies mainly to `universal` types
-interface DirectSerializable
-       super Serializable
-
-       redef fun serialize_to_or_delay(v) do serialize_to(v)
-end
-
-redef class Bool super DirectSerializable end
-redef class Char super DirectSerializable end
-redef class Byte super DirectSerializable end
-redef class Int super DirectSerializable end
-redef class Float super DirectSerializable end
-redef class CString super DirectSerializable end
-redef class Text super DirectSerializable end
-redef class SimpleCollection[E] super Serializable end
-redef class Map[K, V] super Serializable end
-
-redef class Couple[F, S]
-       super Serializable
-
-       redef init from_deserializer(v)
-       do
-               v.notify_of_creation self
-               var first = v.deserialize_attribute("first")
-               var second = v.deserialize_attribute("second")
-               init(first, second)
-       end
-
-       redef fun core_serialize_to(v)
-       do
-               v.serialize_attribute("first", first)
-               v.serialize_attribute("second", second)
-       end
-end
-
-redef class Ref[E]
-       super Serializable
-
-       redef init from_deserializer(v)
-       do
-               v.notify_of_creation self
-               var item = v.deserialize_attribute("item")
-               init item
-       end
-
-       redef fun core_serialize_to(v)
-       do
-               v.serialize_attribute("item", first)
-       end
-end
-
-redef class Error
-       super Serializable
-
-       redef init from_deserializer(v)
-       do
-               v.notify_of_creation self
-
-               var message = v.deserialize_attribute("message")
-               if not message isa String then message = ""
-               init message
-
-               var cause = v.deserialize_attribute("cause")
-               if cause isa nullable Error then self.cause = cause
-       end
-
-       redef fun core_serialize_to(v)
-       do
-               v.serialize_attribute("message", message)
-               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
+# General serialization services
+module serialization
 
-       redef fun core_serialize_to(v)
-       do
-               v.serialize_attribute("items", items)
-               v.serialize_attribute("comparator", comparator)
-       end
-end
+import serialization_core
+import inspect
diff --git a/lib/serialization/serialization_core.nit b/lib/serialization/serialization_core.nit
new file mode 100644 (file)
index 0000000..a19cfca
--- /dev/null
@@ -0,0 +1,390 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Abstract services to serialize Nit objects to different formats
+#
+# This module declares the `serialize` annotation to mark Nit classes as serializable.
+# For an introduction to this service, refer to the documentation of the `serialization` group.
+# This documentation provides more technical information on interesting entitie of this module.
+#
+# Interesting entities for end users of serializable classes:
+#
+# * Serialize an instance subclass of `Serializable` with either
+#   `Serializer::serializable` and `Serializable::serialize`.
+# * Deserialize an object using `Deserializer::deserialize`.
+#   The object type must the be checked with an `assert` or otherwise.
+#
+# Interesting entities to create custom serializable classes:
+#
+# * Subclass `Serializable` to declare a class as serializable and to customize
+#   the serialization and deserialization behavior.
+# * Redefine `Serializable::core_serialize_to` to customize the serialization
+#   of the receiver class.
+# * Redefine `Deserializer::deserialize_class` to customize the deserialization
+#   of a specific class by name.
+#
+# Interesting entities for serialization format:
+#
+# * Subclass `Serializer` and `Deserializer` with custom serices.
+# * In `Serializer`, `serialize` and `serialize_reference` must be redefined.
+# * In `Deserializer`; `deserialize`, `deserialize_attribute and
+#   `notify_of_creation` must be redefined.
+module serialization_core is
+       new_annotation auto_serializable
+       new_annotation serialize
+       new_annotation noserialize
+       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`
+       #
+       # This method, and refinements, should handle `null` and probably
+       # use double dispatch to customize the bahavior per serializable objects.
+       fun serialize(object: nullable Serializable) is abstract
+
+       # The object currently serialized by `serialized`
+       #
+       # Can be used by a custom serializer to add domain-specific serialization behavior.
+       protected fun current_object: nullable Object is abstract
+
+       # Serialize an object, with full serialization or a simple reference
+       protected fun serialize_reference(object: Serializable) is abstract
+
+       # Serialize an attribute to compose a serializable object
+       #
+       # This method should be called from `Serializable::core_serialize_to`.
+       fun serialize_attribute(name: String, value: nullable Object)
+       do
+               if not try_to_serialize(value) then
+                       assert value != null # null would have been serialized
+                       warn("argument {name} of type {value.class_name} is not serializable.")
+               end
+       end
+
+       # Serialize `value` is possible, i.e. it is `Serializable` or `null`
+       fun try_to_serialize(value: nullable Object): Bool
+       do
+               if value isa Serializable then
+                       value.serialize_to_or_delay(self)
+               else if value == null then
+                       serialize value
+               else return false
+               return true
+       end
+
+       # The method is called when a standard `value` is serialized
+       #
+       # The default behavior is to call `value.core_serialize_to(self)` but it
+       # can be redefined by a custom serializer to add domain-specific serialization behavior.
+       fun serialize_core(value: Serializable)
+       do
+               value.core_serialize_to(self)
+       end
+
+       # Warn of problems and potential errors (such as if an attribute
+       # is not serializable)
+       fun warn(msg: String) do print "Serialization warning: {msg}"
+end
+
+# Abstract deserialization service
+#
+# The main service is `deserialize`.
+abstract class Deserializer
+       # Deserialize and return an object, storing errors in the attribute `errors`
+       #
+       # If a `static_type` is given, only subtypes of the `static_type` are accepted.
+       #
+       # This method behavior varies according to the implementation engines.
+       fun deserialize(static_type: nullable String): nullable Object is abstract
+
+       # Deserialize the attribute with `name` from the object open for deserialization
+       #
+       # 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.
+       #
+       # Internal method to be implemented by the engines.
+       fun deserialize_attribute(name: String, static_type: nullable String): nullable Object is abstract
+
+       # Was the attribute queried by the last call to `deserialize_attribute` missing?
+       var deserialize_attribute_missing = false
+
+       # Register a newly allocated object (even if not completely built)
+       #
+       # Internal method called by objects in creation, to be implemented by the engines.
+       fun notify_of_creation(new_object: Object) is abstract
+
+       # Deserialize the next available object as an instance of `class_name`
+       #
+       # Return the deserialized object on success and
+       # record in `errors` if `class_name` is unknown.
+       #
+       # This method should be redefined for each custom subclass of `Serializable`.
+       # All refinement should look for a precise `class_name` and call super
+       # on unsupported classes.
+       protected fun deserialize_class(class_name: Text): nullable Object do
+               if class_name == "Error" then return new Error.from_deserializer(self)
+               return deserialize_class_intern(class_name)
+       end
+
+       # Generated service to deserialize the next available object as an instance of `class_name`
+       #
+       # Refinements to this method will be generated by the serialization phase.
+       # To avoid conflicts, there should not be any other refinements to this method.
+       # You can instead use `deserialize_class`.
+       protected fun deserialize_class_intern(class_name: Text): nullable Object do
+               errors.add new Error("Deserialization Error: Doesn't know how to deserialize class \"{class_name}\"")
+               return null
+       end
+
+       # Should `self` keep trying to deserialize an object after an error?
+       #
+       # This behavior takes effect after each attribute deserialization with
+       # errors such as a missing attribute or the value is of the wrong type.
+       # If `keep_going`, the attribute will be skipped but the engine will
+       # deserialize the next attribute.
+       # If `not keep_going`, the engine stops deserializing right away.
+       #
+       # When at `true`, this may cause the accumulation of a lot of entries in `errors`.
+       #
+       # Default at `true`.
+       var keep_going: nullable Bool = null is writable
+
+       # Errors encountered in the last call to `deserialize`
+       var errors = new Array[Error]
+end
+
+# Deserialization error related to an attribute of `receiver`
+abstract class AttributeError
+       super Error
+
+       # 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
+
+       # Name of the type expected for `attribute`
+       var expected_type: String
+
+       redef var message is lazy do
+               var attribute = attribute
+               var found_type = if attribute != null then attribute.class_name else "null"
+
+               return "Deserialization Error: {
+               }Wrong type on `{receiver.class_name}::{attribute_name}` expected `{expected_type}`, got `{found_type}`"
+       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`
+       #
+       # This is a shortcut to `Serializer::serialize`.
+       fun serialize_to(serializer: Serializer) do serializer.serialize(self)
+
+       # Actual serialization of `self` to `serializer`
+       #
+       # This writes the full data of `self` to `serializer`.
+       #
+       # This method can be redefined in sub classes and refinements.
+       # It should use `Serializer::serialize_attribute` to to register real or
+       # logical attributes.
+       #
+       # Any refinement should have its equivalent refinement of
+       # `Deserializer::deserialize_class` to support this custom deserialization.
+       fun core_serialize_to(serializer: Serializer) do end
+
+       # Accept references or force direct serialization (using `serialize_to`)
+       #
+       # The subclass change the default behavior, which will accept references,
+       # to force to always serialize copies of `self`.
+       private fun serialize_to_or_delay(v: Serializer) do v.serialize_reference(self)
+
+       # Create an instance of this class from the `deserializer`
+       #
+       # This constructor is refined by subclasses to correctly build their instances.
+       init from_deserializer(deserializer: Deserializer) is nosuper do end
+end
+
+# Instances of this class are not delayed and instead serialized immediately
+# This applies mainly to `universal` types
+interface DirectSerializable
+       super Serializable
+
+       redef fun serialize_to_or_delay(v) do serialize_to(v)
+end
+
+redef class Bool super DirectSerializable end
+redef class Char super DirectSerializable end
+redef class Byte super DirectSerializable end
+redef class Int super DirectSerializable end
+redef class Float super DirectSerializable end
+redef class CString super DirectSerializable end
+redef class Text super DirectSerializable end
+redef class SimpleCollection[E] super Serializable end
+redef class Map[K, V] super Serializable end
+
+redef class Couple[F, S]
+       super Serializable
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+               var first = v.deserialize_attribute("first")
+               var second = v.deserialize_attribute("second")
+               init(first, second)
+       end
+
+       redef fun core_serialize_to(v)
+       do
+               v.serialize_attribute("first", first)
+               v.serialize_attribute("second", second)
+       end
+end
+
+redef class Ref[E]
+       super Serializable
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+               var item = v.deserialize_attribute("item")
+               init item
+       end
+
+       redef fun core_serialize_to(v)
+       do
+               v.serialize_attribute("item", first)
+       end
+end
+
+redef class Error
+       super Serializable
+
+       redef init from_deserializer(v)
+       do
+               v.notify_of_creation self
+
+               var message = v.deserialize_attribute("message")
+               if not message isa String then message = ""
+               init message
+
+               var cause = v.deserialize_attribute("cause")
+               if cause isa nullable Error then self.cause = cause
+       end
+
+       redef fun core_serialize_to(v)
+       do
+               v.serialize_attribute("message", message)
+               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
diff --git a/tests/sav/nitce/test_inspect_serialization.res b/tests/sav/nitce/test_inspect_serialization.res
new file mode 100644 (file)
index 0000000..2ee51e5
--- /dev/null
@@ -0,0 +1,49 @@
+# Custom:
+<A: true a 0.123 1234 asdf false p4ssw0rd>
+
+# Inspect:
+<A#0 b:true, c:'a', f:0.123, i:1234, serialization_specific_name:"asdf", n:null>
+
+# Custom:
+<B: <A: false b 123.123 2345 hjkl true p4ssw0rd> 1111 qwer>
+
+# Inspect:
+<B#0 b:false, c:'b', f:123.123, i:2345, serialization_specific_name:"hjkl", n:…>
+
+# Custom:
+<C: <A: true a 0.123 1234 asdf false p4ssw0rd> <B: <A: false b 123.123 2345 hjkl true p4ssw0rd> 1111 qwer>>
+
+# Inspect:
+<C#0 a:<A#1>, b:<B#2>, aa:<A#1>>
+
+# Custom:
+<D: <B: <A: false b 123.123 2345 new line ->
+<- false p4ssw0rd> 1111        f"\r\/> true>
+
+# Inspect:
+<D#0 b:false, c:'b', f:123.123, i:2345, serialization_specific_name:"new line …>
+
+# Custom:
+<E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
+
+# Inspect:
+<E#0 a:<Array#1>, b:<Array#2>>
+
+# Custom:
+<F: 2222>
+
+# Inspect:
+<F#0 n:2222>
+
+# Custom:
+<F: 33.33>
+
+# Inspect:
+<F#0 n:33.33>
+
+# Custom:
+<G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
+
+# Inspect:
+<G#0 hs:<HashSet#1>, s:<ArraySet#2>, hm:<HashMap#3>, am:<ArrayMap#4>>
+
index f8172b0..6658182 100644 (file)
@@ -19,6 +19,7 @@ redef class Deserializer
                if name == "Array[Error]" then return new Array[Error].from_deserializer(self)
                if name == "StrictHashMap[Int, Object]" then return new StrictHashMap[Int, Object].from_deserializer(self)
                if name == "POSet[String]" then return new POSet[String].from_deserializer(self)
+               if name == "StrictHashMap[Serializable, Int]" then return new StrictHashMap[Serializable, Int].from_deserializer(self)
                if name == "Array[Int]" then return new Array[Int].from_deserializer(self)
                if name == "Array[nullable Object]" then return new Array[nullable Object].from_deserializer(self)
                if name == "HashSet[String]" then return new HashSet[String].from_deserializer(self)
@@ -29,7 +30,6 @@ 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)
diff --git a/tests/sav/test_inspect_serialization.res b/tests/sav/test_inspect_serialization.res
new file mode 100644 (file)
index 0000000..313b090
--- /dev/null
@@ -0,0 +1,49 @@
+# Custom:
+<A: true a 0.123 1234 asdf false p4ssw0rd>
+
+# Inspect:
+<A#0 b:true, c:'a', f:0.123, i:1234, serialization_specific_name:"asdf", n:null>
+
+# Custom:
+<B: <A: false b 123.123 2345 hjkl true p4ssw0rd> 1111 qwer>
+
+# Inspect:
+<B#0 b:false, c:'b', f:123.123, i:2345, serialization_specific_name:"hjkl", n:…>
+
+# Custom:
+<C: <A: true a 0.123 1234 asdf false p4ssw0rd> <B: <A: false b 123.123 2345 hjkl true p4ssw0rd> 1111 qwer>>
+
+# Inspect:
+<C#0 a:<A#1>, b:<B#2>, aa:<A#1>>
+
+# Custom:
+<D: <B: <A: false b 123.123 2345 new line ->
+<- false p4ssw0rd> 1111        f"\r\/> true>
+
+# Inspect:
+<D#0 b:false, c:'b', f:123.123, i:2345, serialization_specific_name:"new line …>
+
+# Custom:
+<E: a: hello, 1234, 123.4; b: hella, 2345, 234.5>
+
+# Inspect:
+<E#0 a:<Array[Object]#1>, b:<Array[nullable Serializable]#2>>
+
+# Custom:
+<F: 2222>
+
+# Inspect:
+<F[Int]#0 n:2222>
+
+# Custom:
+<F: 33.33>
+
+# Inspect:
+<F[Float]#0 n:33.33>
+
+# Custom:
+<G: hs: -1, 0; s: one, two; hm: one. 1, two. 2; am: three. 3, four. 4>
+
+# Inspect:
+<G#0 hs:<HashSet[Int]#1>, s:<ArraySet[String]#2>, hm:<HashMap[String, Int]#3>,…>
+
diff --git a/tests/test_inspect_serialization.nit b/tests/test_inspect_serialization.nit
new file mode 100644 (file)
index 0000000..42fd089
--- /dev/null
@@ -0,0 +1,23 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import test_deserialization
+
+var entities = new TestEntities
+var tests = entities.with_generics
+
+for o in tests do
+       print "# Custom:\n{o}\n"
+       print "# Inspect:\n{o.inspect}\n"
+end