Skip to content

Commit

Permalink
Add models extension system.
Browse files Browse the repository at this point in the history
  • Loading branch information
Madeorsk committed Oct 4, 2024
1 parent 576338f commit 4eb8b7d
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 109 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</p>

<p align="center">
<img alt="Version 3.0.2" src="https://img.shields.io/badge/version-3.0.2-blue" />
<img alt="Version 3.1.0" src="https://img.shields.io/badge/version-3.1.0-blue" />
</p>

## Introduction
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sharkitek/core",
"version": "3.0.2",
"version": "3.1.0",
"description": "TypeScript library for well-designed model architectures.",
"keywords": [
"deserialization",
Expand Down
257 changes: 151 additions & 106 deletions src/Model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ export type SerializedModel<Shape extends ModelShape> = {
*/
export type Model<Shape extends ModelShape, IdentifierType = unknown> = ModelDefinition<Shape, IdentifierType> & PropertiesModel<Shape>;

/**
* Type of the extends function of model classes.
*/
export type ExtendsFunctionType<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any> =
<Extension extends object>(extension: ThisType<ModelType> & Extension) => ModelClass<ModelType & Extension, Shape, Identifier>;

/**
* Type of a model class.
*/
export type ModelClass<Shape extends ModelShape, Identifier extends keyof Shape = any> = ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>;
export type ModelClass<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any> = (
ConstructorOf<ModelType> & {
extends: ExtendsFunctionType<ModelType, Shape, Identifier>;
}
);

/**
* Identifier type.
Expand Down Expand Up @@ -96,141 +106,176 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
shape: Shape,
identifier?: Identifier,
): ConstructorOf<ModelType>
): ModelClass<ModelType, Shape, Identifier>
{
// Get shape entries.
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];

return class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>, ModelType>
{
constructor()
return withExtends(
// Initialize generic model class.
class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>, ModelType>
{
// Initialize properties to undefined.
Object.assign(this,
// Build empty properties model from shape entries.
Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel<Shape>
);
}
constructor()
{
// Initialize properties to undefined.
Object.assign(this,
// Build empty properties model from shape entries.
Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel<Shape>
);
}

/**
* Calling a function for each defined property.
* @param callback - The function to call.
* @protected
*/
protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
{
for (const [propertyName, propertyDefinition] of shapeEntries)
{ // For each property, checking that its type is defined and calling the callback with its type.
// If the property is defined, calling the function with the property name and definition.
const result = callback(propertyName, propertyDefinition);
// If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result;
/**
* Calling a function for each defined property.
* @param callback - The function to call.
* @protected
*/
protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
{
for (const [propertyName, propertyDefinition] of shapeEntries)
{ // For each property, checking that its type is defined and calling the callback with its type.
// If the property is defined, calling the function with the property name and definition.
const result = callback(propertyName, propertyDefinition);
// If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result;
}
}
}


/**
* The original properties values.
* @protected
*/
protected _originalProperties: Partial<PropertiesModel<Shape>> = {};
/**
* The original properties values.
* @protected
*/
protected _originalProperties: Partial<PropertiesModel<Shape>> = {};

/**
* The original (serialized) object.
* @protected
*/
protected _originalObject: SerializedModel<Shape>|null = null;
/**
* The original (serialized) object.
* @protected
*/
protected _originalObject: SerializedModel<Shape>|null = null;



getIdentifier(): IdentifierType<Shape, Identifier>
{
return (this as PropertiesModel<Shape>)?.[identifier];
}
getIdentifier(): IdentifierType<Shape, Identifier>
{
return (this as PropertiesModel<Shape>)?.[identifier];
}

serialize(): SerializedModel<Shape>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
serialize(): SerializedModel<Shape>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};

this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel<Shape>)?.[propertyName]);
});
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel<Shape>)?.[propertyName]);
});

return serializedObject as SerializedModel<Shape>; // Returning the serialized object.
}
return serializedObject as SerializedModel<Shape>; // Returning the serialized object.
}

deserialize(obj: SerializedModel<Shape>): ModelType
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value.
(this as PropertiesModel<Shape>)[propertyName] = propertyDefinition.type.deserialize(obj[propertyName]);
});
deserialize(obj: SerializedModel<Shape>): ModelType
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value.
(this as PropertiesModel<Shape>)[propertyName] = propertyDefinition.type.deserialize(obj[propertyName]);
});

// Reset original property values.
this.resetDiff();
// Reset original property values.
this.resetDiff();

this._originalObject = obj; // The model is not a new one, but loaded from a deserialized one. Storing it.
this._originalObject = obj; // The model is not a new one, but loaded from a deserialized one. Storing it.

return this as unknown as ModelType;
}
return this as unknown as ModelType;
}


isNew(): boolean
{
return !this._originalObject;
}
isNew(): boolean
{
return !this._originalObject;
}

isDirty(): boolean
{
return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different.
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
// There is a difference, we should return false.
? true
// There is no difference, returning nothing.
: undefined
)) === true;
}
isDirty(): boolean
{
return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different.
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
// There is a difference, we should return false.
? true
// There is no difference, returning nothing.
: undefined
)) === true;
}


serializeDiff(): Partial<SerializedModel<Shape>>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
serializeDiff(): Partial<SerializedModel<Shape>>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};

this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object if it has changed or if it is the identifier.
if (
identifier == propertyName ||
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
) // Adding the current property to the serialized object if it is the identifier or its value has changed.
serializedObject[propertyName] = propertyDefinition.type.serializeDiff((this as PropertiesModel<Shape>)?.[propertyName]);
});
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object if it has changed or if it is the identifier.
if (
identifier == propertyName ||
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
) // Adding the current property to the serialized object if it is the identifier or its value has changed.
serializedObject[propertyName] = propertyDefinition.type.serializeDiff((this as PropertiesModel<Shape>)?.[propertyName]);
});

return serializedObject; // Returning the serialized object.
}
return serializedObject; // Returning the serialized object.
}

resetDiff(): void
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as PropertiesModel<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
});
}
resetDiff(): void
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as PropertiesModel<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
});
}

save(): Partial<SerializedModel<Shape>>
{
// Get the difference.
const diff = this.serializeDiff();
save(): Partial<SerializedModel<Shape>>
{
// Get the difference.
const diff = this.serializeDiff();

// Once the difference has been obtained, reset it.
this.resetDiff();
// Once the difference has been obtained, reset it.
this.resetDiff();

return diff; // Return the difference.
}
return diff; // Return the difference.
}

} as unknown as ConstructorOf<ModelType>
);
}

/**
* Any Sharkitek model.
*/
export type AnyModel = Model<any, any>;
/**
* Any Sharkitek model class.
*/
export type AnyModelClass = ModelClass<AnyModel, any>;

} as unknown as ConstructorOf<ModelType>;
/**
* Add extends function to a model class.
* @param genericModel The model class on which to add the extends function.
*/
function withExtends<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
genericModel: ConstructorOf<ModelType>
): ModelClass<ModelType, Shape, Identifier>
{
return Object.assign(
genericModel,
{ // Extends function definition.
extends<Extension extends object>(extension: Extension): ModelClass<ModelType & Extension, Shape, Identifier>
{
// Clone the model class and add extends function.
const classClone = withExtends(class extends (genericModel as AnyModelClass) {} as AnyModelClass as ConstructorOf<ModelType & Extension>);
// Add extension to the model class prototype.
Object.assign(classClone.prototype, extension);
return classClone;
}
}
) as AnyModelClass as ModelClass<ModelType, Shape, Identifier>;
}
11 changes: 11 additions & 0 deletions tests/Model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class Author extends s.model({
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.bool(),
}).extends({
extension(): string
{
return this.name;
}
})
{
active: boolean = true;
Expand Down Expand Up @@ -157,3 +162,9 @@ it("save with modified submodels", () => {
],
});
});

it("test author extension", () => {
const author = new Author();
author.name = "test name";
expect(author.extension()).toStrictEqual("test name");
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"incremental": true,
"sourceMap": true,
"noImplicitAny": true,
"noImplicitThis": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
Expand All @@ -24,6 +25,6 @@
"lib": [
"ESNext",
"DOM"
],
]
}
}

0 comments on commit 4eb8b7d

Please sign in to comment.