Skip to main content

Custom Serialization

Difficulty: Medium

Pre-requisite

  • Serialization tutorial

Problematic

Two common situations:

  • In case you have created you own types, such as a date type, and you need a custom serialization/deserialization rule for them.
  • You want to add a custom security: the serialization shall behave differently based on the user profil.

For each type you want to serialize you can associate an adapter, for example:

  • aCStringType => aCStringAdapter
  • aIntType => aIntAdapter
  • aCustomType => aCustomAdapter
  • ...

Those association rules are done within the factory. The factory shall be referenced in a module so it can initialize the types only once.

Default serialization

The serialization leverages the 3 main concepts:

  • Factory: created once and providing appropriate adapters
  • Context: specific to each serialization: collect errors and define/resolve outside relations ($ref)
  • Adapter: with specific code based on the typing
procedure Serialize
uses aDataAdapter, aDataDocument, aSequenceType

var factory : aDataAdapterFactory
var context : aDataAdapterContext
var adapter : aDataAdapter
var document : aDataDocument
var object : aNamedObject
var output : Text

factory = Doc.wGetSystemAdapterFactory
context = factory.GetNewAdapterContext
new(object)
adapter = factory.GetAdapterFor(object.type)
;
if adapter <> Nil
new(document)
adapter.SerializeValue(@object, document, context)
document.SetIndentation(True)
if Doc.JSON.StringifyText(document, output)
; => result is in output
output := ''
endIf
;
endIf
dispose(context)
endProc

In this tutorial we will change to code above to use:

  • a custom factory returned by a module: aWS_DataDefinitionFactory
  • a custom context: aWS_DataAdapterContext
  • a specific adapter: aWS_RestResourceDefinition which will drive how we serialize our class defs.

Implementation

You can also install the tutorial classes:

# this will copy the tutorials
npx @ewam/installer@0.0.85 --tutorials
# installation with ewam-cli
npm run ewam:cli -- --import ./tutorials/custom-serialization/1

Create the Adapter context

The Adapter context is the entity that will contain:

  • the specific algorithm to create edges between object (references via $ref keyword)
  • errors if applicable
aWS_DataAdapterContext
; aWS_DataAdapterContext (aDataAdapterContext) (Def Version:4) (Implem Version:5)

class aWS_DataAdapterContext (aDataAdapterContext)

uses aDataValue, aFullObject


procedure AddKeyRef(value : aDataValue, TheKey : CString, obj : aFullObject) private
value.Map('$ref').SetCString(TheKey)
value.Map('Name').SetCString(obj.StringExtract(NameExtract, 0, 255))
endProc

procedure SerializeCanonicalReference(NsId : Int4, Id : Int8, Version : Int4, value : aDataValue) override
uses Motor

var TheKey : CString
var obj : aFullObject

if NsId = 0
if Id = 0
value.SetNull
else
TheKey = '0x' + IaS(Id)
self.AddKeyRef(value, TheKey, Nil)
endIf
else
TheKey = IaS(NsId) + '_' + IaS(Id)
obj = Motor.ThingFromId(NsId, Id, Version)
self.AddKeyRef(value, TheKey, obj)
endIf
endProc

procedure SerializeObjectReference(object : aLightObject, value : aDataValue) override
var TheKey : CString

if object = Nil
value.SetNull
elseif self.factory <> Nil
if member(object, aFullObject)
; self.SerializeCanonicalReference(aFullObject(object).NSId, aFullObject(object).Id,
; aFullObject(object).Version, value)
TheKey = IaS(aFullObject(object).NSId) + '_' + IaS(aFullObject(object).Id)
self.AddKeyRef(value, TheKey, aFullObject(object))
endIf
endIf
endProc


Create a new factory

Use the code below but comment the part corresponding to aWS_RestResourceDefinition as you do not have it yet.

aWS_DataDefinitionFactory
; aWS_DataDefinitionFactory (aDataDefinitionFactory) (Def Version:13) (Implem Version:21)

class aWS_DataDefinitionFactory (aDataDefinitionFactory)

uses aVarDesc, aDataAdapter, aFullObject, aRecordDesc, aRecordAdapter, aPointerType,
aReferenceType, aReftoTypeAdapter, aListofReftosTypeAdapter, aModuleDef, aDataDefinition,
aWS_DataAdapterContext


function ShouldDisplayVar(model : aVarDesc) return Boolean
uses aInstanceVarDesc, SerializationAnnotation, aClassDef

var NSID : aVarDesc
var id : aVarDesc
var version : aVarDesc
var annotation : SerializationAnnotation

NSID = MetaModelEntity(aFullObject.NSId)
id = MetaModelEntity(aFullObject.Id)
version = MetaModelEntity(aFullObject.Version)
if (model = NSID) or (model = id) or (model = version)
return False
endIf
annotation = model.getAnnotation(MetaModelEntity(SerializationAnnotation))
if annotation <> Nil
return not annotation.ignore
endIf
if member(model, aInstanceVarDesc) and aInstanceVarDesc(model).IsTransient
;memory variable are skipped by default
return False
endIf
return True
endFunc

function GetAdapterFor(model : aFullObject) return aDataAdapter override
uses aRenamingType

if member(model, aRenamingType)
_Result = self.GetAdapterFor(aRenamingType(model).GoodOne)
else
_Result = inherited self.GetAdapterFor(model)
endIf
endFunc

procedure SpreadRefTo(vardesc : aVarDesc)
uses aReftoInlineTypeAdapter, aDataStoredField

var result : aDataAdapter
var adapter : aReftoInlineTypeAdapter
var field : aDataStoredField

new(field)
field.InitFor(vardesc)
new(adapter)
adapter.factory = self
adapter.InitFor(vardesc.myType)
field.adapter = adapter
result = self.RegisterAdapterFor(vardesc, field)
endProc

procedure Init protected override
inherited self.Init
;self.SpreadRefTo(MetaModelEntity(aWLIMemberContract.SubscriberAsMember))
;self.SpreadRefTo(MetaModelEntity(aWFContract.Subscriber))
; self.SpreadRefTo(MetaModelEntity(aWLIContract.Subscriber))
; self.SpreadRefTo(MetaModelEntity(aWLIPersonContractRoot.Subscriber))
endProc

function NewAdapterFor(model : aFullObject) return aDataAdapter override
uses aCStringType, aClassDef, aCStringAdapter, aIntType, aIntAdapter, aRoleType,
aSubRangeType, aBooleanType, aBooleanAdapter, aNumType, aNumAdapter, aDecimalType,
aDecimalAdapter, aSequenceType, aSequenceAdapter, aReftoType, aSingleRoleType,
aReftoInlineTypeAdapter, aListofReftosInlineTypeAdapter, aDataStoredField,
aTextType, aTextAdapter, aPointerAdapter, aSetType, aSetAdapter, aDataValue,
aDataValueAdapter, aWS_RestResourceDefinition

var roleType : aRoleType

if member(model, aVarDesc)
if self.ShouldDisplayVar(aVarDesc(model))
new(aDataStoredField(_Result))
endIf
elseif member(model, aClassDef)
if aClassDef(model).IsADescendantOf(MetaModelEntity(aDataValue))
new(aDataValueAdapter(_Result))
else
new(aWS_RestResourceDefinition(_Result))
endIf
elseif member(model, aRecordDesc)
new(aRecordAdapter(_Result))
elseif member(model, aCStringType)
new(aCStringAdapter(_Result))
elseif member(model, aIntType)
new(aIntAdapter(_Result))
elseif member(model, aSubRangeType)
new(aIntAdapter(_Result))
elseif member(model, aBooleanType)
new(aBooleanAdapter(_Result))
elseif member(model, aNumType)
new(aNumAdapter(_Result))
elseif member(model, aDecimalType)
new(aDecimalAdapter(_Result))
elseif member(model, aTextType)
new(aTextAdapter(_Result))
elseif member(model, aPointerType)
new(aPointerAdapter(_Result))
elseif member(model, aSetType)
new(aSetAdapter(_Result))
elseif member(model, aSequenceType)
new(aSequenceAdapter(_Result))
elseif member(model, aReferenceType)
roleType = aReferenceType(model).GetRunTimeRole
if member(roleType, aSingleRoleType)
if aReferenceType(model).isOwner
if member(model, aReftoType)
new(aReftoInlineTypeAdapter(_Result))
else
new(aListofReftosInlineTypeAdapter(_Result))
endIf
else
if member(model, aReftoType)
new(aReftoTypeAdapter(_Result))
else
new(aListofReftosTypeAdapter(_Result))
endIf
endIf
else
_Result = inherited self.NewAdapterFor(model)
endIf
endIf
endFunc

function GetDefautObjDefinitionClassId return Int8 protected
uses aClassDef, aWS_RestResourceDefinition

return MetaModelEntity(aWS_RestResourceDefinition).Id
endFunc

function NewDefinitionFor(forModule : aModuleDef) return aDataDefinition override
uses Motor

_Result = Motor.NewInst(self.GetDefautObjDefinitionClassId)
endFunc

function GetNewAdapterContext return aWS_DataAdapterContext override
new(_Result)
_Result.factory = self
endFunc


Implement the specific adapter

aWS_RestResourceDefinition
; aWS_RestResourceDefinition (aDataDefinition) (Def Version:4) (Implem Version:12)

class aWS_RestResourceDefinition (aDataDefinition)

uses aModuleDef, aClassDef, aWS_DataDefinitionFactory

typing : aModuleDef
factory : aWS_DataDefinitionFactory override


procedure AddClassNameAndStringExtract protected
uses aDataComputedField, aMethodDesc

var field : aDataComputedField

if self.typing <> Nil
new(field)
field.factory = self.factory
field.name = 'className'
field.SetGetterMethod(MetaModelEntity(aLightObject.ClassName))
self.AddField(field)
new(field)
field.factory = self.factory
field.name = '$ref'
field.SetGetterMethod(MetaModelEntity(WS_Delegates.GetRefForObject))
self.AddField(field)
endIf
endProc

procedure GetAllVars protected
uses aInstanceVarDesc, aDataStoredField

var CurVar : aInstanceVarDesc
var CurClass : aClassDef
var cancestor : aDataDefinition
var CurField : aDataStoredField

if member(self.typing, aClassDef)
CurClass = aClassDef(self.typing)
while CurClass <> Nil
forEach CurVar in CurClass.myVars
if self.factory.ShouldDisplayVar(CurVar)
new(CurField)
CurField.factory = self.factory
CurField.InitFor(CurVar)
CurField.SetVariable(CurVar)
self.AddField(CurField)
endIf
endFor
CurClass = CurClass.DerivesFrom
endWhile
endIf
endProc

procedure InitFor(model : aClassDef) override
self.typing = model
self.GetAllVars
self.AddClassNameAndStringExtract
endProc

procedure SerializeValue(varPtr : tpLightObject, value : aDataValue, context : aDataAdapterContext) override
var objClassdef : aClassDef
var adapter : aDataAdapter

objClassdef = varPtr.ClassDef
; Delegate adaptation to the best fitted adapter, only if it's not the current one
if (self.typing <> Nil) and (objClassdef <> self.typing)
adapter = self.factory.GetAdapterFor(objClassdef)
if (adapter <> Nil) and (adapter <> self)
adapter.SerializeValue(varPtr, value, context)
else
inherited self.SerializeValue(varPtr, value, context)
endIf
else
inherited self.SerializeValue(varPtr, value, context)
endIf
endProc



Serialize with the new factory

procedure TestSerialization
uses aDataAdapter, aDataDocument, aClassDef, Doc, aStringFormat

var factory : aWS_DataDefinitionFactory
var context : aDataAdapterContext
var adapter : aDataAdapter
var document : aDataDocument
var object : aNamedObject
var output : Text

;It is recommended to keep the factory in a module
new(factory)
context = factory.GetNewAdapterContext
;
new(object)
adapter = factory.GetAdapterFor(object.type)
if adapter <> Nil
new(document)
;adapter.SerializeValue(@object, document, context)
adapter.GenerateSchema(document, context)
document.SetIndentation(True)
if Doc.JSON.StringifyText(document, output)
output := ''
endIf
dispose(document)
endIf
dispose(object)
dispose(context)
endProc

Create a custom adapter

You can create your own adapter and override SerializeValue/DeserializeValue, for example:

aWFCStringAdapter
; aWFCStringAdapter (aCStringAdapter) (Def Version:3) (Implem Version:4)

class aWFCStringAdapter (aCStringAdapter)

procedure SerializeValue(varPtr : tpCString, value : aDataValue, context : aDataAdapterContext) override
if varPtr <> Nil
value.SetCString(varPtr.)
endIf
endProc

procedure DeserializeValue(varPtr : tpCString, value : aDataValue, context : aDataAdapterContext) override
if varPtr <> Nil
varPtr. = value.ToCString
endIf
endProc