UNPKG

@travetto/schema

Version:

Data type registry for runtime validation, reflection and binding.

225 lines (198 loc) 7.45 kB
import ts from 'typescript'; import { type TransformerState, DocUtil, DeclarationUtil, DecoratorUtil, TransformerHandler } from '@travetto/transformer'; import { SchemaTransformUtil } from './transformer/util.ts'; const CONSTRUCTOR_PROPERTY = 'CONSTRUCTOR'; const InSchema = Symbol(); const IsOptIn = Symbol(); const AccessorsSymbol = Symbol(); const AutoEnrollMethods = Symbol(); interface AutoState { [InSchema]?: boolean; [IsOptIn]?: boolean; [AutoEnrollMethods]?: Set<string>; [AccessorsSymbol]?: Set<string>; } /** * Processes `@Schema` to register class as a valid Schema */ export class SchemaTransformer { static { TransformerHandler(this, this.startSchema, 'before', 'class', ['Schema']); TransformerHandler(this, this.finalizeSchema, 'after', 'class', ['Schema']); TransformerHandler(this, this.processSchemaMethod, 'before', 'method'); TransformerHandler(this, this.processSchemaMethod, 'before', 'static-method'); TransformerHandler(this, this.processSchemaField, 'before', 'property'); TransformerHandler(this, this.processSchemaGetter, 'before', 'getter'); TransformerHandler(this, this.processSchemaSetter, 'before', 'setter'); } static isInvisible(state: AutoState & TransformerState, node: ts.Declaration, isStatic?: boolean): boolean { if (!state[InSchema] && !isStatic) { return true; } const ignore = state.findDecorator(this, node, 'Ignore'); if (ignore) { return true; } const manuallyOpted = !!( state.findDecorator(this, node, 'Input') ?? state.findDecorator(this, node, 'Field') ?? state.findDecorator(this, node, 'Method') ); if (manuallyOpted) { return false; } if (ts.isMethodDeclaration(node)) { if (!node.body || !state[AutoEnrollMethods]?.has(node.name.getText())) { return true; } } if (state[IsOptIn] || !DeclarationUtil.isPublic(node)) { return true; } return false; } /** * Track schema on start */ static startSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration { state[AccessorsSymbol] = new Set(); state[AutoEnrollMethods] = new Set(); state[InSchema] = true; // Determine auto enrol methods for (const item of state.getDecoratorList(node)) { if (item.targets?.includes('@travetto/schema:Schema')) { state[IsOptIn] ||= item.options?.includes('opt-in') ?? false; const methodEnrolls = item.options?.filter(option => option.startsWith('method:'))?.map(option => option.replace('method:', '')) ?? []; for (const method of methodEnrolls) { state[AutoEnrollMethods].add(method); } } } return node; } /** * Mark the end of the schema, document */ static finalizeSchema(state: AutoState & TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration { const comments = DocUtil.describeDocs(node); const existing = state.findDecorator(this, node, 'Schema', SchemaTransformUtil.SCHEMA_IMPORT); const cons = node.members.find(member => ts.isConstructorDeclaration(member)); const attrs: Record<string, string | boolean | ts.Expression | number | object | unknown[]> = {}; if (comments.description) { attrs.description = comments.description; } // Extract all interfaces const interfaces: ts.Node[] = []; for (const clause of node.heritageClauses ?? []) { if (clause.token === ts.SyntaxKind.ImplementsKeyword) { for (const typeExpression of clause.types) { const resolvedType = state.resolveType(typeExpression); if (resolvedType.key === 'managed') { const resolved = state.getOrImport(resolvedType); interfaces.push(resolved); } } } } if (interfaces.length > 0) { attrs.interfaces = interfaces; } if (cons) { attrs.methods = { [CONSTRUCTOR_PROPERTY]: { parameters: cons.parameters .map((parameter, i) => SchemaTransformUtil.computeInputDecoratorParams(state, parameter, { index: i })) .map(expr => state.extendObjectLiteral({}, ...expr)), } }; } let params = DecoratorUtil.getArguments(existing) ?? []; if (Object.keys(attrs).length) { params = [...params, state.fromLiteral(attrs)]; } delete state[InSchema]; delete state[IsOptIn]; delete state[AccessorsSymbol]; delete state[AutoEnrollMethods]; return state.factory.updateClassDeclaration( node, DecoratorUtil.spliceDecorators(node, existing, [ state.createDecorator(SchemaTransformUtil.SCHEMA_IMPORT, 'Schema', ...params) ]), node.name, node.typeParameters, node.heritageClauses, node.members ); } /** * Handle explicitly registered methods */ static processSchemaMethod(state: TransformerState & AutoState, node: ts.MethodDeclaration): ts.MethodDeclaration { if ( this.isInvisible(state, node, node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)) && !state[AutoEnrollMethods]?.has(node.name.getText())) { return node; } const existing = state.findDecorator(this, node, 'Method', SchemaTransformUtil.METHOD_IMPORT); const comments = DocUtil.describeDocs(node); const params = DecoratorUtil.getArguments(existing) ?? []; if (comments.description) { params.unshift(state.fromLiteral({ description: comments.description })); } if (DeclarationUtil.isStatic(node)) { params.push(state.fromLiteral({ isStatic: true })); } params.push(...SchemaTransformUtil.computeReturnTypeDecoratorParams(state, node)); return state.factory.updateMethodDeclaration( node, DecoratorUtil.spliceDecorators(node, existing, [ state.createDecorator(SchemaTransformUtil.METHOD_IMPORT, 'Method', ...params) ]), node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters.map((parameter, i) => SchemaTransformUtil.computeInput(state, parameter, { index: i })), node.type, node.body ); } /** * Handle all properties, while in schema */ static processSchemaField(state: TransformerState & AutoState, node: ts.PropertyDeclaration): ts.PropertyDeclaration { if (this.isInvisible(state, node)) { return node; } return SchemaTransformUtil.computeInput(state, node); } /** * Handle getters */ static processSchemaGetter(state: TransformerState & AutoState, node: ts.GetAccessorDeclaration): ts.GetAccessorDeclaration { if (this.isInvisible(state, node) || DeclarationUtil.isStatic(node)) { return node; } if (state[AccessorsSymbol]?.has(node.name.getText())) { return node; } else { state[AccessorsSymbol]?.add(node.name.getText()); return SchemaTransformUtil.computeInput(state, node); } } /** * Handle setters */ static processSchemaSetter(state: TransformerState & AutoState, node: ts.SetAccessorDeclaration): ts.SetAccessorDeclaration { if (this.isInvisible(state, node) || DeclarationUtil.isStatic(node)) { return node; } if (state[AccessorsSymbol]?.has(node.name.getText())) { return node; } else { state[AccessorsSymbol]?.add(node.name.getText()); return SchemaTransformUtil.computeInput(state, node); } } }