import { Constructor, Metadata } from "@decaf-ts/decoration";
import { ModelBuilderFunction, ModelConstructor } from "./types";
import { type Model } from "./Model";
import { BuilderRegistry } from "../utils/registry";
import { jsTypes, ReservedModels } from "./constants";
import { ListValidatorOptions, ValidationKeys } from "../validation/index";
let modelBuilderFunction: ModelBuilderFunction | undefined;
let actingModelRegistry: BuilderRegistry<any>;
/**
* @description Registry type for storing and retrieving model constructors
* @summary The ModelRegistry type defines a registry for model constructors that extends
* the BuilderRegistry interface. It provides a standardized way to register, retrieve,
* and build model instances, enabling the model system to work with different types of models.
*
* @interface ModelRegistry
* @template T Type of model that can be registered, must extend Model
* @extends BuilderRegistry<T>
* @memberOf module:decorator-validation
* @category Model
*/
export type ModelRegistry<T extends Model> = BuilderRegistry<T>;
/**
* @description Registry manager for model constructors that enables serialization and rebuilding
* @summary The ModelRegistryManager implements the ModelRegistry interface and provides
* functionality for registering, retrieving, and building model instances. It maintains
* a cache of model constructors indexed by name, allowing for efficient lookup and instantiation.
* This class is essential for the serialization and deserialization of model objects.
*
* @param {function(Record<string, any>): boolean} [testFunction] - Function to test if an object is a model, defaults to {@link Model#isModel}
*
* @class ModelRegistryManager
* @template M Type of model that can be registered, must extend Model
* @implements ModelRegistry<M>
* @category Model
*
* @example
* ```typescript
* // Create a model registry
* const registry = new ModelRegistryManager();
*
* // Register a model class
* registry.register(User);
*
* // Retrieve a model constructor by name
* const UserClass = registry.get("User");
*
* // Build a model instance from a plain object
* const userData = { name: "John", age: 30 };
* const user = registry.build(userData, "User");
* ```
*
* @mermaid
* sequenceDiagram
* participant C as Client
* participant R as ModelRegistryManager
* participant M as Model Class
*
* C->>R: new ModelRegistryManager(testFunction)
* C->>R: register(ModelClass)
* R->>R: Store in cache
* C->>R: get("ModelName")
* R-->>C: ModelClass constructor
* C->>R: build(data, "ModelName")
* R->>R: Get constructor from cache
* R->>M: new ModelClass(data)
* M-->>R: Model instance
* R-->>C: Model instance
*/
export class ModelRegistryManager<M extends Model<true | false>>
implements ModelRegistry<M>
{
private cache: Record<string, ModelConstructor<M>> = {};
private readonly testFunction: (obj: object) => boolean;
constructor(
testFunction: (obj: Record<string, any>) => boolean = Metadata.isModel
) {
this.testFunction = testFunction;
}
/**
* @description Registers a model constructor with the registry
* @summary Adds a model constructor to the registry cache, making it available for
* later retrieval and instantiation. If no name is provided, the constructor's name
* property is used as the key in the registry.
*
* @param {ModelConstructor<M>} constructor - The model constructor to register
* @param {string} [name] - Optional name to register the constructor under, defaults to constructor.name
* @return {void}
* @throws {Error} If the constructor is not a function
*/
register(constructor: ModelConstructor<M>, name?: string): void {
if (typeof constructor !== "function")
throw new Error(
"Model registering failed. Missing Class name or constructor"
);
name = name || constructor.name;
this.cache[name] = constructor;
}
/**
* @summary Gets a registered Model {@link ModelConstructor}
* @param {string} name
*/
get(name: string): ModelConstructor<M> | undefined {
try {
return this.cache[name];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e: any) {
return undefined;
}
}
/**
* @param {Record<string, any>} obj
* @param {string} [clazz] when provided, it will attempt to find the matching constructor
*
* @throws Error If clazz is not found, or obj is not a {@link Model} meaning it has no {@link ModelKeys.ANCHOR} property
*/
build(obj: Record<string, any> = {}, clazz?: string): M {
if (!clazz && !this.testFunction(obj))
throw new Error("Provided obj is not a Model object");
const name = clazz || Metadata.modelName(obj.constructor as any);
if (!(name in this.cache))
throw new Error(
`Provided class ${name} is not a registered Model object`
);
return new this.cache[name](obj);
}
/**
* @description Configures the global model builder function
* @summary Sets the Global {@link ModelBuilderFunction} used for building model instances
*
* @param {ModelBuilderFunction} [builder] - The builder function to set as the global builder
* @return {void}
*/
static setBuilder(builder?: ModelBuilderFunction) {
modelBuilderFunction = builder;
}
/**
* @description Retrieves the currently configured global model builder function
* @summary Returns the current global {@link ModelBuilderFunction} used for building model instances
*
* @return {ModelBuilderFunction | undefined} - The current global builder function or undefined if not set
*/
static getBuilder(): ModelBuilderFunction | undefined {
return modelBuilderFunction || ModelRegistryManager.fromModel;
}
/**
* @description Copies and rebuilds properties from a source object to a model instance, handling nested models
* @summary Repopulates the instance with properties from the new Model Object, recursively rebuilding nested models
*
* @template T
* @param {T} self - The target model instance to update
* @param {T | Record<string, any>} [obj] - The source object containing properties to copy
* @return {T} - The updated model instance with rebuilt nested models
*
* @mermaid
* sequenceDiagram
* participant C as Client
* participant M as Model.fromModel
* participant B as Model.build
* participant R as Reflection
*
* C->>M: fromModel(self, obj)
* M->>M: Get attributes from self
* loop For each property
* M->>M: Copy property from obj to self
* alt Property is a model
* M->>M: Check if property is a model
* M->>B: build(property, modelType)
* B-->>M: Return built model
* else Property is a complex type
* M->>R: Get property decorators
* R-->>M: Return decorators
* M->>M: Filter type decorators
* alt Property is Array/Set with list decorator
* M->>M: Process each item in collection
* loop For each item
* M->>B: build(item, itemModelType)
* B-->>M: Return built model
* end
* else Property is another model type
* M->>B: build(property, propertyType)
* B-->>M: Return built model
* end
* end
* end
* M-->>C: Return updated self
*/
static fromModel<T extends Model>(self: T, obj?: T | Record<string, any>): T {
if (!obj) obj = {};
let decorators: DecoratorMetadata[];
const props = Metadata.getAttributes(self);
const proto = Object.getPrototypeOf(self);
let descriptor: PropertyDescriptor | undefined;
for (const prop of props) {
try {
(self as Record<string, any>)[prop] =
(obj as Record<string, any>)[prop] ??
(self as Record<string, any>)[prop] ??
undefined;
} catch (e: unknown) {
descriptor = Object.getOwnPropertyDescriptor(proto, prop);
if (!descriptor || descriptor.writable)
throw new Error(`Unable to write property ${prop} to model: ${e}`);
}
if (typeof (self as any)[prop] !== "object") continue;
const propM = Metadata.isPropertyModel(self, prop);
if (propM) {
try {
(self as Record<string, any>)[prop] =
ModelRegistryManager.getRegistry().build(
(self as Record<string, any>)[prop],
typeof propM === "string" ? propM : undefined
);
} catch (e: any) {
console.log(e);
}
continue;
}
decorators = Metadata.allowedTypes(self.constructor as any, prop);
if (!decorators || !decorators.length)
throw new Error(`failed to find decorators for property ${prop}`);
const clazz = decorators.map((t: any) =>
typeof t === "function" && !t.name ? t() : t
);
const reserved: any = Object.values(ReservedModels);
clazz.forEach((c: Constructor<any>) => {
if (!reserved.includes(c))
try {
switch (c.name) {
case "Array":
case "Set": {
const validation: any = Metadata.validationFor(
self.constructor as Constructor,
prop
);
if (!validation || !validation[ValidationKeys.LIST]) break;
const listDec: ListValidatorOptions =
validation[ValidationKeys.LIST];
const clazzName = (
listDec.clazz as (
| Constructor<any>
| (() => Constructor<any>)
)[]
)
.map((t) =>
typeof t === "function" && !(t as any).name
? (t as any)()
: t
)
.find((t) => !jsTypes.includes(t.name));
if (c.name === "Array")
(self as Record<string, any>)[prop] = (
self as Record<string, any>
)[prop].map((el: any) => {
return ["object", "function"].includes(typeof el) &&
clazzName
? ModelRegistryManager.getRegistry().build(
el,
clazzName.name
)
: el;
});
if (c.name === "Set") {
const s = new Set();
for (const v of (self as Record<string, any>)[prop]) {
if (
["object", "function"].includes(typeof v) &&
clazzName
) {
s.add(
ModelRegistryManager.getRegistry().build(
v,
clazzName.name
)
);
} else {
s.add(v);
}
}
(self as Record<string, any>)[prop] = s;
}
break;
}
default:
if (
typeof self[prop as keyof typeof self] !== "undefined" &&
ModelRegistryManager.getRegistry().get(c.name)
)
(self as Record<string, any>)[prop] =
ModelRegistryManager.getRegistry().build(
(self as any)[prop],
c.name
);
}
} catch (e: any) {
console.log(e);
// do nothing. we have no registry of this class
}
});
}
return self;
}
/**
* @description Provides access to the current model registry
* @summary Returns the current {@link ModelRegistryManager} instance, creating one if it doesn't exist
*
* @return {ModelRegistry<any>} - The current model registry, defaults to a new {@link ModelRegistryManager} if not set
* @private
*/
static getRegistry() {
if (!actingModelRegistry) actingModelRegistry = new ModelRegistryManager();
return actingModelRegistry;
}
/**
* @description Configures the model registry to be used by the Model system
* @summary Sets the current model registry to a custom implementation
*
* @param {BuilderRegistry<any>} modelRegistry - The new implementation of Registry to use
* @return {void}
*/
static setRegistry(modelRegistry: BuilderRegistry<any>) {
actingModelRegistry = modelRegistry;
}
}
/**
* @summary Bulk Registers Models
* @description Useful when using bundlers that might not evaluate all the code at once
*
* @template M extends Model
* @param {Array<Constructor<M>> | Array<{name: string, constructor: Constructor<M>}>} [models]
*
* @memberOf module:decorator-validation
* @category Model
*/
export function bulkModelRegister<M extends Model>(
...models: (Constructor<M> | { name: string; constructor: Constructor<M> })[]
) {
models.forEach(
(m: Constructor<M> | { name: string; constructor: Constructor<M> }) => {
const constructor: Constructor<M> = (
m.constructor ? m.constructor : m
) as Constructor<M>;
ModelRegistryManager.getRegistry().register(
constructor,
(m as Constructor<M>).name
);
}
);
}
Source