Merge: Document Nit Serialization
authorJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:40:51 +0000 (12:40 +0700)
committerJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:40:51 +0000 (12:40 +0700)
The Nit serialization system did not have any documentation. This is the base to a better documentation.

In a next PR I may make more services of the serializers private. Implementations, such as json_serialization, will need to intrude import serialization. However, the end-user will only see the basic services.

Pull-Request: #1202
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Philippe Pépos Petitclerc <>
Reviewed-by: Frédéric Vachon <fredvac@gmail.com>

lib/json_serialization.nit
lib/serialization.nit [deleted file]
lib/serialization/README.md [new file with mode: 0644]
lib/serialization/serialization.nit [new file with mode: 0644]
tests/sav/nitg-e/test_deserialization.res
tests/sav/nitg-e/test_deserialization_serial.res
tests/sav/test_deserialization.res

index 4c552f6..e8c76b4 100644 (file)
@@ -102,7 +102,7 @@ class JsonDeserializer
 
                assert current.keys.has(name)
                var value = current[name]
-               
+
                return convert_object(value)
        end
 
@@ -164,7 +164,7 @@ class JsonDeserializer
                                assert val isa String
 
                                if val.length != 1 then print "Error: expected a single char when deserializing '{val}'."
-                               
+
                                return val.chars.first
                        end
 
diff --git a/lib/serialization.nit b/lib/serialization.nit
deleted file mode 100644 (file)
index 8a1d1ee..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# 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.
-
-# Offers services to serialize a Nit objects to different persistent formats
-module serialization is
-       new_annotation auto_serializable
-end
-
-# Abstract serialization service to be sub-classed by specialized services.
-interface Serializer
-       # Main method of this service, serialize the `object`
-       fun serialize(object: nullable Serializable) is abstract
-
-       # Serialize an object as a "possible" reference, depending of the service
-       fun serialize_reference(object: Serializable) is abstract
-
-       # Serialize an attribute, used by `Serializable::core_serialize_to`
-       fun serialize_attribute(name: String, value: nullable Object)
-       do
-               if not try_to_serialize(value) then
-                       warn("argument {value.class_name}::{name} is not serializable.")
-               end
-       end
-
-       # Serialize `value` is possie, 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
-
-       # 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
-#
-# After initialization of one of its sub-classes, call `deserialize`
-interface Deserializer
-       # Main method of this class, returns a Nit object
-       fun deserialize: nullable Object is abstract
-
-       # Internal method to be implemented by sub-classes
-       fun deserialize_attribute(name: String): nullable Object is abstract
-
-       # Internal method called by objects in creation,
-       # to be implemented by sub-classes
-       fun notify_of_creation(new_object: Object) is abstract
-
-       # Mainly generated method to return the next instance of the givent
-       # class by name
-       fun deserialize_class(class_name: String): Object do
-               print "Error: doesn't know how to deserialize class \"{class_name}\""
-               abort
-       end
-end
-
-# Instances of this class can be passed to `Serializer::serialize`
-interface Serializable
-       # Full or true serialization
-       fun serialize_to(v: Serializer) do v.serialize(self)
-
-       # Body of the serialization of this class
-       # Can be redefed in sub classes and refinements
-       fun core_serialize_to(v: Serializer) do end
-
-       # Whether full serialization (calls `serialize_to`) or place only references
-       fun serialize_to_or_delay(v: Serializer) do v.serialize_reference(self)
-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 Int super DirectSerializable end
-redef class Float super DirectSerializable end
-redef class NativeString super DirectSerializable end
-redef class String super DirectSerializable end
-redef class Array[E] super Serializable end
diff --git a/lib/serialization/README.md b/lib/serialization/README.md
new file mode 100644 (file)
index 0000000..4cbcf64
--- /dev/null
@@ -0,0 +1,254 @@
+# Abstract serialization services
+
+The serialization services are centered around the `auto_serializable` annotation,
+the `Serializable` interface and the implementations of `Serializer` and `Deserializer`.
+
+## The `auto_serializable` annotation
+
+A class annotated with `auto_serializable` identifies it as a subclass of Serializable and
+triggers the generation of customized serialization and deserialization services.
+
+~~~
+import serialization
+
+# Simple serializable class identifying a human
+class Person
+       auto_serializable
+
+       # First and last name
+       var name: String
+
+       # Year of birth (`null` if unknown)
+       var birth: nullable Int
+
+       redef fun ==(o) do return o isa SELF and name == o.name and birth == o.birth
+       redef fun hash do return name.hash
+end
+~~~
+
+The `Person` class also defines `==` and `hash`, this is optional but we will use it to make an important point.
+By definition of a serializable class, an instance can be serialized to a stream, then deserialized.
+The deserialized instance will not be the same instance, but they should be equal.
+So, in this case, we can compare both instances with `==` to test their equality.
+
+Some conditions applies to the classes that can be annotated as `auto_serializable`.
+All attributes of the class must be serializable, runtime errors will be
+raised when trying to serialize non-serializable attributes.
+
+In the class `Person`, all attributes are typed with classes the standards library.
+These common types are defined defined as serializable by this project.
+The attributes could also be typed with user-defined `auto_serializable`
+classes or any other subclass of `Serializable`.
+
+~~~
+# This `auto_serializable` class is composed of two `auto_serializable` attributes
+class Partnership
+       auto_serializable
+
+       var partner_a: Person
+       var partner_b: Person
+
+       redef fun ==(o) do return o isa SELF and partner_a == o.partner_a and partner_b == o.partner_b
+       redef fun hash do return partner_a.hash + 1024*partner_b.hash
+end
+~~~
+
+The `auto_serializable` applies only to the class definition,
+only attributes declared locally will be serialized.
+However, each definition of a class (a refinement or specialization)
+can declare `auto_serializable`.
+
+## Custom serializable classes
+
+The annotation `auto_serializable` should be enough for most cases,
+but in some cases you need more control over the serialization process.
+
+For more control, create a subclass to `Serializable` and redefine `core_serialize_to`.
+This method should use `Serializer::serialize_attribute` to serialize its components.
+`serialize_attribute` works as a dictionary and organize attributes with a key.
+
+You will also need to redefine `Deserializer::deserialize_class` to support this specific class.
+The method should only act on known class names, and call super otherwise.
+
+### Example: the User class
+
+The following example cannot use the `auto_serializable` annotations
+because some of the arguments to the `User` class need special treatment:
+
+* The `name` attribute is perfectly normal, it can be serialized and deserialized
+  directly.
+
+* The `password` attribute must be encrypted before being serialized,
+  and unencrypted on deserialization.
+
+* The `avatar` attributes is kept as ASCII art in memory.
+  It could be serialized as such but it is cleaner to only
+  serialize the path to its source on the file system.
+  The data is reloaded on deserialization.
+
+For this customization, the following code snippet implements
+two serialization services: `User::core_serialize_to` and
+`Deserializer::deserialize_class`.
+
+~~~
+module user_credentials
+
+# User credentials for a website
+class User
+       super Serializable
+
+       # User name
+       var name: String
+
+       # Clear text password
+       var password: String
+
+       # User's avatar image as data blob
+       var avatar: Image
+
+       redef fun core_serialize_to(serializer: Serializer)
+       do
+               # This is the normal serialization process
+               serializer.serialize_attribute("name", name)
+
+               # Serialized an encrypted version of the password
+               #
+               # Obviously, `rot(13)` is not a good encrption
+               serializer.serialize_attribute("pass", password.rot(13))
+
+               # Do not serialize the image, only its path
+               serializer.serialize_attribute("avatar_path", avatar.path)
+       end
+end
+
+redef class Deserializer
+       redef fun deserialize_class(name)
+       do
+               if name == "User" then
+                       # Deserialize normally
+                       var user = deserialize_attribute("name")
+
+                       # Decrypt password
+                       var pass = deserialize_attribute("pass").rot(-13)
+
+                       # Deserialize the path and load the avatar from the file system
+                       var avatar_path = deserialize_attribute("avatar_path")
+                       var avatar = new Image(avatar_path)
+
+                       return new User(user, pass, avatar)
+               end
+
+               return super
+       end
+end
+
+# An image loaded in memory as ASCII art
+#
+# Not really useful for this example, provided for consistency only.
+class Image
+       # Path on the filesystem for `self`
+       var path: String
+
+       # ASCII art composing this image
+       var ascii_art: String = path.read_all is lazy
+end
+
+~~~
+
+See the documentation of the module `serialization::serialization` for more
+information on the services to redefine.
+
+## Serialization services
+
+The `auto_serializable` annotation and the `Serializable` class are used on
+classes specific to the business domain.
+To write (and read) instances of these classes to a persistent format
+you must use implementations of `Serializer` and `Deserializer`.
+
+The main implementations of these services are `JsonSerializer` and `JsonDeserializer`,
+from the `json_serialization` module.
+
+~~~
+import json_serialization
+import user_credentials
+
+# Data to be serialized and deserialized
+var couple = new Partnership(
+       new Person("Alice", 1985, new Image("alice.png")),
+       new Person("Bob", null, new Image("bob.png")))
+
+var path = "serialized_data.json"
+var writer = new FileWriter(path)
+var serializer = new JsonSerializer(writer)
+serializer.serialize couple
+writer.close
+
+var reader = new FileReader(path)
+var deserializer = new JsonDeserializer(reader.to_s)
+var deserialized_couple = deserializer.deserialize
+reader.close
+
+assert couple == deserialize_couple
+~~~
+
+## Limitations and TODO
+
+The serialization has some limitations:
+
+* Not enough classes from the standard library are supported.
+  This only requires someone to actually code the support.
+  It should not be especially hard for most classes, some can
+  simply declare the `auto_serializable` annotation.
+
+* A limitation of the Json parser prevents deserializing from files
+  with more than one object.
+  This could be improved in the future, but for now you should
+  serialize a single object to each filesand use different instances of
+  serializer and deserializer each time.
+
+* The `auto_serializable` annotation does not handle very well
+  complex constructors. This could be improved in the compiler.
+  For now, you may prefer to use `auto_serializable` on simple classes,
+  of by using custom `Serializable`.
+
+* The serialization uses only the short name of a class, not its qualified name.
+  This will cause problem when different classes using the same name.
+  This could be solved partially in the compiler and the library.
+  A special attention must be given to the consistency of the name across
+  the different programs sharing the serialized data.
+
+* The serialization support in the compiler need some help to
+  deal with generic types. The solution is to use `nitserial`,
+  the next section explores this subject.
+
+## Dealing with generic types
+
+One limitation of the serialization support in the compiler is with generic types.
+For example, the `Array` class is generic and serializable.
+However, the runtime types of Array instances are parameterized and are unknown to the compiler.
+So the compiler won't support serializing instances of `Array[MySerializable]`.
+
+The tool `nitserial` solves this problem at the level of user modules.
+It does so by parsing a Nit module, group or project to find all known
+parameterized types of generic classes.
+It will then generating a Nit module to handle deserialization of these types.
+
+Usage steps to serialize parameterized types:
+
+* Write your program, let's call it `my_prog.nit`,
+  it must use some parameterized serializable types.
+  Let's say that you use `Array[MySerializable]`.
+
+* Run nitserial using `nitserial my_prog.nit` to
+  generate the file `my_prog_serial.nit`.
+
+* Compile your program by mixing in the generated module with:
+  `nitc my_prog.nit -m my_prog_serial.nit`
+
+This was a simple example, in practical cases you may need
+to use more than one generated file.
+For example, on a client/server system, an instance can be created
+server-side, serialized and the used client-side.
+In this case, two files will be generated by nitserial,
+one for the server and one for the client.
+Both the files should be compiled with both the client and the server.
diff --git a/lib/serialization/serialization.nit b/lib/serialization/serialization.nit
new file mode 100644 (file)
index 0000000..31f98f0
--- /dev/null
@@ -0,0 +1,153 @@
+# 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 `auto_serializable` 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
+end
+
+# 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
+
+       # 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
+                       warn("argument {value.class_name}::{name} is not serializable.")
+               end
+       end
+
+       # Serialize `value` is possie, 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
+
+       # 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
+#
+# After initialization of one of its sub-classes, call `deserialize`
+interface Deserializer
+       # Main method of this class, returns a Nit object
+       fun deserialize: nullable Object is abstract
+
+       # Internal method to be implemented by sub-classes
+       fun deserialize_attribute(name: String): nullable Object is abstract
+
+       # Internal method called by objects in creation,
+       # to be implemented by sub-classes
+       fun notify_of_creation(new_object: Object) is abstract
+
+       # Deserialize the next available object as an instance of `class_name`
+       #
+       # Returns the deserialized object on success, aborts on error.
+       #
+       # 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.
+       fun deserialize_class(class_name: String): Object do
+               print "Error: doesn't know how to deserialize class \"{class_name}\""
+               abort
+       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)
+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 Int super DirectSerializable end
+redef class Float super DirectSerializable end
+redef class NativeString super DirectSerializable end
+redef class String super DirectSerializable end
+redef class Array[E] super Serializable end
index 650bdaa..90e0d60 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>
 
index 650bdaa..90e0d60 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>
 
index 33ac09b..13f9a72 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>