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