@opra/common
Version:
Opra common package
644 lines (643 loc) • 27.3 kB
JavaScript
import { cloneObject, resolveThunk, ResponsiveMap, } from '../../helpers/index.js';
import { OpraSchema } from '../../schema/index.js';
import { DocumentInitContext } from '../common/document-init-context.js';
import { DATATYPE_METADATA, DECODER, ENCODER, kCtorMap, kDataTypeMap, } from '../constants.js';
import { ArrayType } from '../data-type/array-type.js';
import { ComplexType } from '../data-type/complex-type.js';
import { ComplexTypeBase } from '../data-type/complex-type-base.js';
import { DataType } from '../data-type/data-type.js';
import { EnumType } from '../data-type/enum-type.js';
import { MappedType } from '../data-type/mapped-type.js';
import { MixinType } from '../data-type/mixin-type.js';
import { SimpleType } from '../data-type/simple-type.js';
import { UnionType } from '../data-type/union-type.js';
const initializingSymbol = Symbol('initializing');
/**
*
* @class DataTypeFactory
*/
export class DataTypeFactory {
static async createDataType(context, owner, thunk) {
context = context || new DocumentInitContext({ maxErrors: 0 });
const initArgs = await this._importDataTypeArgs(context, owner, thunk);
if (initArgs) {
if (typeof initArgs === 'string')
return owner.node.getDataType(initArgs);
return this._createDataType(context, owner, initArgs);
}
}
static async resolveDataType(context, owner, v) {
if (v) {
const dt = owner.node.findDataType(v);
if (dt)
return dt;
}
if (typeof v === 'object' || typeof v === 'function') {
const dt = await DataTypeFactory.createDataType(context, owner, v);
if (dt)
return dt;
}
if (v) {
/** To throw not found error */
const dt = owner.node.getDataType(v);
/** istanbul ignore next */
if (dt)
return dt;
}
return owner.node.getDataType('any');
}
/**
*
* @param context
* @param target
* @param types
*/
static async addDataTypes(context, target, types) {
const dataTypeMap = target.node[kDataTypeMap];
if (!dataTypeMap)
throw new TypeError('DocumentElement should has [kDataTypeMap] property');
const importedTypes = await this.createAllDataTypes(context, target, types);
if (!importedTypes)
return;
for (const dataType of importedTypes) {
if (dataType?.name) {
dataTypeMap.set(dataType.name, dataType);
}
}
}
/**
*
* @param context
* @param owner
* @param types
*/
static async createAllDataTypes(context, owner, types) {
context = context || new DocumentInitContext({ maxErrors: 0 });
const initArgs = await this._prepareAllInitArgs(context, owner, types);
if (!initArgs)
return;
const initArgsMap = new ResponsiveMap();
for (const initArg of initArgs) {
initArgsMap.set(initArg.name, initArg);
}
const ctx = context.extend({ initArgsMap });
const out = [];
for (const [name, initArg] of initArgsMap.entries()) {
context.enter(`[${name}]`, () => {
if (!initArgsMap.has(name))
return;
const dataType = this._createDataType(ctx, owner, initArg);
if (dataType)
out.push(dataType);
});
}
return out;
}
/**
*
* @param ctx
* @param owner
* @param types
*/
static async _prepareAllInitArgs(ctx, owner, types) {
const importQueue = new ResponsiveMap();
const initArgsMap = new ResponsiveMap();
const context = ctx.extend({ owner, importQueue, initArgsMap });
// istanbul ignore next
if (!owner.node[kDataTypeMap])
throw new TypeError('DocumentElement should has [kDataTypeMap] property');
// Add type sources into typeQueue
if (Array.isArray(types)) {
let i = 0;
for (let thunk of types) {
await context.enterAsync(`$[${i++}]`, async () => {
thunk = await resolveThunk(thunk);
const metadata = Reflect.getMetadata(DATATYPE_METADATA, thunk) ||
thunk[DATATYPE_METADATA];
if (!(metadata && metadata.name)) {
if (typeof thunk === 'function') {
return context.addError(`Class "${thunk.name}" doesn't have a valid data type metadata`);
}
return context.addError(`Object doesn't have a valid data type metadata`);
}
importQueue.set(metadata.name, thunk);
});
}
}
else {
let thunk;
let name;
for ([name, thunk] of Object.entries(types)) {
thunk = await resolveThunk(thunk);
importQueue.set(name, typeof thunk === 'object' ? { ...thunk, name } : thunk);
}
}
for (const name of Array.from(importQueue.keys())) {
if (!importQueue.has(name))
continue;
const dt = await this._importDataTypeArgs(context, owner, name);
// istanbul ignore next
if (dt && typeof dt !== 'string') {
context.addError(`Embedded data type can't be loaded into document node directly`);
}
}
return Array.from(initArgsMap.values());
}
/**
*
* @param context
* @param owner
* @param thunk
* @param checkCircularDeps
* @protected
*/
static async _importDataTypeArgs(context, owner, thunk, checkCircularDeps) {
thunk = await resolveThunk(thunk);
const { importQueue, initArgsMap } = context;
// Check if data type already exist (maybe a builtin type or already imported)
const dataType = owner.node.findDataType(thunk);
if (dataType instanceof DataType)
return dataType.name;
let metadata;
let ctor;
let instance;
if (typeof thunk !== 'string') {
const _ctorTypeMap = owner.node.getDocument().types[kCtorMap];
const name = _ctorTypeMap.get(thunk);
if (name)
thunk = name;
}
if (typeof thunk === 'string') {
const name = thunk;
thunk = importQueue?.get(name) || context.initArgsMap?.get(name);
if (!thunk)
return context.addError(`Unknown data type (${name})`);
}
else {
//
}
if (typeof thunk === 'function') {
// Check if class has metadata
metadata = Reflect.getMetadata(DATATYPE_METADATA, thunk);
if (!metadata)
return context.addError(`Class "${thunk.name}" doesn't have a valid DataType metadata`);
ctor = thunk;
}
else if (typeof thunk === 'object') {
// It may be an enum object or enum array
metadata = thunk[DATATYPE_METADATA];
if (metadata) {
instance = thunk;
if (metadata.kind !== OpraSchema.EnumType.Kind)
metadata = undefined;
}
else if (OpraSchema.isDataType(thunk)) {
metadata = thunk;
ctor = metadata.ctor;
}
else {
// Or may be a DataType instance, e.g. an extended SimpleType object
ctor = Object.getPrototypeOf(thunk).constructor;
metadata = ctor && Reflect.getMetadata(DATATYPE_METADATA, ctor);
if (metadata) {
if (metadata.kind === OpraSchema.SimpleType.Kind) {
const baseArgs = await this._importDataTypeArgs(context, owner, metadata.name);
if (!baseArgs)
return;
if (typeof baseArgs === 'object' &&
baseArgs.kind !== OpraSchema.SimpleType.Kind) {
return context.addError('Kind of base data type is not same');
}
return {
kind: OpraSchema.SimpleType.Kind,
name: undefined,
base: baseArgs,
properties: thunk,
};
}
}
}
}
if (!metadata) {
return context.addError(`No DataType metadata found`);
}
return context.enterAsync(metadata.name ? `[${metadata.name}]` : '', async () => {
/** Check for circular dependencies */
if (metadata.name) {
const curr = initArgsMap?.get(metadata.name);
if (curr) {
if (checkCircularDeps && curr[initializingSymbol])
return context.addError('Circular reference detected');
return metadata.name;
}
}
const out = {
kind: metadata.kind,
name: metadata.name,
};
/** Mark "out" object as initializing. This will help us to detect circular dependencies */
out[initializingSymbol] = true;
try {
if (out.name) {
if (importQueue?.has(out.name)) {
initArgsMap?.set(metadata.name, out);
out._instance = { name: metadata.name };
out[kDataTypeMap] = owner.node[kDataTypeMap];
}
else {
return context.addError(`Data Type (${out.name}) must be explicitly added to type list in the document scope`);
}
}
switch (out.kind) {
case OpraSchema.ArrayType.Kind:
await this._prepareArrayTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.ComplexType.Kind:
out.ctor = ctor;
await this._prepareComplexTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.EnumType.Kind:
out.instance = instance;
await this._prepareEnumTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.MappedType.Kind:
await this._prepareMappedTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.MixinType.Kind:
await this._prepareMixinTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.SimpleType.Kind:
out.ctor = ctor;
await this._prepareSimpleTypeArgs(context, owner, out, metadata);
break;
case OpraSchema.UnionType.Kind:
await this._prepareUnionTypeArgs(context, owner, out, metadata);
break;
default:
/** istanbul ignore next */
return context.addError(`Invalid data type kind ${metadata.kind}`);
}
}
finally {
if (out.name)
importQueue?.delete(out.name);
delete out[initializingSymbol];
}
return importQueue && out.name ? out.name : out;
});
}
static async _prepareDataTypeArgs(context, initArgs, metadata) {
initArgs.description = metadata.description;
initArgs.abstract = metadata.abstract;
initArgs.examples = metadata.examples;
initArgs.scopePattern = metadata.scopePattern;
}
static async _prepareArrayTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
await context.enterAsync('.type', async () => {
const baseArgs = await this._importDataTypeArgs(context, owner, metadata.type);
if (!baseArgs)
return;
initArgs.type = preferName(baseArgs);
});
}
static async _prepareComplexTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
initArgs.keyField = metadata.keyField;
initArgs.discriminatorField = metadata.discriminatorField;
initArgs.discriminatorValue = metadata.discriminatorValue;
await context.enterAsync('.base', async () => {
let baseArgs;
if (metadata.base) {
baseArgs = await this._importDataTypeArgs(context, owner, metadata.base, true);
}
else if (initArgs.ctor) {
const baseClass = Object.getPrototypeOf(initArgs.ctor.prototype).constructor;
if (Reflect.hasMetadata(DATATYPE_METADATA, baseClass)) {
baseArgs = await this._importDataTypeArgs(context, owner, baseClass);
}
}
if (!baseArgs)
return;
initArgs.base = preferName(baseArgs);
initArgs.ctor = initArgs.ctor || baseArgs.ctor;
});
// Initialize additionalFields
if (metadata.additionalFields != null) {
if (typeof metadata.additionalFields === 'boolean' ||
Array.isArray(metadata.additionalFields)) {
initArgs.additionalFields = metadata.additionalFields;
}
else {
await context.enterAsync('.additionalFields', async () => {
const t = await this._importDataTypeArgs(context, owner, metadata.additionalFields);
if (t)
initArgs.additionalFields = preferName(t);
});
}
}
if (metadata.fields) {
initArgs.fields = {};
await context.enterAsync('.fields', async () => {
for (const [k, v] of Object.entries(metadata.fields)) {
await context.enterAsync(`[${k}]`, async () => {
const fieldMeta = typeof v === 'string' ? { type: v } : v;
if (fieldMeta.isArray && !fieldMeta.type) {
return context.addError(`"type" must be defined explicitly for array fields`);
}
const t = await this._importDataTypeArgs(context, owner, fieldMeta.type || 'any');
if (!t)
return;
initArgs.fields[k] = {
...fieldMeta,
type: preferName(t),
};
});
}
});
}
}
static async _prepareEnumTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
if (metadata.base) {
await context.enterAsync('.base', async () => {
const baseArgs = await this._importDataTypeArgs(context, owner, metadata.base);
if (!baseArgs)
return;
initArgs.base = preferName(baseArgs);
});
}
initArgs.attributes = cloneObject(metadata.attributes);
}
static async _prepareSimpleTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
await context.enterAsync('.base', async () => {
let baseArgs;
if (metadata.base) {
baseArgs = await this._importDataTypeArgs(context, owner, metadata.base, true);
}
else if (initArgs.ctor) {
const baseClass = Object.getPrototypeOf(initArgs.ctor.prototype).constructor;
if (Reflect.hasMetadata(DATATYPE_METADATA, baseClass)) {
baseArgs = await this._importDataTypeArgs(context, owner, baseClass);
}
}
if (!baseArgs)
return;
initArgs.base = preferName(baseArgs);
initArgs.ctor = initArgs.ctor || baseArgs.ctor;
});
initArgs.properties = metadata.properties;
initArgs.nameMappings = metadata.nameMappings;
if (!initArgs.properties && initArgs.ctor)
initArgs.properties = new initArgs.ctor();
if (metadata.attributes)
initArgs.attributes = cloneObject(metadata.attributes);
if (typeof initArgs.properties?.[DECODER] === 'function') {
initArgs.generateDecoder = initArgs.properties?.[DECODER].bind(initArgs.properties);
}
if (typeof initArgs.properties?.[ENCODER] === 'function') {
initArgs.generateEncoder = initArgs.properties?.[ENCODER].bind(initArgs.properties);
}
}
static async _prepareMappedTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
initArgs.discriminatorField = metadata.discriminatorField;
initArgs.discriminatorValue = metadata.discriminatorValue;
await context.enterAsync('.base', async () => {
let baseArgs;
if (metadata.base) {
baseArgs = await this._importDataTypeArgs(context, owner, metadata.base, true);
}
else if (initArgs.ctor) {
const baseClass = Object.getPrototypeOf(initArgs.ctor.prototype).constructor;
if (Reflect.hasMetadata(DATATYPE_METADATA, baseClass)) {
baseArgs = await this._importDataTypeArgs(context, owner, baseClass);
}
}
if (!baseArgs)
return;
initArgs.base = preferName(baseArgs);
initArgs.ctor = initArgs.ctor || baseArgs.ctor;
});
if (metadata.pick)
initArgs.pick = [...metadata.pick];
else if (metadata.omit)
initArgs.omit = [...metadata.omit];
else if (metadata.partial) {
initArgs.partial = Array.isArray(metadata.partial)
? [...metadata.partial]
: metadata.partial;
}
else if (metadata.required) {
initArgs.required = Array.isArray(metadata.required)
? [...metadata.required]
: metadata.required;
}
}
static async _prepareMixinTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
initArgs.types = [];
await context.enterAsync('.types', async () => {
const _initTypes = metadata.types;
let i = 0;
for (const t of _initTypes) {
await context.enterAsync(`[${i++}]`, async () => {
const baseArgs = await this._importDataTypeArgs(context, owner, t);
if (!baseArgs)
return;
initArgs.types.push(preferName(baseArgs));
});
}
});
}
static async _prepareUnionTypeArgs(context, owner, initArgs, metadata) {
await this._prepareDataTypeArgs(context, initArgs, metadata);
initArgs.types = [];
await context.enterAsync('.types', async () => {
const _initTypes = metadata.types;
let i = 0;
for (const t of _initTypes) {
await context.enterAsync(`[${i++}]`, async () => {
const baseArgs = await this._importDataTypeArgs(context, owner, t);
if (!baseArgs)
return;
initArgs.types.push(preferName(baseArgs));
});
}
});
}
static _createDataType(context, owner, args) {
let dataType = owner.node.findDataType(typeof args === 'string' ? args : args.name || '');
if (dataType instanceof DataType)
return dataType;
const initArgs = typeof args === 'string' ? context.initArgsMap?.get(args) : args;
if (initArgs) {
const dataTypeMap = initArgs[kDataTypeMap];
if (!dataTypeMap)
delete initArgs._instance;
dataType = initArgs._instance;
if (dataType?.name && dataTypeMap) {
dataTypeMap.set(dataType.name, dataType);
}
switch (initArgs?.kind) {
case OpraSchema.ArrayType.Kind:
return this._createArrayType(context, owner, initArgs);
case OpraSchema.ComplexType.Kind:
return this._createComplexType(context, owner, initArgs);
case OpraSchema.EnumType.Kind:
return this._createEnumType(context, owner, initArgs);
case OpraSchema.MappedType.Kind:
return this._createMappedType(context, owner, initArgs);
case OpraSchema.MixinType.Kind:
return this._createMixinType(context, owner, initArgs);
case OpraSchema.SimpleType.Kind:
return this._createSimpleType(context, owner, initArgs);
case OpraSchema.UnionType.Kind:
return this._createUnionType(context, owner, initArgs);
default:
break;
}
}
context.addError(`Unknown data type (${String(args)})`);
}
static _createArrayType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, ArrayType.prototype);
const initArgs = cloneObject(args);
if (args.type) {
context.enter('.type', () => {
initArgs.type = this._createDataType(context, owner, args.type);
});
}
ArrayType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createComplexType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, ComplexType.prototype);
const initArgs = cloneObject(args);
if (args.base) {
context.enter('.base', () => {
initArgs.base = this._createDataType(context, owner, args.base);
});
}
/** Set additionalFields */
if (args.additionalFields) {
context.enter('.additionalFields', () => {
if (typeof args.additionalFields === 'boolean' ||
Array.isArray(args.additionalFields)) {
initArgs.additionalFields = args.additionalFields;
}
else {
initArgs.additionalFields = this._createDataType(context, owner, args.additionalFields);
}
});
}
/** Add own fields */
initArgs.fields = {};
if (args.fields) {
context.enter('.fields', () => {
for (const [k, v] of Object.entries(args.fields)) {
context.enter(`[${k}]`, () => {
const type = this._createDataType(context, owner, v.type);
if (type) {
initArgs.fields[k] = {
...v,
name: k,
type,
};
}
});
}
});
}
ComplexType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createEnumType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, EnumType.prototype);
const initArgs = cloneObject(args);
if (args.base) {
context.enter('.base', () => {
initArgs.base = this._createDataType(context, owner, args.base);
});
}
initArgs.attributes = args.attributes;
EnumType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createMappedType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, MappedType.prototype);
const initArgs = cloneObject(args);
if (args.base) {
context.enter('.base', () => {
initArgs.base = this._createDataType(context, owner, args.base);
});
}
MappedType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createMixinType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, MixinType.prototype);
const initArgs = cloneObject(args);
if (args.types) {
context.enter('.types', () => {
initArgs.types = [];
let i = 0;
for (const t of args.types) {
context.enter(`[${i++}]`, () => {
const base = this._createDataType(context, owner, t);
if (!(base instanceof ComplexTypeBase)) {
throw new TypeError(`"${base?.kind}" can't be set as base for a "${initArgs.kind}"`);
}
initArgs.types.push(base);
});
}
});
}
MixinType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createSimpleType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, SimpleType.prototype);
const initArgs = cloneObject(args);
if (args.base) {
context.enter('.base', () => {
initArgs.base = this._createDataType(context, owner, args.base);
});
}
SimpleType.apply(dataType, [owner, initArgs]);
return dataType;
}
static _createUnionType(context, owner, args) {
const dataType = args._instance || {};
Object.setPrototypeOf(dataType, UnionType.prototype);
const initArgs = cloneObject(args);
if (args.types) {
context.enter('.types', () => {
initArgs.types = [];
let i = 0;
for (const t of args.types) {
context.enter(`[${i++}]`, () => {
const base = this._createDataType(context, owner, t);
initArgs.types.push(base);
});
}
});
}
UnionType.apply(dataType, [owner, initArgs]);
return dataType;
}
}
function preferName(initArgs) {
return typeof initArgs === 'object'
? initArgs.name
? initArgs.name
: initArgs
: initArgs;
}