import type { Ref } from '@hey-api/codegen-core'; import { fromRef, ref } from '@hey-api/codegen-core'; import type { SchemaWithType } from '../plugins/shared/types/schema'; import { deduplicateSchema } from './schema'; import type { IR } from './types'; /** * Context passed to all visitor methods. */ export interface SchemaVisitorContext { /** Current path in the schema tree. */ path: Ref>; /** The plugin instance. */ plugin: TPlugin; } /** * The walk function signature. Fully generic over TResult. */ export type Walker = ( schema: IR.SchemaObject, ctx: SchemaVisitorContext, ) => TResult; /** * The visitor interface. Plugins define their own TResult type. * * The walker handles orchestration (dispatch, deduplication, path tracking). * Result shape and semantics are entirely plugin-defined. */ export interface SchemaVisitor { /** * Apply modifiers to a result. */ applyModifiers( result: TResult, ctx: SchemaVisitorContext, context?: { /** Is this property optional? */ optional?: boolean; }, ): unknown; array( schema: SchemaWithType<'array'>, ctx: SchemaVisitorContext, walk: Walker, ): TResult; boolean(schema: SchemaWithType<'boolean'>, ctx: SchemaVisitorContext): TResult; enum( schema: SchemaWithType<'enum'>, ctx: SchemaVisitorContext, walk: Walker, ): TResult; integer(schema: SchemaWithType<'integer'>, ctx: SchemaVisitorContext): TResult; /** * Called before any dispatch logic. Return a result to short-circuit, * or undefined to continue normal dispatch. */ intercept?( schema: IR.SchemaObject, ctx: SchemaVisitorContext, walk: Walker, ): TResult | undefined; /** * Handle intersection types. Receives already-walked child results. */ intersection( items: Array, schemas: ReadonlyArray, parentSchema: IR.SchemaObject, ctx: SchemaVisitorContext, ): TResult; never(schema: SchemaWithType<'never'>, ctx: SchemaVisitorContext): TResult; null(schema: SchemaWithType<'null'>, ctx: SchemaVisitorContext): TResult; number(schema: SchemaWithType<'number'>, ctx: SchemaVisitorContext): TResult; object( schema: SchemaWithType<'object'>, ctx: SchemaVisitorContext, walk: Walker, ): TResult; /** * Called after each typed schema visitor returns. */ postProcess?( result: TResult, schema: IR.SchemaObject, ctx: SchemaVisitorContext, ): TResult; /** * Handle $ref to another schema. */ reference($ref: string, schema: IR.SchemaObject, ctx: SchemaVisitorContext): TResult; string(schema: SchemaWithType<'string'>, ctx: SchemaVisitorContext): TResult; tuple( schema: SchemaWithType<'tuple'>, ctx: SchemaVisitorContext, walk: Walker, ): TResult; undefined(schema: SchemaWithType<'undefined'>, ctx: SchemaVisitorContext): TResult; /** * Handle union types. Receives already-walked child results. */ union( items: Array, schemas: ReadonlyArray, parentSchema: IR.SchemaObject, ctx: SchemaVisitorContext, ): TResult; unknown(schema: SchemaWithType<'unknown'>, ctx: SchemaVisitorContext): TResult; void(schema: SchemaWithType<'void'>, ctx: SchemaVisitorContext): TResult; } /** * Create a schema walker from a visitor. * * The walker handles: * - Dispatch order ($ref → type → items → fallback) * - Deduplication of union/intersection schemas * - Path tracking for child schemas */ export function createSchemaWalker( visitor: SchemaVisitor, ): Walker { const walk: Walker = (schema, ctx) => { // escape hatch if (visitor.intercept) { const intercepted = visitor.intercept(schema, ctx, walk); if (intercepted !== undefined) { return intercepted; } } if (schema.$ref) { return visitor.reference(schema.$ref, schema, ctx); } if (schema.type) { let result = visitTyped(schema as SchemaWithType, ctx, visitor, walk); if (visitor.postProcess) { result = visitor.postProcess(result, schema, ctx); } return result; } if (schema.items) { const deduplicated = deduplicateSchema({ schema }); // deduplication might collapse to a single schema if (!deduplicated.items) { return walk(deduplicated, ctx); } const itemResults = deduplicated.items.map((item, index) => walk(item, { ...ctx, path: ref([...fromRef(ctx.path), 'items', index]), }), ); return deduplicated.logicalOperator === 'and' ? visitor.intersection(itemResults, deduplicated.items, schema, ctx) : visitor.union(itemResults, deduplicated.items, schema, ctx); } // fallback return visitor.unknown({ type: 'unknown' }, ctx); }; return walk; } /** * Dispatch to the appropriate visitor method based on schema type. */ function visitTyped( schema: SchemaWithType, ctx: SchemaVisitorContext, visitor: SchemaVisitor, walk: Walker, ): TResult { switch (schema.type) { case 'array': return visitor.array(schema as SchemaWithType<'array'>, ctx, walk); case 'boolean': return visitor.boolean(schema as SchemaWithType<'boolean'>, ctx); case 'enum': return visitor.enum(schema as SchemaWithType<'enum'>, ctx, walk); case 'integer': return visitor.integer(schema as SchemaWithType<'integer'>, ctx); case 'never': return visitor.never(schema as SchemaWithType<'never'>, ctx); case 'null': return visitor.null(schema as SchemaWithType<'null'>, ctx); case 'number': return visitor.number(schema as SchemaWithType<'number'>, ctx); case 'object': return visitor.object(schema as SchemaWithType<'object'>, ctx, walk); case 'string': return visitor.string(schema as SchemaWithType<'string'>, ctx); case 'tuple': return visitor.tuple(schema as SchemaWithType<'tuple'>, ctx, walk); case 'undefined': return visitor.undefined(schema as SchemaWithType<'undefined'>, ctx); case 'unknown': return visitor.unknown(schema as SchemaWithType<'unknown'>, ctx); case 'void': return visitor.void(schema as SchemaWithType<'void'>, ctx); } } /** * Helper to create a child context with an extended path. */ export function childContext( ctx: SchemaVisitorContext, ...segments: ReadonlyArray ): SchemaVisitorContext { return { ...ctx, path: ref([...fromRef(ctx.path), ...segments]), }; }