fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 223 lines 7.1 kB view raw
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}