fork of hey-api/openapi-ts because I need some additional things
1import type { Ref } from '@hey-api/codegen-core';
2import { fromRef, ref } from '@hey-api/codegen-core';
3
4import type { SchemaWithType } from '../plugins/shared/types/schema';
5import { deduplicateSchema } from './schema';
6import type { IR } from './types';
7
8/**
9 * Context passed to all visitor methods.
10 */
11export interface SchemaVisitorContext<TPlugin = unknown> {
12 /** Current path in the schema tree. */
13 path: Ref<ReadonlyArray<string | number>>;
14 /** The plugin instance. */
15 plugin: TPlugin;
16}
17
18/**
19 * The walk function signature. Fully generic over TResult.
20 */
21export type Walker<TResult, TPlugin = unknown> = (
22 schema: IR.SchemaObject,
23 ctx: SchemaVisitorContext<TPlugin>,
24) => TResult;
25
26/**
27 * The visitor interface. Plugins define their own TResult type.
28 *
29 * The walker handles orchestration (dispatch, deduplication, path tracking).
30 * Result shape and semantics are entirely plugin-defined.
31 */
32export interface SchemaVisitor<TResult, TPlugin = unknown> {
33 /**
34 * Apply modifiers to a result.
35 */
36 applyModifiers(
37 result: TResult,
38 ctx: SchemaVisitorContext<TPlugin>,
39 context?: {
40 /** Is this property optional? */
41 optional?: boolean;
42 },
43 ): unknown;
44 array(
45 schema: SchemaWithType<'array'>,
46 ctx: SchemaVisitorContext<TPlugin>,
47 walk: Walker<TResult, TPlugin>,
48 ): TResult;
49 boolean(schema: SchemaWithType<'boolean'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
50 enum(
51 schema: SchemaWithType<'enum'>,
52 ctx: SchemaVisitorContext<TPlugin>,
53 walk: Walker<TResult, TPlugin>,
54 ): TResult;
55 integer(schema: SchemaWithType<'integer'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
56 /**
57 * Called before any dispatch logic. Return a result to short-circuit,
58 * or undefined to continue normal dispatch.
59 */
60 intercept?(
61 schema: IR.SchemaObject,
62 ctx: SchemaVisitorContext<TPlugin>,
63 walk: Walker<TResult, TPlugin>,
64 ): TResult | undefined;
65 /**
66 * Handle intersection types. Receives already-walked child results.
67 */
68 intersection(
69 items: Array<TResult>,
70 schemas: ReadonlyArray<IR.SchemaObject>,
71 parentSchema: IR.SchemaObject,
72 ctx: SchemaVisitorContext<TPlugin>,
73 ): TResult;
74 never(schema: SchemaWithType<'never'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
75 null(schema: SchemaWithType<'null'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
76 number(schema: SchemaWithType<'number'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
77 object(
78 schema: SchemaWithType<'object'>,
79 ctx: SchemaVisitorContext<TPlugin>,
80 walk: Walker<TResult, TPlugin>,
81 ): TResult;
82 /**
83 * Called after each typed schema visitor returns.
84 */
85 postProcess?(
86 result: TResult,
87 schema: IR.SchemaObject,
88 ctx: SchemaVisitorContext<TPlugin>,
89 ): TResult;
90 /**
91 * Handle $ref to another schema.
92 */
93 reference($ref: string, schema: IR.SchemaObject, ctx: SchemaVisitorContext<TPlugin>): TResult;
94 string(schema: SchemaWithType<'string'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
95 tuple(
96 schema: SchemaWithType<'tuple'>,
97 ctx: SchemaVisitorContext<TPlugin>,
98 walk: Walker<TResult, TPlugin>,
99 ): TResult;
100 undefined(schema: SchemaWithType<'undefined'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
101 /**
102 * Handle union types. Receives already-walked child results.
103 */
104 union(
105 items: Array<TResult>,
106 schemas: ReadonlyArray<IR.SchemaObject>,
107 parentSchema: IR.SchemaObject,
108 ctx: SchemaVisitorContext<TPlugin>,
109 ): TResult;
110 unknown(schema: SchemaWithType<'unknown'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
111 void(schema: SchemaWithType<'void'>, ctx: SchemaVisitorContext<TPlugin>): TResult;
112}
113
114/**
115 * Create a schema walker from a visitor.
116 *
117 * The walker handles:
118 * - Dispatch order ($ref → type → items → fallback)
119 * - Deduplication of union/intersection schemas
120 * - Path tracking for child schemas
121 */
122export function createSchemaWalker<TResult, TPlugin = unknown>(
123 visitor: SchemaVisitor<TResult, TPlugin>,
124): Walker<TResult, TPlugin> {
125 const walk: Walker<TResult, TPlugin> = (schema, ctx) => {
126 // escape hatch
127 if (visitor.intercept) {
128 const intercepted = visitor.intercept(schema, ctx, walk);
129 if (intercepted !== undefined) {
130 return intercepted;
131 }
132 }
133
134 if (schema.$ref) {
135 return visitor.reference(schema.$ref, schema, ctx);
136 }
137
138 if (schema.type) {
139 let result = visitTyped(schema as SchemaWithType, ctx, visitor, walk);
140 if (visitor.postProcess) {
141 result = visitor.postProcess(result, schema, ctx);
142 }
143 return result;
144 }
145
146 if (schema.items) {
147 const deduplicated = deduplicateSchema({ schema });
148
149 // deduplication might collapse to a single schema
150 if (!deduplicated.items) {
151 return walk(deduplicated, ctx);
152 }
153
154 const itemResults = deduplicated.items.map((item, index) =>
155 walk(item, {
156 ...ctx,
157 path: ref([...fromRef(ctx.path), 'items', index]),
158 }),
159 );
160
161 return deduplicated.logicalOperator === 'and'
162 ? visitor.intersection(itemResults, deduplicated.items, schema, ctx)
163 : visitor.union(itemResults, deduplicated.items, schema, ctx);
164 }
165
166 // fallback
167 return visitor.unknown({ type: 'unknown' }, ctx);
168 };
169
170 return walk;
171}
172
173/**
174 * Dispatch to the appropriate visitor method based on schema type.
175 */
176function visitTyped<TResult, TPlugin>(
177 schema: SchemaWithType,
178 ctx: SchemaVisitorContext<TPlugin>,
179 visitor: SchemaVisitor<TResult, TPlugin>,
180 walk: Walker<TResult, TPlugin>,
181): TResult {
182 switch (schema.type) {
183 case 'array':
184 return visitor.array(schema as SchemaWithType<'array'>, ctx, walk);
185 case 'boolean':
186 return visitor.boolean(schema as SchemaWithType<'boolean'>, ctx);
187 case 'enum':
188 return visitor.enum(schema as SchemaWithType<'enum'>, ctx, walk);
189 case 'integer':
190 return visitor.integer(schema as SchemaWithType<'integer'>, ctx);
191 case 'never':
192 return visitor.never(schema as SchemaWithType<'never'>, ctx);
193 case 'null':
194 return visitor.null(schema as SchemaWithType<'null'>, ctx);
195 case 'number':
196 return visitor.number(schema as SchemaWithType<'number'>, ctx);
197 case 'object':
198 return visitor.object(schema as SchemaWithType<'object'>, ctx, walk);
199 case 'string':
200 return visitor.string(schema as SchemaWithType<'string'>, ctx);
201 case 'tuple':
202 return visitor.tuple(schema as SchemaWithType<'tuple'>, ctx, walk);
203 case 'undefined':
204 return visitor.undefined(schema as SchemaWithType<'undefined'>, ctx);
205 case 'unknown':
206 return visitor.unknown(schema as SchemaWithType<'unknown'>, ctx);
207 case 'void':
208 return visitor.void(schema as SchemaWithType<'void'>, ctx);
209 }
210}
211
212/**
213 * Helper to create a child context with an extended path.
214 */
215export function childContext<TPlugin>(
216 ctx: SchemaVisitorContext<TPlugin>,
217 ...segments: ReadonlyArray<string | number>
218): SchemaVisitorContext<TPlugin> {
219 return {
220 ...ctx,
221 path: ref([...fromRef(ctx.path), ...segments]),
222 };
223}