UNPKG

@opra/common

Version:
644 lines (643 loc) 27.3 kB
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; }