// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import { TypeAssertion, TypeAssertionMap, TypeAssertionSetValue, ObjectAssertion, AssertionSymlink, SymbolResolverOperators, ResolveSymbolOptions, SymbolResolverContext } from '../types'; import * as operators from '../operators'; import { NumberPattern } from '../lib/util'; function mergeTypeAndSymlink(ty: TypeAssertion, link: AssertionSymlink): TypeAssertion { const link2 = {...link}; delete (link2 as any).kind; // NOTE: (TS>=4.0) TS2790: The operand of a 'delete' operator must be optional. delete (link2 as any).symlinkTargetName; // NOTE: (TS>=4.0) TS2790: The operand of a 'delete' operator must be optional. delete (link2 as any).memberTree; // NOTE: (TS>=4.0) TS2790: The operand of a 'delete' operator must be optional. return ({...ty, ...link2} as any as TypeAssertion); } function updateSchema(original: TypeAssertion, schema: TypeAssertionMap, ty: TypeAssertion, typeName: string | undefined) { if (typeName && schema.has(typeName)) { const z: TypeAssertionSetValue = schema.get(typeName) as TypeAssertionSetValue; if (z.ty === original) { schema.set(typeName, {...z, ty, resolved: true}); } } return ty; } export function resolveMemberNames( ty: TypeAssertion, rootSym: string, memberTreeSymbols: string[], memberPos: number): TypeAssertion { const addTypeName = (mt: TypeAssertion, typeName: string | undefined, memberSym: string) => { if (typeName) { return ({ ...mt, typeName: memberPos === 0 ? `${rootSym}.${memberTreeSymbols.join('.')}` : `${typeName}.${memberSym}`, }); } else { return mt; } }; for (let i = memberPos; i < memberTreeSymbols.length; i++) { const memberSym = memberTreeSymbols[i]; switch (ty.kind) { case 'optional': return resolveMemberNames(ty.optional, rootSym, memberTreeSymbols, i + 1); case 'object': for (const m of ty.members) { if (memberSym === m[0]) { return addTypeName( resolveMemberNames(m[1], rootSym, memberTreeSymbols, i + 1), ty.typeName, memberSym, ); } } if (ty.additionalProps) { for (const m of ty.additionalProps) { for (const k of m[0]) { switch (k) { case 'number': if (NumberPattern.test(memberSym)) { return resolveMemberNames(m[1], rootSym, memberTreeSymbols, i + 1); } break; case 'string': return resolveMemberNames(m[1], rootSym, memberTreeSymbols, i + 1); default: if (k.test(memberSym)) { return resolveMemberNames(m[1], rootSym, memberTreeSymbols, i + 1); } break; } } } } throw new Error(`Undefined member name is appeared: ${memberSym}`); case 'symlink': if (! ty.typeName) { throw new Error(`Reference of anonymous type is appeared: ${memberSym}`); } return ({ ...{ kind: 'symlink', symlinkTargetName: rootSym, name: memberSym, typeName: rootSym, }, ...(0 < memberTreeSymbols.length ? { memberTree: memberTreeSymbols, } : {}), }); default: // TODO: kind === 'operator' throw new Error(`Unsupported type kind is appeared: (kind:${ty.kind}).${memberSym}`); } } return ty; } export function resolveSymbols(schema: TypeAssertionMap, ty: TypeAssertion, ctx: SymbolResolverContext): TypeAssertion { const ctx2 = {...ctx, nestLevel: ctx.nestLevel + 1}; switch (ty.kind) { case 'symlink': { const x = schema.get(ty.symlinkTargetName); if (! x) { throw new Error(`Undefined symbol '${ty.symlinkTargetName}' is referred.`); } if (0 <= ctx.symlinkStack.findIndex(s => s === ty.symlinkTargetName)) { return ty; } const ty2 = {...ty}; let xTy = x.ty; if (ty.memberTree && 0 < ty.memberTree.length) { xTy = { ...resolveMemberNames(xTy, ty.symlinkTargetName, ty.memberTree, 0), }; ty2.typeName = xTy.typeName; } return ( resolveSymbols( schema, mergeTypeAndSymlink(xTy, ty2), {...ctx2, symlinkStack: [...ctx2.symlinkStack, ty2.symlinkTargetName]}, ) ); } case 'repeated': return updateSchema(ty, schema, { ...ty, repeated: resolveSymbols(schema, ty.repeated, ctx2), }, ty.typeName); case 'spread': return updateSchema(ty, schema, { ...ty, spread: resolveSymbols(schema, ty.spread, ctx2), }, ty.typeName); case 'sequence': return updateSchema(ty, schema, { ...ty, sequence: ty.sequence.map(x => resolveSymbols(schema, x, ctx2)), }, ty.typeName); case 'one-of': return updateSchema(ty, schema, { ...ty, oneOf: ty.oneOf.map(x => resolveSymbols(schema, x, ctx2)), }, ty.typeName); case 'optional': return updateSchema(ty, schema, { ...ty, optional: resolveSymbols(schema, ty.optional, ctx2), }, ty.typeName); case 'object': { if (0 < ctx.nestLevel && ty.typeName && 0 <= ctx.symlinkStack.findIndex(s => s === ty.typeName)) { if (schema.has(ty.typeName)) { const z = schema.get(ty.typeName) as TypeAssertionSetValue; if (z.resolved) { return z.ty; } } } const baseSymlinks = ty.baseTypes?.filter(x => x.kind === 'symlink') as AssertionSymlink[]; if (baseSymlinks && baseSymlinks.length > 0 && !ctx.isDeserialization) { const exts = baseSymlinks .map(x => resolveSymbols(schema, x, ctx2)) .filter(x => x.kind === 'object'); // TODO: if x.kind !== 'object' items exist -> error? const d2 = resolveSymbols( schema, operators.derived({ ...ty, ...(ty.baseTypes ? { baseTypes: ty.baseTypes.filter(x => x.kind !== 'symlink'), } : {}), }, ...exts), ty.typeName ? {...ctx2, symlinkStack: [...ctx2.symlinkStack, ty.typeName]} : ctx2, ); return updateSchema(ty, schema, { ...ty, ...d2, }, ty.typeName); } else { return updateSchema(ty, schema, { ...{ ...ty, members: ty.members .map(x => [ x[0], resolveSymbols(schema, x[1], ty.typeName ? {...ctx2, symlinkStack: [...ctx2.symlinkStack, ty.typeName]} : ctx2), ...x.slice(2), ] as any), }, ...(ty.additionalProps && 0 < ty.additionalProps.length ? { additionalProps: ty.additionalProps .map(x => [ x[0], resolveSymbols(schema, x[1], ty.typeName ? {...ctx2, symlinkStack: [...ctx2.symlinkStack, ty.typeName]} : ctx2), ...x.slice(2), ] as any), } : {}), ...(ty.baseTypes && 0 < ty.baseTypes.length ? { baseTypes: ctx.isDeserialization ? ty.baseTypes .map(x => x.kind === 'symlink' ? resolveSymbols(schema, x, ctx2) : x) .filter(x => x.kind === 'object') as ObjectAssertion[] : ty.baseTypes, } : {}), }, ty.typeName); } } case 'operator': if (ctx2.operators) { const ctx3 = ty.typeName ? {...ctx2, symlinkStack: [...ctx2.symlinkStack, ty.typeName]} : ctx2; const operands = ty.operands.map(x => { if (typeof x === 'object' && x.kind) { return resolveSymbols(schema, x, ctx3); } return x; }); if (0 < operands.filter(x => x && typeof x === 'object' && (x.kind === 'symlink' || x.kind === 'operator')).length) { throw new Error(`Unresolved type operator is found: ${ty.operator}`); } if (! ctx2.operators[ty.operator]) { throw new Error(`Undefined type operator is found: ${ty.operator}`); } const ty2 = {...ty}; delete (ty2 as any).operator; // NOTE: (TS>=4.0) TS2790: The operand of a 'delete' operator must be optional. delete (ty2 as any).operands; // NOTE: (TS>=4.0) TS2790: The operand of a 'delete' operator must be optional. return updateSchema( ty, schema, { ...ty2, ...resolveSymbols(schema, ctx2.operators[ty.operator](...operands), ctx3), }, ty.typeName, ); } else { return ty; } default: return ty; } } const resolverOps: SymbolResolverOperators = { picked: operators.picked, omit: operators.omit, partial: operators.partial, intersect: operators.intersect, subtract: operators.subtract, }; export function resolveSchema(schema: TypeAssertionMap, opts?: ResolveSymbolOptions): TypeAssertionMap { for (const ent of schema.entries()) { const ty = resolveSymbols(schema, ent[1].ty, {...opts, nestLevel: 0, symlinkStack: [ent[0]], operators: resolverOps}); ent[1].ty = ty; } return schema; }