Skip to content

Instantly share code, notes, and snippets.

@nros
Created March 21, 2018 11:01
Show Gist options
  • Save nros/7791baca1927ee6b25e790da76544d99 to your computer and use it in GitHub Desktop.
Save nros/7791baca1927ee6b25e790da76544d99 to your computer and use it in GitHub Desktop.
TypeScript: usage example of custom serializer/deserializer with serializr module
// WARNING - only an excerpt of real implemenation
/***
* the exported blFactory constant is the factory to be used with this application to create new
* instanced of types/classes. The factory creates these instances and automatically resolves all the dependencies.
*/
import { Container, interfaces } from "inversify";
import * as serializr from "serializr";
import { PrismModel } from "./easy3d/geometry/PrismModel";
export {
DITYPES,
blFactory,
BLFactory,
};
class BLFactory implements IBLFactory {
// Singletons
private _diContainer: Container;
public constructor() {
this._diContainer = this.configureDIContainer(new Container());
this.getDIContainer().bind<BLFactory>(DITYPES.BLFactory).toConstantValue(this);
setGlobalDIFactory(this);
}
/***
* returns an instance for the provided symbol. It depends on the configuration of the dependency injection
* system for this type, whether it is a new instance or a singleton instance is re-used.
*
* @param {string | symbol | interfaces.ServiceIdentifier<T>} typeName
* @param {JSONType} constructorParameters
* The values of the provided parameter object is added to the DI container prior to creating the new instance.
* Afterwards these values are removed from the container.
*
* @returns {T}
*/
public create<T>(
typeName: string | symbol | interfaces.ServiceIdentifier<T>,
constructorParameters?: { [propName: string]: any },
): T {
const di: Container = this.getDIContainer();
let temporaryDI = di;
// bind the provided parameters to the container
if (constructorParameters !== undefined && constructorParameters !== null
&& typeof constructorParameters === "object"
) {
// create temporary DI container
temporaryDI = di.createChild();
// bind parameters
Object.keys(constructorParameters).forEach((key: string) => {
const value = constructorParameters[key];
if (temporaryDI.isBoundNamed(DITYPES.CArg, key)) {
logger.debug("Overriding existing contructor argument binding for key: " + key);
}
temporaryDI.bind(DITYPES.CArg).toConstantValue(value).whenTargetNamed(key);
});
}
return this.createFromContainer(temporaryDI, typeName);
}
/**
* configures the provided dependency injection container to know what classes to use
* for each symbol.
* @param {Container} diContainer
* @returns {Container} the configured container
*/
public configureDIContainer(diContainer: Container): Container {
if (diContainer !== undefined && diContainer !== null && typeof diContainer.bind === "function") {
this.bindTo<ProjectModel>(diContainer, "ProjectModel", ProjectModel)
.inTransientScope();
this.bindTo<PrismModel>(diContainer, "PrismModel", PrismModel)
.inTransientScope();
}
return diContainer;
}
/***
* helper function to bind and add "serializr" factory functions if applicable.
* For serializr we create a new factory function that will call this container to actually create the instance
* needed.
*
* @param {Container} diContainer
* @param {string} serializedClassName the name used to find the service identifier from DITYPES and to use
* with serialized data to find the proper schema for deserialization
* @param {any} constructor
* @returns {interfaces.BindingToSyntax<T>}
*/
public bindTo<T extends object>(
diContainer: Container,
serializedClassName: string,
constructor: { new (...args: any[]): T; },
): interfaces.BindingInWhenOnSyntax<T> {
if (diContainer !== undefined && diContainer !== null && typeof diContainer.bind === "function") {
const serviceIdentifier: interfaces.ServiceIdentifier<T> = DITYPES[serializedClassName];
if (!serviceIdentifier) {
throw RangeError("No DITYPE for SerializedClassName: " + serializedClassName);
}
// add factory functions to use the BLFactory to create the instances
// see: https://github.com/mobxjs/serializr#5-use-custom-factory-methods-to-reuse-model-object-instances
// check if this class has a serializr schema available. If so, then change the factory function.
const modelSchema: serializr.ModelSchema<T> = serializr.getDefaultModelSchema(constructor);
if (modelSchema !== undefined && modelSchema !== null) {
modelSchema.factory = ((originalFactory: serializr.Factory<T>): serializr.Factory<T> => {
return (context: serializr.Context): T => {
let obj: T;
// try to use the DI container
if (obj === undefined || obj === null) {
try {
obj = blFactory.create<T>(serviceIdentifier);
} catch (e) { /* */ }
}
// create an empty instance with original factory function.
if (obj === undefined || obj === null) {
try {
obj = originalFactory(context);
} catch (e) { /* */ }
}
// create an empty instance with class constructor
if (obj === undefined || obj === null) {
try {
obj = Object.create(Object.getPrototypeOf(constructor));
constructor.apply(obj);
} catch (e) { /* */ }
}
return obj as T;
};
})(modelSchema.factory);
// if there is a model schema, remember the service identifier name
// tslint:disable:no-string-literal
constructor["serializedClassName"] = serializedClassName;
}
return diContainer.bind(serviceIdentifier).to(constructor);
}
return undefined;
}
}
const blFactory = new BLFactory();
export default blFactory;
/**
* @class IModelSerializer
*
* @classdesc
* instances of this serializer provide functions to serialize or deserialize/re-compose model classes.
*/
import { Context, SerializeContext } from "serializr";
export interface SerializedData {
data: any;
schemaName: string;
}
export interface SerializedDataReference {
id: string;
schemaName: string;
}
export type TSerializedData = SerializedData | SerializedDataReference;
export interface IModelSerializer {
serialize(objectToSerialize: object[], context?: SerializeContext): TSerializedData[];
serialize(objectToSerialize: object, context?: SerializeContext): TSerializedData;
serialize(dataToSerialize: boolean): boolean;
serialize(dataToSerialize: boolean[]): boolean[];
serialize(dataToSerialize: number): number;
serialize(dataToSerialize: number[]): number[];
serialize(dataToSerialize: string): string;
serialize(dataToSerialize: string[]): string[];
serialize(dataToSerialize: null): null;
serialize(dataToSerialize: null[]): null[];
serialize(dataToSerialize: undefined): undefined;
serialize(dataToSerialize: undefined[]): undefined[];
deserialize<T>(serializedData: TSerializedData, context?: Context): T;
deserialize<T>(serializedData: TSerializedData[], context?: Context): T[];
deserialize(serializedData: boolean): boolean;
deserialize(serializedData: boolean[]): boolean[];
deserialize(serializedData: number): number;
deserialize(serializedData: number[]): number[];
deserialize(serializedData: string): string;
deserialize(serializedData: string[]): string[];
deserialize(serializedData: null): null;
deserialize(serializedData: null[]): null[];
deserialize(serializedData: undefined): undefined;
deserialize(serializedData: undefined[]): undefined[];
}
/**
* @class ModelSerializer
*
* @classdesc
* instances of this serializer provide functions to serialize or deserialize/re-compose model classes.
*/
import { injectable, interfaces } from "inversify";
import {
Context,
createModelSchema,
deserializeObjectWithSchema,
getDefaultModelSchema,
getIdentifierProperty,
reference,
serialize,
SerializeContext,
} from "serializr";
import { BLFactoryDependent } from "../../BLFactoryDependent";
import { DITYPES } from "../../DITypes";
import {
IModelSerializer,
TSerializedData,
} from "./IModelSerializer";
@injectable()
export class ModelSerializer extends BLFactoryDependent implements IModelSerializer {
public serialize(objectToSerialize: object[], context: SerializeContext): TSerializedData[];
public serialize(objectToSerialize: object, context: SerializeContext): TSerializedData;
public serialize(dataToSerialize: boolean): boolean;
public serialize(dataToSerialize: boolean[]): boolean[];
public serialize(dataToSerialize: number): number;
public serialize(dataToSerialize: number[]): number[];
public serialize(dataToSerialize: string): string;
public serialize(dataToSerialize: string[]): string[];
public serialize(dataToSerialize: null): null;
public serialize(dataToSerialize: null[]): null[];
public serialize(dataToSerialize: undefined): undefined;
public serialize(dataToSerialize: undefined[]): undefined[];
public serialize(objectToSerialize, context?: SerializeContext): any {
if (objectToSerialize === undefined || objectToSerialize === null
|| (typeof objectToSerialize !== "object" && typeof objectToSerialize !== "function")
) {
return objectToSerialize;
} else if (objectToSerialize instanceof Array) {
// serialize array as array of serialized data
return (objectToSerialize as any[]).map((item: any) => this.serialize(item, context));
}
let serializedData;
// tslint:disable:no-string-literal
const className = objectToSerialize.constructor["serializedClassName"] || objectToSerialize.constructor.name;
const modelSchema = getDefaultModelSchema(objectToSerialize as any);
if (modelSchema) {
// has this object been serialized before? in that case, store only an ID in order to maintain references
const temp = context && context.rootContext as any || undefined;
const identifierProperty = temp && getIdentifierProperty(getDefaultModelSchema(objectToSerialize as any));
if (identifierProperty
&& temp && temp.alreadySerialized && Array.isArray(temp.alreadySerialized)
&& temp.alreadySerialized.filter((serializedItem: object): boolean =>
serializedItem && serializedItem[identifierProperty] === objectToSerialize[identifierProperty],
).length > 0
) {
return {
id: objectToSerialize[identifierProperty],
schemaName: className,
};
}
// no - the data has not yet been serialized
serializedData = serialize(modelSchema, objectToSerialize);
// save the serialized data for later referencing
if (temp && serializedData) {
temp.alreadySerialized = temp.alreadySerialized || [];
temp.alreadySerialized.push(serializedData);
}
} else {
// try JSON serialization instead
serializedData = JSON.parse(JSON.stringify(objectToSerialize));
}
return {
data: serializedData,
schemaName: className,
};
}
public deserialize<T>(serializedData: TSerializedData, context?: Context): T;
public deserialize<T>(serializedData: TSerializedData[], context?: Context): T[];
public deserialize(serializedData: boolean): boolean;
public deserialize(serializedData: boolean[]): boolean[];
public deserialize(serializedData: number): number;
public deserialize(serializedData: number[]): number[];
public deserialize(serializedData: string): string;
public deserialize(serializedData: string[]): string[];
public deserialize(serializedData: null): null;
public deserialize(serializedData: null[]): null[];
public deserialize(serializedData: undefined): undefined;
public deserialize(serializedData: undefined[]): undefined[];
public deserialize<T>(data, context?: Context): any {
const globalRoot = window || global;
if (data === undefined || data === null || typeof data !== "object") {
return data;
} else if (data instanceof Array) {
// deserialize array as array of serialized data
return data.map((item) => this.deserialize(item, context));
} else if (
typeof data.schemaName !== "string" || !data.schemaName
|| (!DITYPES[data.schemaName] && !globalRoot[data.schemaName])
) {
// invalid serialized data - no such class or schema name
return (data.data !== undefined) ? data.data : (
(data.id !== undefined) ? data.id : data
);
}
// let's see, whether the ID was previously resolved
const temp = context && context.rootContext as any || undefined;
if (data.id && temp && temp.alreadyDeserialized && temp.alreadyDeserialized[data.schemaName + "_" + data.id]) {
return temp.alreadyDeserialized[data.schemaName + "_" + data.id];
}
const blFactory = this.getBLFactory();
// detect the model schema - if there is one
let modelSchema;
let clazz;
if (DITYPES[data.schemaName]) {
try {
// dirty to access the internal dictionary but there is no other way at the moment.
const bindings = (
(blFactory.getDIContainer() as any)._bindingDictionary as interfaces.Lookup<interfaces.Binding<any>>
).get(DITYPES[data.schemaName]);
clazz = bindings && bindings.length > 0 && bindings[0].implementationType || undefined;
modelSchema = getDefaultModelSchema(clazz as any);
} catch (e) {
console.log(e);
}
}
let resolvedEntity;
if (modelSchema && context && data.id) {
// use the ID to find the previously serialized entity. Use serializr default reference resolver
// tslint:disable:max-classes-per-file
const temporarySchema = createModelSchema(function TemporarySchema() { /* */ } as any, {
id: reference(
modelSchema,
(uuid: string, callback: (error: Error, value: any) => void, lookupContext?: Context) => {
(lookupContext.rootContext as any).await(modelSchema, uuid, callback);
},
),
});
const resolvedTempEntity = deserializeObjectWithSchema(context, temporarySchema, data) as {id: T};
// save the serialized data for later referencing
if (temp && resolvedTempEntity && resolvedTempEntity.id) {
temp.alreadyDeserialized = temp.alreadyDeserialized || [];
temp.alreadyDeserialized[data.schemaName + "_" + data.id] = resolvedTempEntity.id;
}
resolvedEntity = resolvedTempEntity && resolvedTempEntity.id;
} else if (data.data === undefined || data.data === null) {
// no serialized data
return data.data;
} else if (modelSchema) {
resolvedEntity = deserializeObjectWithSchema(context, modelSchema, data.data) as any as T;
} else if (globalRoot[data.schemaName] && typeof globalRoot[data.schemaName] === "function") {
// try JSON deserialization as alternative and last resort
resolvedEntity = Object.create(Object.getPrototypeOf(globalRoot[data.schemaName]));
globalRoot[data.schemaName].apply(resolvedEntity);
for (const propName of data.data) {
resolvedEntity[propName] = data.data[propName];
}
} else {
// create a copy of the JSON data
return JSON.parse(JSON.stringify(data.data));
}
// because the object might be created without the DI, check for some most important dependency
// setters.
// tslint:disable:no-string-literal
if (resolvedEntity !== undefined && resolvedEntity !== null
&& typeof resolvedEntity["setBLFactory"] === "function"
) {
resolvedEntity.setBLFactory(blFactory);
}
if (resolvedEntity !== undefined && resolvedEntity !== null
&& typeof resolvedEntity["setEasy3DFactory"] === "function"
) {
resolvedEntity.setEasy3DFactory(blFactory.getEasy3dFactory());
}
if (resolvedEntity !== undefined && resolvedEntity !== null
&& typeof resolvedEntity["setExecutionModelFactory"] === "function"
) {
resolvedEntity.setExecutionModelFactory(blFactory.getExecutionModelFactory());
}
// tslint:enable:no-string-literal
return resolvedEntity;
}
}
@nros
Copy link
Author

nros commented Mar 21, 2018

This is an example for mobxjs/serializr#67

  • To make the custom de-serializer function work, there must be a central place to keep information about all available classes. In this case dependency injector is used, using inversify.
  • the basic idea is, to add the class name and the ID of the class to the serialized content and then perform a lookup to the DI when deserializing takes place.
  • since the class name usually is crippled with compression and obfuscation utilities, a symbolic class name is used instead, stored with the prototype of that class and with the DI container (in this case the map DITYPES).
     constructor["serializedClassName"] = serializedClassName;
    
    There is no need to use the "real" class name. On contrary, it is useful to use a symbolic name instead. In case a new, compatible implementation of that class replaces the previous one, you must not change the class name in with this information. Otherwise you loose the ability to de-serialize legacy classes.

This implementation has some draw backs:

  • it does not yet handle migration to new class implementation very well. It just ignores it :)
  • a central lookup table for class names is needed. In this case the DI container with its lookup table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment