Merge: Custom serializer
authorJean Privat <jean@pryen.org>
Tue, 16 May 2017 13:28:17 +0000 (09:28 -0400)
committerJean Privat <jean@pryen.org>
Tue, 16 May 2017 13:28:17 +0000 (09:28 -0400)
This is an old attempt to add the possibility to have a custom serializer. The idea is that a same set of objects can be serialized in different ways if different serializers are used.

Most of what is needed to do that is already present; the main contribution of this PR is the example that tries to propose a way to do that. The example focuses on 3 simples needs:

* inject a phantom attribute.
  when serializing, the custom serializer inject new attributes.
* hide a normally serialized attribute.
  when serializing, the custom serializer hides some specific attributes.
* replace a full business object.
  instead of serializing an attribute, the custom serializer use a different representation

Pull-Request: #2426
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/binary/serialization.nit
lib/json/serialization_write.nit
lib/serialization/examples/custom_serialization.nit [new file with mode: 0644]
lib/serialization/serialization.nit
tests/sav/custom_serialization.res [new file with mode: 0644]

index c99572e..ddb87ad 100644 (file)
@@ -70,6 +70,8 @@ class BinarySerializer
        # Target writing stream
        var stream: Writer is writable
 
+       redef var current_object = null
+
        redef fun serialize(object)
        do
                if object == null then
@@ -92,7 +94,10 @@ class BinarySerializer
                        stream.write_int64 id
                else
                        # serialize here
+                       var last_object = current_object
+                       current_object = object
                        object.serialize_to_binary self
+                       current_object = last_object
                end
        end
 
@@ -366,7 +371,7 @@ redef class Serializable
        private fun serialize_to_binary(v: BinarySerializer)
        do
                serialize_header_to_binary v
-               core_serialize_to v
+               v.serialize_core self
                v.stream.write_byte new_object_end
        end
 end
@@ -454,7 +459,7 @@ redef class Map[K, V]
        do
                serialize_header_to_binary v
 
-               core_serialize_to v
+               v.serialize_core self
 
                v.stream.write_string "keys"
                v.serialize_flat_array keys
index efbf22a..eeb30ff 100644 (file)
@@ -69,6 +69,8 @@ class JsonSerializer
        # Used only when `plain_json == true`.
        private var first_attribute = false
 
+       redef var current_object = null
+
        redef fun serialize(object)
        do
                if object == null then
@@ -88,8 +90,11 @@ class JsonSerializer
                        end
 
                        first_attribute = true
+                       var last_object = current_object
+                       current_object = object
                        object.accept_json_serializer self
                        first_attribute = false
+                       current_object = last_object
 
                        if plain_json then open_objects.pop
                end
@@ -248,7 +253,7 @@ redef class Serializable
                        v.stream.write class_name
                        v.stream.write "\""
                end
-               core_serialize_to(v)
+               v.serialize_core(self)
 
                v.indent_level -= 1
                v.new_line_and_indent
diff --git a/lib/serialization/examples/custom_serialization.nit b/lib/serialization/examples/custom_serialization.nit
new file mode 100644 (file)
index 0000000..083bcfb
--- /dev/null
@@ -0,0 +1,155 @@
+# 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.
+
+# Example of an ad hoc serializer that is tailored to transform business specific objects into customized representation.
+#
+# In the following, we expose 3 business classes, `E`, `A` and `B`, and a specific business serializer `RestrictedSerializer`.
+# The principle is that the custom serialization logic in enclosed into the `RestrictedSerializer` and that the
+# standard serializer is unchanged.
+#
+# The additional behaviors exposed are:
+#
+# * replace a full business object (see `E::id`):
+#   instead of serializing an attribute, the custom serializer uses a different representation.
+# * inject a phantom attribute (see `E::phantom`):
+#   when serializing, the custom serializer injects new attributes.
+# * hide a normally serialized attribute (see `E::semi_private`):
+#   when serializing, the custom serializer hides some specific attributes.
+#
+# The advantage of the approach is that it is done programmatically so can be adapted to real complex use cases.
+# Basically, this is half-way between the full automatic serialization and the full manual serialisation.
+module custom_serialization
+
+import serialization
+import json::serialization_write
+
+# The root class of the business objects.
+# This factorizes most services and domain knowledge used by the `RestrictedSerializer`
+#
+# In real enterprise-level code, the various specific behaviors can be specified in more semantic classifiers.
+abstract class E
+       serialize
+
+       # The semantic business identifier.
+       #
+       # With the `RestrictedSerializer`, references to `E` objects will be replaced with `id`-based information.
+       # This avoid to duplicate or enlarge the information cross-call wise.
+       #
+       # A future API/REST call can then request the _missing_ object from its identifier.
+       var id: String
+
+       # A phantom attribute to be serialized by the custom `RestrictedSerializer`.
+       #
+       # This can be used to inject constant or computed information that make little sense to have as a genuine attribute in
+       # the Nit model.
+       fun phantom: String do return "So Much Fun"
+
+       # An attribute not to be serialized by the custom `RestrictedSerializer`.
+       # e.g. we want it on the DB but not in API/REST JSON messages
+       #
+       # Note that the annotation `noserialize` hides the attribute for all serializers.
+       # To hide the attribute only in the `RestrictedSerializer`, it will have to actively ignore it.
+       var semi_private = "secret"
+
+       # Test method that serializes `self` and prints with the standard JsonSerializer
+       fun ser_json
+       do
+               var w = new StringWriter
+               var js = new JsonSerializer(w)
+               js.plain_json = true
+               js.serialize(self)
+               print w
+       end
+
+       # Test method that serializes `self` and prints with the custom RestrictedJsonSerializer.
+       fun ser_json2
+       do
+               var w = new StringWriter
+               var js = new RestrictedJsonSerializer(w)
+               js.plain_json = true
+               js.serialize(self)
+               print w
+       end
+end
+
+# Extends Serializer and adds specific business behaviors when dealing with business objects.
+#
+# As with standard Nit, additional level of customization can be achieved by adding more double-dispatching :)
+# We can thus choose to locate the specific behavior in the serializer, or the serializees.
+class RestrictedSerializer
+       super Serializer
+
+       # This method is called to generate the attributes of a serialized representation
+       redef fun serialize_core(value)
+       do
+               super
+
+               if value isa E then
+                       # Inject additional special domain-specific information
+                       serialize_attribute("more-data", value.phantom)
+               end
+       end
+
+       # This method is called when trying to serialize a specific attribute
+       redef fun serialize_attribute(name, value)
+       do
+               var recv = current_object
+               if recv isa E then
+                       # do not serialize `E::semi_private`
+                       if name == "semi_private" then return
+               end
+
+               if value isa E then
+                       # Do not serialize references to `E`.
+                       # Just use a domain-specific value that make sense in the business logic.
+                       serialize_attribute(name, "ID:" + value.id)
+                       return
+               end
+
+               super
+       end
+end
+
+# Extends JsonSerializer and adds specific business behaviors when dealing with business objects.
+class RestrictedJsonSerializer
+       super JsonSerializer
+       super RestrictedSerializer
+end
+
+# A business object, with an integer information
+class A
+       super E
+       serialize
+
+       # A business information
+       var i: Int
+end
+
+# A business object associated with an `A`.
+class B
+       super E
+       serialize
+
+       # A business association
+       var a: A
+end
+
+# The business data to serialize
+var a = new A("a", 1)
+var b = new B("b", a)
+
+a.ser_json
+a.ser_json2
+b.ser_json
+b.ser_json2
index f94445e..b0d5d6c 100644 (file)
@@ -57,6 +57,11 @@ interface Serializer
        # 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
 
@@ -81,6 +86,15 @@ interface Serializer
                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}"
diff --git a/tests/sav/custom_serialization.res b/tests/sav/custom_serialization.res
new file mode 100644 (file)
index 0000000..a7d78f6
--- /dev/null
@@ -0,0 +1,4 @@
+{"id":"a","semi_private":"secret","i":1}
+{"id":"a","semi_private":,"i":1,"more-data":"So Much Fun"}
+{"id":"b","semi_private":"secret","a":{"id":"a","semi_private":"secret","i":1}}
+{"id":"b","semi_private":,"a":,"a":"ID:a","more-data":"So Much Fun"}