@travetto/schema
Advanced tools
Comparing version 3.1.2 to 3.1.3
{ | ||
"name": "@travetto/schema", | ||
"version": "3.1.2", | ||
"version": "3.1.3", | ||
"description": "Data type registry for runtime validation, reflection and binding.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -5,3 +5,3 @@ import { Class } from '@travetto/base'; | ||
import { SchemaRegistry } from '../service/registry'; | ||
import { ViewFieldsConfig } from '../service/types'; | ||
import { ClassConfig, ViewFieldsConfig } from '../service/types'; | ||
import { DeepPartial } from '../types'; | ||
@@ -15,3 +15,3 @@ import { ValidatorFn } from '../validate/types'; | ||
*/ | ||
export function Schema() { // Auto is used during compilation | ||
export function Schema(cfg?: Partial<Pick<ClassConfig, 'subTypeField' | 'baseType'>>) { // Auto is used during compilation | ||
return <T, U extends Class<T>>(target: U): U => { | ||
@@ -21,3 +21,3 @@ target.from ??= function <V>(this: Class<V>, data: DeepPartial<V>, view?: string): V { | ||
}; | ||
SchemaRegistry.getOrCreatePending(target); | ||
SchemaRegistry.register(target, cfg); | ||
return target; | ||
@@ -55,3 +55,3 @@ }; | ||
*/ | ||
export function SubType<T>(name: string) { | ||
export function SubType<T>(name?: string) { | ||
return (target: Class<Partial<T>>): void => { | ||
@@ -58,0 +58,0 @@ SchemaRegistry.registerSubTypes(target, name); |
@@ -1,2 +0,3 @@ | ||
import { Class, AppError, ObjectUtil, ClassInstance, ConcreteClass } from '@travetto/base'; | ||
import { RootIndex } from '@travetto/manifest'; | ||
import { Class, AppError } from '@travetto/base'; | ||
import { MetadataRegistry, RootRegistry, ChangeEvent } from '@travetto/registry'; | ||
@@ -8,16 +9,7 @@ | ||
function hasType<T>(o: unknown): o is { type: Class<T> | string } { | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
return !!o && !ObjectUtil.isPrimitive(o) && 'type' in (o as object) && !!(o as Record<string, string>)['type']; | ||
} | ||
const classToSubTypeName = (cls: Class): string => cls.name | ||
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`) | ||
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase()) | ||
.toLowerCase(); | ||
function isWithType<T>(o: T, cfg: ClassConfig | undefined): o is T & { type?: string } { | ||
return !!cfg && !!cfg.subType && 'type' in cfg.views[AllViewⲐ].schema; | ||
} | ||
function getConstructor<T>(o: T): ConcreteClass<T> { | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
return (o as unknown as ClassInstance<T>).constructor; | ||
} | ||
/** | ||
@@ -30,4 +22,4 @@ * Schema registry for listening to changes | ||
#subTypes = new Map<Class, Map<string, Class>>(); | ||
#typeKeys = new Map<Class, string>(); | ||
#pendingViews = new Map<Class, Map<string, ViewFieldsConfig<unknown>>>(); | ||
#baseSchema = new Map<Class, Class>(); | ||
@@ -38,10 +30,18 @@ constructor() { | ||
#computeSubTypeName(cls: Class): string { | ||
if (!this.#typeKeys.has(cls)) { | ||
this.#typeKeys.set(cls, cls.name | ||
.replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`) | ||
.replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase()) | ||
.toLowerCase()); | ||
/** | ||
* Find base schema class for a given class | ||
*/ | ||
getBaseSchema(cls: Class): Class { | ||
if (!this.#baseSchema.has(cls)) { | ||
let conf = this.get(cls) ?? this.getOrCreatePending(cls); | ||
let parent = cls; | ||
while (conf && !conf.baseType) { | ||
parent = this.getParentClass(parent)!; | ||
conf = this.get(parent) ?? this.pending.get(MetadataRegistry.id(parent)); | ||
} | ||
this.#baseSchema.set(cls, conf ? parent : cls); | ||
} | ||
return this.#typeKeys.get(cls)!; | ||
return this.#baseSchema.get(cls)!; | ||
} | ||
@@ -54,5 +54,3 @@ | ||
getSubTypeName(cls: Class): string | undefined { | ||
if (this.get(cls).subType) { | ||
return this.#computeSubTypeName(cls); | ||
} | ||
return this.get(cls).subTypeName; | ||
} | ||
@@ -90,4 +88,8 @@ | ||
ensureInstanceTypeField<T>(cls: Class, o: T): void { | ||
if (isWithType(o, this.get(cls)) && !o.type) { // Do we have a type field defined | ||
o.type = this.#computeSubTypeName(cls); // Assign if missing | ||
const schema = this.get(cls); | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const typeField = (schema.subTypeField) as keyof T; | ||
if (schema.subTypeName && typeField in schema.views[AllViewⲐ].schema && !o[typeField]) { // Do we have a type field defined | ||
// @ts-expect-error | ||
o[typeField] = schema.subTypeName; // Assign if missing | ||
} | ||
@@ -120,23 +122,18 @@ } | ||
resolveSubTypeForInstance<T>(cls: Class<T>, o: T): Class { | ||
return this.resolveSubType(cls, hasType<T>(o) ? o.type : getConstructor(o)); | ||
} | ||
const base = this.getBaseSchema(cls); | ||
const clsSchema = this.get(cls); | ||
const baseSchema = this.get(base); | ||
/** | ||
* Resolve the sub type for a class and a type | ||
* @param cls The base class | ||
* @param type The sub tye value | ||
*/ | ||
resolveSubType(cls: Class, type: Class | string): Class { | ||
if (this.#subTypes.has(cls)) { | ||
const typeId = type && (typeof type === 'string' ? type : type.Ⲑid); | ||
if (type) { | ||
return this.#subTypes.get(cls)!.get(typeId) ?? cls; | ||
if (clsSchema.subTypeName || baseSchema.baseType) { // We have a sub type | ||
// @ts-expect-error | ||
const type = o[baseSchema.subTypeField] ?? clsSchema.subTypeName ?? baseSchema.subTypeName; | ||
const ret = this.#subTypes.get(base)!.get(type)!; | ||
// @ts-expect-error | ||
if (ret && !(new ret() instanceof cls)) { | ||
throw new AppError(`Resolved class ${ret.name} is not assignable to ${cls.name}`); | ||
} | ||
} else if (this.get(cls)?.subType) { | ||
const expectedType = this.#typeKeys.get(cls); | ||
if (expectedType && typeof type === 'string' && expectedType !== type) { | ||
throw new AppError(`Data of type ${type} does not match expected class type ${expectedType}`, 'data'); | ||
} | ||
return ret; | ||
} else { | ||
return cls; | ||
} | ||
return cls; | ||
} | ||
@@ -148,4 +145,5 @@ | ||
*/ | ||
getSubTypesForClass(cls: Class): Map<string, Class> | undefined { | ||
return this.#subTypes.get(cls); | ||
getSubTypesForClass(cls: Class): Class[] | undefined { | ||
const res = this.#subTypes.get(cls)?.values(); | ||
return res ? [...res] : undefined; | ||
} | ||
@@ -156,26 +154,25 @@ | ||
* @param cls The class to register against | ||
* @param type The subtype name | ||
* @param name The subtype name | ||
*/ | ||
registerSubTypes(cls: Class, type?: string): string { | ||
registerSubTypes(cls: Class, name?: string): void { | ||
// Mark as subtype | ||
(this.get(cls) ?? this.getOrCreatePending(cls)).subType = true; | ||
const config = (this.get(cls) ?? this.getOrCreatePending(cls)); | ||
let base: Class | undefined = this.getBaseSchema(cls); | ||
type ??= this.#computeSubTypeName(cls)!; | ||
if (!this.#subTypes.has(base)) { | ||
this.#subTypes.set(base, new Map()); | ||
} | ||
this.#typeKeys.set(cls, type); | ||
let parent = this.getParentClass(cls)!; | ||
let parentConfig = this.get(parent); | ||
while (parentConfig) { | ||
if (!this.#subTypes.has(parent)) { | ||
this.#subTypes.set(parent, new Map()); | ||
if (base !== cls || config.baseType) { | ||
config.subTypeField = (this.get(base) ?? this.getOrCreatePending(base)).subTypeField; | ||
config.subTypeName = name ?? config.subTypeName ?? classToSubTypeName(cls); | ||
this.#subTypes.get(base)!.set(config.subTypeName!, cls); | ||
} | ||
if (base !== cls) { | ||
while (base && ('Ⲑid' in base)) { | ||
this.#subTypes.get(base)!.set(config.subTypeName!, cls); | ||
const parent = this.getParentClass(base); | ||
base = parent ? this.getBaseSchema(parent) : undefined; | ||
} | ||
this.#subTypes.get(parent)!.set(type, cls); | ||
this.#subTypes.get(parent)!.set(cls.Ⲑid, cls); | ||
parent = this.getParentClass(parent!)!; | ||
parentConfig = this.get(parent); | ||
} | ||
return type; | ||
} | ||
@@ -207,3 +204,4 @@ | ||
validators: [], | ||
subType: false, | ||
subTypeField: 'type', | ||
baseType: RootIndex.getFunctionMetadata(cls)?.abstract, | ||
metadata: {}, | ||
@@ -359,3 +357,3 @@ methods: {}, | ||
*/ | ||
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>): ClassConfig { | ||
mergeConfigs(dest: ClassConfig, src: Partial<ClassConfig>, inherited = false): ClassConfig { | ||
dest.views[AllViewⲐ] = { | ||
@@ -365,5 +363,9 @@ schema: { ...dest.views[AllViewⲐ].schema, ...src.views?.[AllViewⲐ].schema }, | ||
}; | ||
if (!inherited) { | ||
dest.baseType = src.baseType ?? dest.baseType; | ||
dest.subTypeName = src.subTypeName ?? dest.subTypeName; | ||
} | ||
dest.methods = { ...src.methods ?? {}, ...dest.methods ?? {} }; | ||
dest.metadata = { ...src.metadata ?? {}, ...dest.metadata ?? {} }; | ||
dest.subType = src.subType || dest.subType; | ||
dest.subTypeField = src.subTypeField ?? dest.subTypeField; | ||
dest.title = src.title || dest.title; | ||
@@ -411,3 +413,3 @@ dest.validators = [...src.validators ?? [], ...dest.validators]; | ||
if (parentConfig) { | ||
config = this.mergeConfigs(config, parentConfig); | ||
config = this.mergeConfigs(config, parentConfig, true); | ||
} | ||
@@ -443,3 +445,3 @@ } | ||
this.#subTypes.clear(); | ||
this.#typeKeys.delete(cls); | ||
this.#baseSchema.delete(cls); | ||
this.#accessorDescriptors.delete(cls); | ||
@@ -446,0 +448,0 @@ |
@@ -67,6 +67,14 @@ import { Primitive, Class } from '@travetto/base'; | ||
/** | ||
* Is the class a sub type | ||
* Is the class a base type | ||
*/ | ||
subType?: boolean; | ||
baseType?: boolean; | ||
/** | ||
* Sub type name | ||
*/ | ||
subTypeName?: string; | ||
/** | ||
* The field the subtype is determined by | ||
*/ | ||
subTypeField: string; | ||
/** | ||
* Metadata that is related to the schema structure | ||
@@ -73,0 +81,0 @@ */ |
89730
2014