-
-
Save nros/7791baca1927ee6b25e790da76544d99 to your computer and use it in GitHub Desktop.
TypeScript: usage example of custom serializer/deserializer with serializr module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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[]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is an example for mobxjs/serializr#67
inversify
.This implementation has some draw backs: