fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: allow path in top-level detect function

Lubos 27cd91f5 23b15744

+1378 -1068
+5
.changeset/fluffy-words-dress.md
··· 1 + --- 2 + "@hey-api/shared": minor 3 + --- 4 + 5 + **utils**: rename `isTopLevelComponentRef` to `isTopLevelComponent`
+5
.changeset/smart-comics-talk.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **internal**: use shared schema processor
+1 -1
dev/openapi-python.config.ts
··· 12 12 path: './logs', 13 13 }, 14 14 output: { 15 - path: path.resolve(__dirname, '..', '.gen', 'python'), 15 + path: path.resolve(__dirname, '.gen', 'python'), 16 16 }, 17 17 plugins: getPreset(), 18 18 },
+1 -1
dev/openapi-ts.config.ts
··· 12 12 path: './logs', 13 13 }, 14 14 output: { 15 - path: path.resolve(__dirname, '..', '.gen', 'typescript'), 15 + path: path.resolve(__dirname, '.gen', 'typescript'), 16 16 }, 17 17 plugins: getPreset(), 18 18 },
+5 -1
packages/openapi-python/src/plugins/pydantic/shared/types.ts
··· 1 1 import type { Refs, Symbol, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { IR, SchemaExtractor } from '@hey-api/shared'; 3 3 4 4 import type { $ } from '../../../py-dsl'; 5 5 import type { PydanticPlugin } from '../types'; ··· 29 29 }; 30 30 31 31 export type IrSchemaToAstOptions = { 32 + /** The plugin instance. */ 32 33 plugin: PydanticPlugin['Instance']; 34 + /** Optional schema extractor function. */ 35 + schemaExtractor?: SchemaExtractor; 36 + /** The plugin state references. */ 33 37 state: Refs<PluginState>; 34 38 }; 35 39
+13
packages/openapi-python/src/plugins/pydantic/v2/plugin.ts
··· 13 13 optional, 14 14 plugin, 15 15 schema, 16 + schemaExtractor, 16 17 state, 17 18 }: IrSchemaToAstOptions & { 18 19 optional?: boolean; 19 20 schema: IR.SchemaObject; 20 21 }): Ast { 22 + if (schemaExtractor && !schema.$ref) { 23 + const extracted = schemaExtractor({ 24 + meta: { 25 + resource: 'definition', 26 + resourceId: pathToJsonPointer(fromRef(state.path)), 27 + }, 28 + path: fromRef(state.path), 29 + schema, 30 + }); 31 + if (extracted !== schema) schema = extracted; 32 + } 33 + 21 34 if (schema.$ref) { 22 35 const query: SymbolMeta = { 23 36 category: 'schema',
+5
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/types.ts
··· 1 1 import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 + import type { SchemaExtractor } from '@hey-api/shared'; 2 3 3 4 import type { HeyApiTypeScriptPlugin } from '../types'; 4 5 5 6 export type IrSchemaToAstOptions = { 7 + /** The plugin instance. */ 6 8 plugin: HeyApiTypeScriptPlugin['Instance']; 9 + /** Optional schema extractor function. */ 10 + schemaExtractor?: SchemaExtractor; 11 + /** The plugin state references. */ 7 12 state: Refs<PluginState>; 8 13 }; 9 14
+16 -5
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts
··· 1 1 import type { Symbol } from '@hey-api/codegen-core'; 2 - import { refs } from '@hey-api/codegen-core'; 3 - import type { IR } from '@hey-api/shared'; 4 - import type { SchemaWithType } from '@hey-api/shared'; 5 - import { applyNaming } from '@hey-api/shared'; 6 - import { deduplicateSchema } from '@hey-api/shared'; 2 + import { fromRef, refs } from '@hey-api/codegen-core'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { applyNaming, deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 7 5 8 6 import type { MaybeTsDsl, TypeTsDsl } from '../../../../ts-dsl'; 9 7 import { $ } from '../../../../ts-dsl'; ··· 18 16 export function irSchemaToAst({ 19 17 plugin, 20 18 schema, 19 + schemaExtractor, 21 20 state, 22 21 }: IrSchemaToAstOptions & { 23 22 schema: IR.SchemaObject; 24 23 }): MaybeTsDsl<TypeTsDsl> { 24 + if (schemaExtractor && !schema.$ref) { 25 + const extracted = schemaExtractor({ 26 + meta: { 27 + resource: 'definition', 28 + resourceId: pathToJsonPointer(fromRef(state.path)), 29 + }, 30 + path: fromRef(state.path), 31 + schema, 32 + }); 33 + if (extracted !== schema) schema = extracted; 34 + } 35 + 25 36 if (schema.symbolRef) { 26 37 const baseType = $.type(schema.symbolRef); 27 38 if (schema.omit && schema.omit.length > 0) {
+5 -1
packages/openapi-ts/src/plugins/arktype/shared/types.ts
··· 1 1 import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { IR, SchemaExtractor } from '@hey-api/shared'; 3 3 import type ts from 'typescript'; 4 4 5 5 import type { $ } from '../../../ts-dsl'; ··· 13 13 }; 14 14 15 15 export type IrSchemaToAstOptions = { 16 + /** The plugin instance. */ 16 17 plugin: ArktypePlugin['Instance']; 18 + /** Optional schema extractor function. */ 19 + schemaExtractor?: SchemaExtractor; 20 + /** The plugin state references. */ 17 21 state: Refs<PluginState>; 18 22 }; 19 23
+13
packages/openapi-ts/src/plugins/arktype/v2/plugin.ts
··· 13 13 // optional, 14 14 plugin, 15 15 schema, 16 + schemaExtractor, 16 17 state, 17 18 }: IrSchemaToAstOptions & { 18 19 /** ··· 23 24 optional?: boolean; 24 25 schema: IR.SchemaObject; 25 26 }): Ast { 27 + if (schemaExtractor && !schema.$ref) { 28 + const extracted = schemaExtractor({ 29 + meta: { 30 + resource: 'definition', 31 + resourceId: pathToJsonPointer(fromRef(state.path)), 32 + }, 33 + path: fromRef(state.path), 34 + schema, 35 + }); 36 + if (extracted !== schema) schema = extracted; 37 + } 38 + 26 39 let ast: Partial<Ast> = {}; 27 40 28 41 // const z = plugin.referenceSymbol({
+22 -26
packages/openapi-ts/src/plugins/valibot/resolvers/types.ts
··· 5 5 import type { GetIntegerLimit } from '../../../plugins/shared/utils/formats'; 6 6 import type { $, DollarTsDsl } from '../../../ts-dsl'; 7 7 import type { Pipe, PipeResult, Pipes, PipesUtils } from '../shared/pipes'; 8 - import type { Ast, PluginState } from '../shared/types'; 9 - import type { ValibotPlugin } from '../types'; 8 + import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; 10 9 11 10 export type Resolvers = Plugin.Resolvers<{ 12 11 /** ··· 70 69 71 70 type ValidatorResolver = (ctx: ValidatorResolverContext) => PipeResult | null | undefined; 72 71 73 - interface BaseContext extends DollarTsDsl { 74 - /** 75 - * Functions for working with pipes. 76 - */ 77 - pipes: PipesUtils & { 72 + type BaseContext = DollarTsDsl & 73 + Pick<IrSchemaToAstOptions, 'plugin' | 'schemaExtractor'> & { 74 + /** 75 + * Functions for working with pipes. 76 + */ 77 + pipes: PipesUtils & { 78 + /** 79 + * The current pipe. 80 + * 81 + * In Valibot, this represents a list of call expressions ("pipes") 82 + * being assembled to form a schema definition. 83 + * 84 + * Each pipe can be extended, modified, or replaced to customize 85 + * the resulting schema. 86 + */ 87 + current: Pipes; 88 + }; 78 89 /** 79 - * The current pipe. 80 - * 81 - * In Valibot, this represents a list of call expressions ("pipes") 82 - * being assembled to form a schema definition. 83 - * 84 - * Each pipe can be extended, modified, or replaced to customize 85 - * the resulting schema. 90 + * Provides access to commonly used symbols within the plugin. 86 91 */ 87 - current: Pipes; 88 - }; 89 - /** 90 - * The plugin instance. 91 - */ 92 - plugin: ValibotPlugin['Instance']; 93 - /** 94 - * Provides access to commonly used symbols within the plugin. 95 - */ 96 - symbols: { 97 - v: Symbol; 92 + symbols: { 93 + v: Symbol; 94 + }; 98 95 }; 99 - } 100 96 101 97 export interface EnumResolverContext extends BaseContext { 102 98 /**
+23 -8
packages/openapi-ts/src/plugins/valibot/shared/export.ts
··· 1 - import type { Symbol } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 1 + import { applyNaming, pathToName } from '@hey-api/shared'; 3 2 4 3 import { createSchemaComment } from '../../../plugins/shared/utils/schema'; 5 4 import { $ } from '../../../ts-dsl'; 6 5 import { identifiers } from '../v1/constants'; 7 6 import { pipesToNode } from './pipes'; 7 + import type { ProcessorContext } from './processor'; 8 8 import type { Ast, IrSchemaToAstOptions } from './types'; 9 9 10 10 export function exportAst({ 11 11 ast, 12 + meta, 13 + naming, 14 + namingAnchor, 15 + path, 12 16 plugin, 13 17 schema, 14 18 state, 15 - symbol, 16 - }: IrSchemaToAstOptions & { 17 - ast: Ast; 18 - schema: IR.SchemaObject; 19 - symbol: Symbol; 20 - }): void { 19 + tags, 20 + }: Pick<IrSchemaToAstOptions, 'state'> & 21 + ProcessorContext & { 22 + ast: Ast; 23 + }): void { 21 24 const v = plugin.external('valibot.v'); 25 + 26 + const name = pathToName(path, { anchor: namingAnchor }); 27 + const symbol = plugin.symbol(applyNaming(name, naming), { 28 + meta: { 29 + category: 'schema', 30 + path, 31 + tags, 32 + tool: 'valibot', 33 + ...meta, 34 + }, 35 + }); 36 + 22 37 const statement = $.const(symbol) 23 38 .export() 24 39 .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v))
+58
packages/openapi-ts/src/plugins/valibot/shared/operation-schema.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + export interface OperationSchemaResult { 4 + required: ReadonlyArray<string>; 5 + schema: IR.SchemaObject; 6 + } 7 + 8 + export function buildOperationSchema(operation: IR.OperationObject): OperationSchemaResult { 9 + const requiredProperties = new Set<string>(); 10 + 11 + const schema: IR.SchemaObject = { 12 + properties: { 13 + body: { type: 'never' }, 14 + path: { type: 'never' }, 15 + query: { type: 'never' }, 16 + }, 17 + type: 'object', 18 + }; 19 + 20 + if (operation.parameters) { 21 + // TODO: add support for cookies 22 + 23 + for (const location of ['header', 'path', 'query'] satisfies ReadonlyArray< 24 + keyof typeof operation.parameters 25 + >) { 26 + const params = operation.parameters[location]; 27 + if (!params) continue; 28 + 29 + const properties: Record<string, IR.SchemaObject> = {}; 30 + const required: Array<string> = []; 31 + const propKey = location === 'header' ? 'headers' : location; 32 + 33 + for (const key in params) { 34 + const parameter = params[key]!; 35 + properties[parameter.name] = parameter.schema; 36 + if (parameter.required) { 37 + required.push(parameter.name); 38 + requiredProperties.add(propKey); 39 + } 40 + } 41 + 42 + if (Object.keys(properties).length) { 43 + schema.properties![propKey] = { properties, required, type: 'object' }; 44 + } 45 + } 46 + } 47 + 48 + if (operation.body) { 49 + schema.properties!.body = operation.body.schema; 50 + if (operation.body.required) { 51 + requiredProperties.add('body'); 52 + } 53 + } 54 + 55 + schema.required = [...requiredProperties]; 56 + 57 + return { required: schema.required, schema }; 58 + }
+26 -132
packages/openapi-ts/src/plugins/valibot/shared/operation.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming, operationResponsesMap } from '@hey-api/shared'; 2 + import { operationResponsesMap } from '@hey-api/shared'; 4 3 5 - import { exportAst } from './export'; 6 - import type { Ast, IrSchemaToAstOptions } from './types'; 4 + import { buildOperationSchema } from './operation-schema'; 5 + import type { ProcessorContext, ProcessorResult } from './processor'; 6 + import type { IrSchemaToAstOptions } from './types'; 7 7 8 - export const irOperationToAst = ({ 9 - getAst, 8 + export function irOperationToAst({ 10 9 operation, 10 + path, 11 11 plugin, 12 - state, 13 - }: IrSchemaToAstOptions & { 14 - getAst: (schema: IR.SchemaObject, path: ReadonlyArray<string | number>) => Ast; 15 - operation: IR.OperationObject; 16 - }) => { 12 + processor, 13 + tags, 14 + }: Pick<IrSchemaToAstOptions, 'plugin'> & 15 + Pick<ProcessorContext, 'path' | 'tags'> & { 16 + operation: IR.OperationObject; 17 + processor: ProcessorResult; 18 + }): void { 17 19 if (plugin.config.requests.enabled) { 18 - const requiredProperties = new Set<string>(); 19 - 20 - const schemaData: IR.SchemaObject = { 21 - properties: { 22 - body: { 23 - type: 'never', 24 - }, 25 - path: { 26 - type: 'never', 27 - }, 28 - query: { 29 - type: 'never', 30 - }, 31 - }, 32 - type: 'object', 33 - }; 34 - 35 - if (operation.parameters) { 36 - // TODO: add support for cookies 37 - 38 - if (operation.parameters.header) { 39 - const properties: Record<string, IR.SchemaObject> = {}; 40 - const required: Array<string> = []; 41 - 42 - for (const key in operation.parameters.header) { 43 - const parameter = operation.parameters.header[key]!; 44 - properties[parameter.name] = parameter.schema; 45 - if (parameter.required) { 46 - required.push(parameter.name); 47 - requiredProperties.add('headers'); 48 - } 49 - } 50 - 51 - if (Object.keys(properties).length) { 52 - schemaData.properties!.headers = { 53 - properties, 54 - required, 55 - type: 'object', 56 - }; 57 - } 58 - } 59 - 60 - if (operation.parameters.path) { 61 - const properties: Record<string, IR.SchemaObject> = {}; 62 - const required: Array<string> = []; 63 - 64 - for (const key in operation.parameters.path) { 65 - const parameter = operation.parameters.path[key]!; 66 - properties[parameter.name] = parameter.schema; 67 - if (parameter.required) { 68 - required.push(parameter.name); 69 - requiredProperties.add('path'); 70 - } 71 - } 72 - 73 - if (Object.keys(properties).length) { 74 - schemaData.properties!.path = { 75 - properties, 76 - required, 77 - type: 'object', 78 - }; 79 - } 80 - } 81 - 82 - if (operation.parameters.query) { 83 - const properties: Record<string, IR.SchemaObject> = {}; 84 - const required: Array<string> = []; 85 - 86 - for (const key in operation.parameters.query) { 87 - const parameter = operation.parameters.query[key]!; 88 - properties[parameter.name] = parameter.schema; 89 - if (parameter.required) { 90 - required.push(parameter.name); 91 - requiredProperties.add('query'); 92 - } 93 - } 94 - 95 - if (Object.keys(properties).length) { 96 - schemaData.properties!.query = { 97 - properties, 98 - required, 99 - type: 'object', 100 - }; 101 - } 102 - } 103 - } 104 - 105 - if (operation.body) { 106 - schemaData.properties!.body = operation.body.schema; 20 + const { schema } = buildOperationSchema(operation); 107 21 108 - if (operation.body.required) { 109 - requiredProperties.add('body'); 110 - } 111 - } 112 - 113 - schemaData.required = [...requiredProperties]; 114 - 115 - const ast = getAst(schemaData, fromRef(state.path)); 116 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.requests), { 22 + processor.process({ 117 23 meta: { 118 - category: 'schema', 119 - path: fromRef(state.path), 120 24 resource: 'operation', 121 25 resourceId: operation.id, 122 26 role: 'data', 123 - tags: fromRef(state.tags), 124 - tool: 'valibot', 125 27 }, 126 - }); 127 - exportAst({ 128 - ast, 28 + naming: plugin.config.requests, 29 + namingAnchor: operation.id, 30 + path, 129 31 plugin, 130 - schema: schemaData, 131 - state, 132 - symbol, 32 + schema, 33 + tags, 133 34 }); 134 35 } 135 36 ··· 138 39 const { response } = operationResponsesMap(operation); 139 40 140 41 if (response) { 141 - const path = [...fromRef(state.path), 'responses']; 142 - const ast = getAst(response, path); 143 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.responses), { 42 + processor.process({ 144 43 meta: { 145 - category: 'schema', 146 - path, 147 44 resource: 'operation', 148 45 resourceId: operation.id, 149 46 role: 'responses', 150 - tags: fromRef(state.tags), 151 - tool: 'valibot', 152 47 }, 153 - }); 154 - exportAst({ 155 - ast, 48 + naming: plugin.config.responses, 49 + namingAnchor: operation.id, 50 + path: [...path, 'responses'], 156 51 plugin, 157 52 schema: response, 158 - state, 159 - symbol, 53 + tags, 160 54 }); 161 55 } 162 56 } 163 57 } 164 - }; 58 + }
+16
packages/openapi-ts/src/plugins/valibot/shared/processor.ts
··· 1 + import type { 2 + IR, 3 + NamingConfig, 4 + SchemaProcessorContext, 5 + SchemaProcessorResult, 6 + } from '@hey-api/shared'; 7 + 8 + import type { IrSchemaToAstOptions } from './types'; 9 + 10 + export type ProcessorContext = Pick<IrSchemaToAstOptions, 'plugin'> & 11 + SchemaProcessorContext & { 12 + naming: NamingConfig; 13 + schema: IR.SchemaObject; 14 + }; 15 + 16 + export type ProcessorResult = SchemaProcessorResult<ProcessorContext>;
+6 -1
packages/openapi-ts/src/plugins/valibot/shared/types.ts
··· 1 1 import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { IR, SchemaExtractor } from '@hey-api/shared'; 3 3 import type ts from 'typescript'; 4 4 5 5 import type { ValibotPlugin } from '../types'; 6 6 import type { Pipes } from './pipes'; 7 + import type { ProcessorContext } from './processor'; 7 8 8 9 export type Ast = { 9 10 hasLazyExpression?: boolean; ··· 12 13 }; 13 14 14 15 export type IrSchemaToAstOptions = { 16 + /** The plugin instance. */ 15 17 plugin: ValibotPlugin['Instance']; 18 + /** Optional schema extractor function. */ 19 + schemaExtractor?: SchemaExtractor<ProcessorContext>; 20 + /** The plugin state references. */ 16 21 state: Refs<PluginState>; 17 22 }; 18 23
+20 -120
packages/openapi-ts/src/plugins/valibot/shared/webhook.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming } from '@hey-api/shared'; 4 2 5 - import { exportAst } from './export'; 6 - import type { Ast, IrSchemaToAstOptions } from './types'; 3 + import { buildOperationSchema } from './operation-schema'; 4 + import type { ProcessorContext, ProcessorResult } from './processor'; 5 + import type { IrSchemaToAstOptions } from './types'; 7 6 8 - export const irWebhookToAst = ({ 9 - getAst, 7 + export function irWebhookToAst({ 10 8 operation, 9 + path, 11 10 plugin, 12 - state, 13 - }: IrSchemaToAstOptions & { 14 - getAst: (schema: IR.SchemaObject, path: ReadonlyArray<string | number>) => Ast; 15 - operation: IR.OperationObject; 16 - }) => { 11 + processor, 12 + tags, 13 + }: Pick<IrSchemaToAstOptions, 'plugin'> & 14 + Pick<ProcessorContext, 'path' | 'tags'> & { 15 + operation: IR.OperationObject; 16 + processor: ProcessorResult; 17 + }): void { 17 18 if (plugin.config.webhooks.enabled) { 18 - const requiredProperties = new Set<string>(); 19 - 20 - const schemaData: IR.SchemaObject = { 21 - properties: { 22 - body: { 23 - type: 'never', 24 - }, 25 - path: { 26 - type: 'never', 27 - }, 28 - query: { 29 - type: 'never', 30 - }, 31 - }, 32 - type: 'object', 33 - }; 34 - 35 - if (operation.parameters) { 36 - // TODO: add support for cookies 37 - 38 - if (operation.parameters.header) { 39 - const properties: Record<string, IR.SchemaObject> = {}; 40 - const required: Array<string> = []; 41 - 42 - for (const key in operation.parameters.header) { 43 - const parameter = operation.parameters.header[key]!; 44 - properties[parameter.name] = parameter.schema; 45 - if (parameter.required) { 46 - required.push(parameter.name); 47 - requiredProperties.add('headers'); 48 - } 49 - } 50 - 51 - if (Object.keys(properties).length) { 52 - schemaData.properties!.headers = { 53 - properties, 54 - required, 55 - type: 'object', 56 - }; 57 - } 58 - } 59 - 60 - if (operation.parameters.path) { 61 - const properties: Record<string, IR.SchemaObject> = {}; 62 - const required: Array<string> = []; 63 - 64 - for (const key in operation.parameters.path) { 65 - const parameter = operation.parameters.path[key]!; 66 - properties[parameter.name] = parameter.schema; 67 - if (parameter.required) { 68 - required.push(parameter.name); 69 - requiredProperties.add('path'); 70 - } 71 - } 72 - 73 - if (Object.keys(properties).length) { 74 - schemaData.properties!.path = { 75 - properties, 76 - required, 77 - type: 'object', 78 - }; 79 - } 80 - } 81 - 82 - if (operation.parameters.query) { 83 - const properties: Record<string, IR.SchemaObject> = {}; 84 - const required: Array<string> = []; 85 - 86 - for (const key in operation.parameters.query) { 87 - const parameter = operation.parameters.query[key]!; 88 - properties[parameter.name] = parameter.schema; 89 - if (parameter.required) { 90 - required.push(parameter.name); 91 - requiredProperties.add('query'); 92 - } 93 - } 94 - 95 - if (Object.keys(properties).length) { 96 - schemaData.properties!.query = { 97 - properties, 98 - required, 99 - type: 'object', 100 - }; 101 - } 102 - } 103 - } 104 - 105 - if (operation.body) { 106 - schemaData.properties!.body = operation.body.schema; 107 - 108 - if (operation.body.required) { 109 - requiredProperties.add('body'); 110 - } 111 - } 19 + const { schema } = buildOperationSchema(operation); 112 20 113 - schemaData.required = [...requiredProperties]; 114 - 115 - const ast = getAst(schemaData, fromRef(state.path)); 116 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.webhooks), { 21 + processor.process({ 117 22 meta: { 118 - category: 'schema', 119 - path: fromRef(state.path), 120 23 resource: 'webhook', 121 24 resourceId: operation.id, 122 25 role: 'data', 123 - tags: fromRef(state.tags), 124 - tool: 'valibot', 125 26 }, 126 - }); 127 - exportAst({ 128 - ast, 27 + naming: plugin.config.webhooks, 28 + namingAnchor: operation.id, 29 + path, 129 30 plugin, 130 - schema: schemaData, 131 - state, 132 - symbol, 31 + schema, 32 + tags, 133 33 }); 134 34 } 135 - }; 35 + }
+52 -76
packages/openapi-ts/src/plugins/valibot/v1/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref, refs } from '@hey-api/codegen-core'; 3 - import type { IR, SchemaExtractor, SchemaWithType } from '@hey-api/shared'; 4 - import { 5 - applyNaming, 6 - deduplicateSchema, 7 - inlineSchema, 8 - pathToJsonPointer, 9 - refToName, 10 - } from '@hey-api/shared'; 2 + import { fromRef, ref } from '@hey-api/codegen-core'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 11 5 12 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 13 7 import { $ } from '../../../ts-dsl'; 14 - import { exportAst } from '../shared/export'; 15 8 import { irOperationToAst } from '../shared/operation'; 16 9 import { pipesToNode } from '../shared/pipes'; 17 - import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; 10 + import type { Ast, IrSchemaToAstOptions } from '../shared/types'; 18 11 import { irWebhookToAst } from '../shared/webhook'; 19 12 import type { ValibotPlugin } from '../types'; 20 13 import { identifiers } from './constants'; 14 + import { createProcessor } from './processor'; 21 15 import { irSchemaWithTypeToAst } from './toAst'; 22 16 23 17 export function irSchemaToAst({ 24 18 optional, 25 19 plugin, 26 20 schema, 27 - schemaExtractor = inlineSchema, 21 + schemaExtractor, 28 22 state, 29 23 }: IrSchemaToAstOptions & { 30 24 /** ··· 34 28 */ 35 29 optional?: boolean; 36 30 schema: IR.SchemaObject; 37 - schemaExtractor?: SchemaExtractor; 38 31 }): Ast { 39 - if (!schema.$ref) { 40 - const resolved = schemaExtractor({ path: fromRef(state.path), schema }); 41 - if (resolved !== schema) { 42 - schema = resolved; 43 - } 32 + if (schemaExtractor && !schema.$ref) { 33 + const extracted = schemaExtractor({ 34 + meta: { 35 + resource: 'definition', 36 + resourceId: pathToJsonPointer(fromRef(state.path)), 37 + }, 38 + naming: plugin.config.definitions, 39 + path: fromRef(state.path), 40 + plugin, 41 + schema, 42 + }); 43 + if (extracted !== schema) schema = extracted; 44 44 } 45 45 46 46 const ast: Ast = { ··· 71 71 const typeAst = irSchemaWithTypeToAst({ 72 72 plugin, 73 73 schema: schema as SchemaWithType, 74 + schemaExtractor, 74 75 state, 75 76 }); 76 77 ast.typeName = typeAst.anyType; ··· 121 122 schema: { 122 123 type: 'unknown', 123 124 }, 125 + schemaExtractor, 124 126 state, 125 127 }); 126 128 ast.typeName = typeAst.anyType; ··· 152 154 return ast as Ast; 153 155 } 154 156 155 - function handleComponent({ 156 - plugin, 157 - schema, 158 - state, 159 - }: IrSchemaToAstOptions & { 160 - schema: IR.SchemaObject; 161 - }): void { 162 - const $ref = pathToJsonPointer(fromRef(state.path)); 163 - const ast = irSchemaToAst({ plugin, schema, state }); 164 - const baseName = refToName($ref); 165 - const symbol = plugin.symbol(applyNaming(baseName, plugin.config.definitions), { 166 - meta: { 167 - category: 'schema', 168 - path: fromRef(state.path), 169 - resource: 'definition', 170 - resourceId: $ref, 171 - tags: fromRef(state.tags), 172 - tool: 'valibot', 173 - }, 174 - }); 175 - exportAst({ 176 - ast, 177 - plugin, 178 - schema, 179 - state, 180 - symbol, 181 - }); 182 - } 183 - 184 157 export const handlerV1: ValibotPlugin['Handler'] = ({ plugin }) => { 185 158 plugin.symbol('v', { 186 159 external: 'valibot', ··· 191 164 }, 192 165 }); 193 166 167 + const processor = createProcessor(plugin); 168 + 194 169 plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { 195 - const state = refs<PluginState>({ 196 - hasLazyExpression: false, 197 - path: event._path, 198 - tags: event.tags, 199 - }); 200 170 switch (event.type) { 201 171 case 'operation': 202 172 irOperationToAst({ 203 - getAst: (schema, path) => { 204 - const state = refs<PluginState>({ 205 - hasLazyExpression: false, 206 - path, 207 - tags: event.tags, 208 - }); 209 - return irSchemaToAst({ plugin, schema, state }); 210 - }, 211 173 operation: event.operation, 174 + path: event._path, 212 175 plugin, 213 - state, 176 + processor, 177 + tags: event.tags, 214 178 }); 215 179 break; 216 180 case 'parameter': 217 - handleComponent({ 181 + processor.process({ 182 + meta: { 183 + resource: 'definition', 184 + resourceId: pathToJsonPointer(event._path), 185 + }, 186 + naming: plugin.config.definitions, 187 + path: event._path, 218 188 plugin, 219 189 schema: event.parameter.schema, 220 - state, 190 + tags: event.tags, 221 191 }); 222 192 break; 223 193 case 'requestBody': 224 - handleComponent({ 194 + processor.process({ 195 + meta: { 196 + resource: 'definition', 197 + resourceId: pathToJsonPointer(event._path), 198 + }, 199 + naming: plugin.config.definitions, 200 + path: event._path, 225 201 plugin, 226 202 schema: event.requestBody.schema, 227 - state, 203 + tags: event.tags, 228 204 }); 229 205 break; 230 206 case 'schema': 231 - handleComponent({ 207 + processor.process({ 208 + meta: { 209 + resource: 'definition', 210 + resourceId: pathToJsonPointer(event._path), 211 + }, 212 + naming: plugin.config.definitions, 213 + path: event._path, 232 214 plugin, 233 215 schema: event.schema, 234 - state, 216 + tags: event.tags, 235 217 }); 236 218 break; 237 219 case 'webhook': 238 220 irWebhookToAst({ 239 - getAst: (schema, path) => { 240 - const state = refs<PluginState>({ 241 - hasLazyExpression: false, 242 - path, 243 - tags: event.tags, 244 - }); 245 - return irSchemaToAst({ plugin, schema, state }); 246 - }, 247 221 operation: event.operation, 222 + path: event._path, 248 223 plugin, 249 - state, 224 + processor, 225 + tags: event.tags, 250 226 }); 251 227 break; 252 228 }
+58
packages/openapi-ts/src/plugins/valibot/v1/processor.ts
··· 1 + import { refs } from '@hey-api/codegen-core'; 2 + import type { IR } from '@hey-api/shared'; 3 + import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { exportAst } from '../shared/export'; 6 + import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 + import type { PluginState } from '../shared/types'; 8 + import type { ValibotPlugin } from '../types'; 9 + import { irSchemaToAst } from './plugin'; 10 + 11 + export function createProcessor(plugin: ValibotPlugin['Instance']): ProcessorResult { 12 + const processor = createSchemaProcessor(); 13 + 14 + const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 15 + 16 + function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 + if (processor.hasEmitted(ctx.path)) { 18 + return ctx.schema; 19 + } 20 + 21 + for (const hook of hooks) { 22 + const result = hook?.shouldExtract?.(ctx); 23 + if (result) { 24 + process({ 25 + namingAnchor: processor.context.anchor, 26 + tags: processor.context.tags, 27 + ...ctx, 28 + }); 29 + return { $ref: pathToJsonPointer(ctx.path) }; 30 + } 31 + } 32 + 33 + return ctx.schema; 34 + } 35 + 36 + function process(ctx: ProcessorContext): void { 37 + if (!processor.markEmitted(ctx.path)) return; 38 + 39 + processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 + const state = refs<PluginState>({ 41 + hasLazyExpression: false, 42 + path: ctx.path, 43 + tags: ctx.tags, 44 + }); 45 + 46 + const ast = irSchemaToAst({ 47 + plugin, 48 + schema: ctx.schema, 49 + schemaExtractor: extractor, 50 + state, 51 + }); 52 + 53 + exportAst({ ...ctx, ast, plugin, state }); 54 + }); 55 + } 56 + 57 + return { process }; 58 + }
+14 -15
packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts
··· 9 9 import { irSchemaToAst } from '../plugin'; 10 10 import { unknownToAst } from './unknown'; 11 11 12 - export const arrayToAst = ({ 13 - plugin, 14 - schema, 15 - state, 16 - }: IrSchemaToAstOptions & { 17 - schema: SchemaWithType<'array'>; 18 - }): Omit<Ast, 'typeName'> => { 12 + export function arrayToAst( 13 + options: IrSchemaToAstOptions & { 14 + schema: SchemaWithType<'array'>; 15 + }, 16 + ): Omit<Ast, 'typeName'> { 17 + const { plugin } = options; 18 + let { schema } = options; 19 + 19 20 const result: Omit<Ast, 'typeName'> = { 20 21 pipes: [], 21 22 }; ··· 26 27 if (!schema.items) { 27 28 const expression = functionName.call( 28 29 unknownToAst({ 29 - plugin, 30 + ...options, 30 31 schema: { 31 32 type: 'unknown', 32 33 }, 33 - state, 34 34 }), 35 35 ); 36 36 result.pipes.push(expression); ··· 40 40 // at least one item is guaranteed 41 41 const itemExpressions = schema.items!.map((item, index) => { 42 42 const itemAst = irSchemaToAst({ 43 - plugin, 43 + ...options, 44 44 schema: item, 45 45 state: { 46 - ...state, 47 - path: ref([...fromRef(state.path), 'items', index]), 46 + ...options.state, 47 + path: ref([...fromRef(options.state.path), 'items', index]), 48 48 }, 49 49 }); 50 50 if (itemAst.hasLazyExpression) { ··· 69 69 70 70 const expression = functionName.call( 71 71 unknownToAst({ 72 - plugin, 72 + ...options, 73 73 schema: { 74 74 type: 'unknown', 75 75 }, 76 - state, 77 76 }), 78 77 ); 79 78 result.pipes.push(expression); ··· 100 99 } 101 100 102 101 return result as Omit<Ast, 'typeName'>; 103 - }; 102 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts
··· 65 65 return ctx.pipes.current; 66 66 } 67 67 68 - export const enumToAst = ({ 68 + export function enumToAst({ 69 69 plugin, 70 70 schema, 71 71 state, 72 72 }: IrSchemaToAstOptions & { 73 73 schema: SchemaWithType<'enum'>; 74 - }): Pipe => { 74 + }): Pipe { 75 75 const v = plugin.external('valibot.v'); 76 76 77 77 const { enumMembers } = itemsNode({ ··· 118 118 const resolver = plugin.config['~resolvers']?.enum; 119 119 const node = resolver?.(ctx) ?? enumResolver(ctx); 120 120 return ctx.pipes.toNode(node, plugin); 121 - }; 121 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts
··· 4 4 import type { IrSchemaToAstOptions } from '../../shared/types'; 5 5 import { identifiers } from '../constants'; 6 6 7 - export const neverToAst = ({ 7 + export function neverToAst({ 8 8 plugin, 9 9 }: IrSchemaToAstOptions & { 10 10 schema: SchemaWithType<'never'>; 11 - }) => { 11 + }) { 12 12 const v = plugin.external('valibot.v'); 13 13 const expression = $(v).attr(identifiers.schemas.never).call(); 14 14 return expression; 15 - }; 15 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts
··· 4 4 import type { IrSchemaToAstOptions } from '../../shared/types'; 5 5 import { identifiers } from '../constants'; 6 6 7 - export const nullToAst = ({ 7 + export function nullToAst({ 8 8 plugin, 9 9 }: IrSchemaToAstOptions & { 10 10 schema: SchemaWithType<'null'>; 11 - }) => { 11 + }) { 12 12 const v = plugin.external('valibot.v'); 13 13 const expression = $(v).attr(identifiers.schemas.null).call(); 14 14 return expression; 15 - }; 15 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts
··· 105 105 return ctx.pipes.current; 106 106 } 107 107 108 - export const numberToNode = ({ 108 + export function numberToNode({ 109 109 plugin, 110 110 schema, 111 111 }: IrSchemaToAstOptions & { 112 112 schema: SchemaWithType<'integer' | 'number'>; 113 - }): Pipe => { 113 + }): Pipe { 114 114 const ctx: NumberResolverContext = { 115 115 $, 116 116 nodes: { ··· 137 137 const resolver = plugin.config['~resolvers']?.number; 138 138 const node = resolver?.(ctx) ?? numberResolver(ctx); 139 139 return ctx.pipes.toNode(node, plugin); 140 - }; 140 + }
+11 -13
packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts
··· 16 16 if (schema.additionalProperties.type === 'never') return null; 17 17 18 18 const additionalAst = irSchemaToAst({ 19 - plugin, 19 + ...ctx, 20 20 schema: schema.additionalProperties, 21 21 state: { 22 22 ...ctx.utils.state, ··· 64 64 const property = schema.properties[name]!; 65 65 66 66 const propertyAst = irSchemaToAst({ 67 + ...ctx, 67 68 optional: !schema.required?.includes(name), 68 - plugin, 69 69 schema: property, 70 70 state: { 71 71 ...ctx.utils.state, ··· 79 79 return shape; 80 80 } 81 81 82 - export const objectToAst = ({ 83 - plugin, 84 - schema, 85 - state, 86 - }: IrSchemaToAstOptions & { 87 - schema: SchemaWithType<'object'>; 88 - }): Omit<Ast, 'typeName'> => { 82 + export function objectToAst( 83 + options: IrSchemaToAstOptions & { 84 + schema: SchemaWithType<'object'>; 85 + }, 86 + ): Omit<Ast, 'typeName'> { 87 + const { plugin } = options; 89 88 const ctx: ObjectResolverContext = { 89 + ...options, 90 90 $, 91 91 nodes: { 92 92 additionalProperties: additionalPropertiesNode, ··· 97 97 ...pipes, 98 98 current: [], 99 99 }, 100 - plugin, 101 - schema, 102 100 symbols: { 103 101 v: plugin.external('valibot.v'), 104 102 }, 105 103 utils: { 106 104 ast: {}, 107 - state, 105 + state: options.state, 108 106 }, 109 107 }; 110 108 const resolver = plugin.config['~resolvers']?.object; 111 109 const node = resolver?.(ctx) ?? objectResolver(ctx); 112 110 ctx.utils.ast.pipes = [ctx.pipes.toNode(node, plugin)]; 113 111 return ctx.utils.ast as Omit<Ast, 'typeName'>; 114 - }; 112 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts
··· 98 98 return ctx.pipes.current; 99 99 } 100 100 101 - export const stringToNode = ({ 101 + export function stringToNode({ 102 102 plugin, 103 103 schema, 104 104 }: IrSchemaToAstOptions & { 105 105 schema: SchemaWithType<'string'>; 106 - }): Pipe => { 106 + }): Pipe { 107 107 const ctx: StringResolverContext = { 108 108 $, 109 109 nodes: { ··· 128 128 const resolver = plugin.config['~resolvers']?.string; 129 129 const node = resolver?.(ctx) ?? stringResolver(ctx); 130 130 return ctx.pipes.toNode(node, plugin); 131 - }; 131 + }
+12 -13
packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts
··· 8 8 import { irSchemaToAst } from '../plugin'; 9 9 import { unknownToAst } from './unknown'; 10 10 11 - export const tupleToAst = ({ 12 - plugin, 13 - schema, 14 - state, 15 - }: IrSchemaToAstOptions & { 16 - schema: SchemaWithType<'tuple'>; 17 - }): Omit<Ast, 'typeName'> => { 11 + export function tupleToAst( 12 + options: IrSchemaToAstOptions & { 13 + schema: SchemaWithType<'tuple'>; 14 + }, 15 + ): Omit<Ast, 'typeName'> { 16 + const { plugin, schema } = options; 17 + 18 18 const result: Partial<Omit<Ast, 'typeName'>> = {}; 19 19 20 20 const v = plugin.external('valibot.v'); ··· 34 34 if (schema.items) { 35 35 const tupleElements = schema.items.map((item, index) => { 36 36 const schemaPipes = irSchemaToAst({ 37 - plugin, 37 + ...options, 38 38 schema: item, 39 39 state: { 40 - ...state, 41 - path: ref([...fromRef(state.path), 'items', index]), 40 + ...options.state, 41 + path: ref([...fromRef(options.state.path), 'items', index]), 42 42 }, 43 43 }); 44 44 if (schemaPipes.hasLazyExpression) { ··· 57 57 return { 58 58 pipes: [ 59 59 unknownToAst({ 60 - plugin, 60 + ...options, 61 61 schema: { 62 62 type: 'unknown', 63 63 }, 64 - state, 65 64 }), 66 65 ], 67 66 }; 68 - }; 67 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts
··· 4 4 import type { IrSchemaToAstOptions } from '../../shared/types'; 5 5 import { identifiers } from '../constants'; 6 6 7 - export const undefinedToAst = ({ 7 + export function undefinedToAst({ 8 8 plugin, 9 9 }: IrSchemaToAstOptions & { 10 10 schema: SchemaWithType<'undefined'>; 11 - }) => { 11 + }) { 12 12 const v = plugin.external('valibot.v'); 13 13 const expression = $(v).attr(identifiers.schemas.undefined).call(); 14 14 return expression; 15 - }; 15 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts
··· 4 4 import type { IrSchemaToAstOptions } from '../../shared/types'; 5 5 import { identifiers } from '../constants'; 6 6 7 - export const unknownToAst = ({ 7 + export function unknownToAst({ 8 8 plugin, 9 9 }: IrSchemaToAstOptions & { 10 10 schema: SchemaWithType<'unknown'>; 11 - }) => { 11 + }) { 12 12 const v = plugin.external('valibot.v'); 13 13 const expression = $(v).attr(identifiers.schemas.unknown).call(); 14 14 return expression; 15 - }; 15 + }
+3 -3
packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts
··· 4 4 import type { IrSchemaToAstOptions } from '../../shared/types'; 5 5 import { identifiers } from '../constants'; 6 6 7 - export const voidToAst = ({ 7 + export function voidToAst({ 8 8 plugin, 9 9 }: IrSchemaToAstOptions & { 10 10 schema: SchemaWithType<'void'>; 11 - }) => { 11 + }) { 12 12 const v = plugin.external('valibot.v'); 13 13 const expression = $(v).attr(identifiers.schemas.void).call(); 14 14 return expression; 15 - }; 15 + }
+51 -75
packages/openapi-ts/src/plugins/zod/mini/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref, refs } from '@hey-api/codegen-core'; 2 + import { fromRef, ref } from '@hey-api/codegen-core'; 3 3 import type { IR, SchemaWithType } from '@hey-api/shared'; 4 - import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 4 + import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 5 5 6 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 7 7 import { $ } from '../../../ts-dsl'; 8 8 import { identifiers } from '../constants'; 9 - import { exportAst } from '../shared/export'; 10 9 import { getZodModule } from '../shared/module'; 11 10 import { irOperationToAst } from '../shared/operation'; 12 - import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; 11 + import type { Ast, IrSchemaToAstOptions } from '../shared/types'; 13 12 import { irWebhookToAst } from '../shared/webhook'; 14 13 import type { ZodPlugin } from '../types'; 14 + import { createProcessor } from './processor'; 15 15 import { irSchemaWithTypeToAst } from './toAst'; 16 16 17 17 export function irSchemaToAst({ 18 18 optional, 19 19 plugin, 20 20 schema, 21 + schemaExtractor, 21 22 state, 22 23 }: IrSchemaToAstOptions & { 23 24 /** ··· 28 29 optional?: boolean; 29 30 schema: IR.SchemaObject; 30 31 }): Ast { 32 + if (schemaExtractor && !schema.$ref) { 33 + const extracted = schemaExtractor({ 34 + meta: { 35 + resource: 'definition', 36 + resourceId: pathToJsonPointer(fromRef(state.path)), 37 + }, 38 + naming: plugin.config.definitions, 39 + path: fromRef(state.path), 40 + plugin, 41 + schema, 42 + }); 43 + if (extracted !== schema) schema = extracted; 44 + } 45 + 31 46 let ast: Partial<Ast> = {}; 32 47 33 48 const z = plugin.external('zod.z'); ··· 155 170 return ast as Ast; 156 171 } 157 172 158 - function handleComponent({ 159 - plugin, 160 - schema, 161 - state, 162 - }: IrSchemaToAstOptions & { 163 - schema: IR.SchemaObject; 164 - }): void { 165 - const $ref = pathToJsonPointer(fromRef(state.path)); 166 - const ast = irSchemaToAst({ plugin, schema, state }); 167 - const baseName = refToName($ref); 168 - const symbol = plugin.symbol(applyNaming(baseName, plugin.config.definitions), { 169 - meta: { 170 - category: 'schema', 171 - path: fromRef(state.path), 172 - resource: 'definition', 173 - resourceId: $ref, 174 - tags: fromRef(state.tags), 175 - tool: 'zod', 176 - }, 177 - }); 178 - const typeInferSymbol = plugin.config.definitions.types.infer.enabled 179 - ? plugin.symbol(applyNaming(baseName, plugin.config.definitions.types.infer), { 180 - meta: { 181 - category: 'type', 182 - path: fromRef(state.path), 183 - resource: 'definition', 184 - resourceId: $ref, 185 - tags: fromRef(state.tags), 186 - tool: 'zod', 187 - variant: 'infer', 188 - }, 189 - }) 190 - : undefined; 191 - exportAst({ 192 - ast, 193 - plugin, 194 - schema, 195 - symbol, 196 - typeInferSymbol, 197 - }); 198 - } 199 - 200 173 export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { 201 174 plugin.symbol('z', { 202 175 external: getZodModule({ plugin }), ··· 207 180 }, 208 181 }); 209 182 183 + const processor = createProcessor(plugin); 184 + 210 185 plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { 211 - const state = refs<PluginState>({ 212 - hasLazyExpression: false, 213 - path: event._path, 214 - tags: event.tags, 215 - }); 216 186 switch (event.type) { 217 187 case 'operation': 218 188 irOperationToAst({ 219 - getAst: (schema, path) => { 220 - const state = refs<PluginState>({ 221 - hasLazyExpression: false, 222 - path, 223 - tags: event.tags, 224 - }); 225 - return irSchemaToAst({ plugin, schema, state }); 226 - }, 227 189 operation: event.operation, 190 + path: event._path, 228 191 plugin, 229 - state, 192 + processor, 193 + tags: event.tags, 230 194 }); 231 195 break; 232 196 case 'parameter': 233 - handleComponent({ 197 + processor.process({ 198 + meta: { 199 + resource: 'definition', 200 + resourceId: pathToJsonPointer(event._path), 201 + }, 202 + naming: plugin.config.definitions, 203 + path: event._path, 234 204 plugin, 235 205 schema: event.parameter.schema, 236 - state, 206 + tags: event.tags, 237 207 }); 238 208 break; 239 209 case 'requestBody': 240 - handleComponent({ 210 + processor.process({ 211 + meta: { 212 + resource: 'definition', 213 + resourceId: pathToJsonPointer(event._path), 214 + }, 215 + naming: plugin.config.definitions, 216 + path: event._path, 241 217 plugin, 242 218 schema: event.requestBody.schema, 243 - state, 219 + tags: event.tags, 244 220 }); 245 221 break; 246 222 case 'schema': 247 - handleComponent({ 223 + processor.process({ 224 + meta: { 225 + resource: 'definition', 226 + resourceId: pathToJsonPointer(event._path), 227 + }, 228 + naming: plugin.config.definitions, 229 + path: event._path, 248 230 plugin, 249 231 schema: event.schema, 250 - state, 232 + tags: event.tags, 251 233 }); 252 234 break; 253 235 case 'webhook': 254 236 irWebhookToAst({ 255 - getAst: (schema, path) => { 256 - const state = refs<PluginState>({ 257 - hasLazyExpression: false, 258 - path, 259 - tags: event.tags, 260 - }); 261 - return irSchemaToAst({ plugin, schema, state }); 262 - }, 263 237 operation: event.operation, 238 + path: event._path, 264 239 plugin, 265 - state, 240 + processor, 241 + tags: event.tags, 266 242 }); 267 243 break; 268 244 }
+58
packages/openapi-ts/src/plugins/zod/mini/processor.ts
··· 1 + import { refs } from '@hey-api/codegen-core'; 2 + import type { IR } from '@hey-api/shared'; 3 + import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { exportAst } from '../shared/export'; 6 + import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 + import type { PluginState } from '../shared/types'; 8 + import type { ZodPlugin } from '../types'; 9 + import { irSchemaToAst } from './plugin'; 10 + 11 + export function createProcessor(plugin: ZodPlugin['Instance']): ProcessorResult { 12 + const processor = createSchemaProcessor(); 13 + 14 + const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 15 + 16 + function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 + if (processor.hasEmitted(ctx.path)) { 18 + return ctx.schema; 19 + } 20 + 21 + for (const hook of hooks) { 22 + const result = hook?.shouldExtract?.(ctx); 23 + if (result) { 24 + process({ 25 + namingAnchor: processor.context.anchor, 26 + tags: processor.context.tags, 27 + ...ctx, 28 + }); 29 + return { $ref: pathToJsonPointer(ctx.path) }; 30 + } 31 + } 32 + 33 + return ctx.schema; 34 + } 35 + 36 + function process(ctx: ProcessorContext): void { 37 + if (!processor.markEmitted(ctx.path)) return; 38 + 39 + processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 + const state = refs<PluginState>({ 41 + hasLazyExpression: false, 42 + path: ctx.path, 43 + tags: ctx.tags, 44 + }); 45 + 46 + const ast = irSchemaToAst({ 47 + plugin, 48 + schema: ctx.schema, 49 + schemaExtractor: extractor, 50 + state, 51 + }); 52 + 53 + exportAst({ ...ctx, ast, plugin, state }); 54 + }); 55 + } 56 + 57 + return { process }; 58 + }
+36 -13
packages/openapi-ts/src/plugins/zod/shared/export.ts
··· 1 - import type { Symbol } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 1 + import { applyNaming, pathToName } from '@hey-api/shared'; 3 2 4 3 import { createSchemaComment } from '../../../plugins/shared/utils/schema'; 5 4 import { $ } from '../../../ts-dsl'; 6 5 import { identifiers } from '../constants'; 7 - import type { ZodPlugin } from '../types'; 8 - import type { Ast } from './types'; 6 + import type { ProcessorContext } from './processor'; 7 + import type { Ast, IrSchemaToAstOptions } from './types'; 9 8 10 9 export function exportAst({ 11 10 ast, 11 + meta, 12 + naming, 13 + namingAnchor, 14 + path, 12 15 plugin, 13 16 schema, 14 - symbol, 15 - typeInferSymbol, 16 - }: { 17 - ast: Ast; 18 - plugin: ZodPlugin['Instance']; 19 - schema: IR.SchemaObject; 20 - symbol: Symbol; 21 - typeInferSymbol: Symbol | undefined; 22 - }): void { 17 + tags, 18 + }: Pick<IrSchemaToAstOptions, 'state'> & 19 + ProcessorContext & { 20 + ast: Ast; 21 + }): void { 23 22 const z = plugin.external('zod.z'); 23 + 24 + const name = pathToName(path, { anchor: namingAnchor }); 25 + const symbol = plugin.symbol(applyNaming(name, naming), { 26 + meta: { 27 + category: 'schema', 28 + path, 29 + tags, 30 + tool: 'zod', 31 + ...meta, 32 + }, 33 + }); 34 + 35 + const typeInferSymbol = naming.types.infer.enabled 36 + ? plugin.symbol(applyNaming(name, naming.types.infer), { 37 + meta: { 38 + category: 'type', 39 + path, 40 + tags, 41 + tool: 'zod', 42 + variant: 'infer', 43 + ...meta, 44 + }, 45 + }) 46 + : undefined; 24 47 25 48 const statement = $.const(symbol) 26 49 .export()
+58
packages/openapi-ts/src/plugins/zod/shared/operation-schema.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + export interface OperationSchemaResult { 4 + required: ReadonlyArray<string>; 5 + schema: IR.SchemaObject; 6 + } 7 + 8 + export function buildOperationSchema(operation: IR.OperationObject): OperationSchemaResult { 9 + const requiredProperties = new Set<string>(); 10 + 11 + const schema: IR.SchemaObject = { 12 + properties: { 13 + body: { type: 'never' }, 14 + path: { type: 'never' }, 15 + query: { type: 'never' }, 16 + }, 17 + type: 'object', 18 + }; 19 + 20 + if (operation.parameters) { 21 + // TODO: add support for cookies 22 + 23 + for (const location of ['header', 'path', 'query'] satisfies ReadonlyArray< 24 + keyof typeof operation.parameters 25 + >) { 26 + const params = operation.parameters[location]; 27 + if (!params) continue; 28 + 29 + const properties: Record<string, IR.SchemaObject> = {}; 30 + const required: Array<string> = []; 31 + const propKey = location === 'header' ? 'headers' : location; 32 + 33 + for (const key in params) { 34 + const parameter = params[key]!; 35 + properties[parameter.name] = parameter.schema; 36 + if (parameter.required) { 37 + required.push(parameter.name); 38 + requiredProperties.add(propKey); 39 + } 40 + } 41 + 42 + if (Object.keys(properties).length) { 43 + schema.properties![propKey] = { properties, required, type: 'object' }; 44 + } 45 + } 46 + } 47 + 48 + if (operation.body) { 49 + schema.properties!.body = operation.body.schema; 50 + if (operation.body.required) { 51 + requiredProperties.add('body'); 52 + } 53 + } 54 + 55 + schema.required = [...requiredProperties]; 56 + 57 + return { required: schema.required, schema }; 58 + }
+26 -160
packages/openapi-ts/src/plugins/zod/shared/operation.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming, operationResponsesMap } from '@hey-api/shared'; 2 + import { operationResponsesMap } from '@hey-api/shared'; 4 3 5 - import { exportAst } from './export'; 6 - import type { Ast, IrSchemaToAstOptions } from './types'; 4 + import { buildOperationSchema } from './operation-schema'; 5 + import type { ProcessorContext, ProcessorResult } from './processor'; 6 + import type { IrSchemaToAstOptions } from './types'; 7 7 8 - export const irOperationToAst = ({ 9 - getAst, 8 + export function irOperationToAst({ 10 9 operation, 10 + path, 11 11 plugin, 12 - state, 13 - }: IrSchemaToAstOptions & { 14 - getAst: (schema: IR.SchemaObject, path: ReadonlyArray<string | number>) => Ast; 15 - operation: IR.OperationObject; 16 - }): void => { 12 + processor, 13 + tags, 14 + }: Pick<IrSchemaToAstOptions, 'plugin'> & 15 + Pick<ProcessorContext, 'path' | 'tags'> & { 16 + operation: IR.OperationObject; 17 + processor: ProcessorResult; 18 + }): void { 17 19 if (plugin.config.requests.enabled) { 18 - const requiredProperties = new Set<string>(); 19 - 20 - const schemaData: IR.SchemaObject = { 21 - properties: { 22 - body: { 23 - type: 'never', 24 - }, 25 - path: { 26 - type: 'never', 27 - }, 28 - query: { 29 - type: 'never', 30 - }, 31 - }, 32 - type: 'object', 33 - }; 34 - 35 - if (operation.parameters) { 36 - // TODO: add support for cookies 37 - 38 - if (operation.parameters.header) { 39 - const properties: Record<string, IR.SchemaObject> = {}; 40 - const required: Array<string> = []; 41 - 42 - for (const key in operation.parameters.header) { 43 - const parameter = operation.parameters.header[key]!; 44 - properties[parameter.name] = parameter.schema; 45 - if (parameter.required) { 46 - required.push(parameter.name); 47 - requiredProperties.add('headers'); 48 - } 49 - } 50 - 51 - if (Object.keys(properties).length) { 52 - schemaData.properties!.headers = { 53 - properties, 54 - required, 55 - type: 'object', 56 - }; 57 - } 58 - } 59 - 60 - if (operation.parameters.path) { 61 - const properties: Record<string, IR.SchemaObject> = {}; 62 - const required: Array<string> = []; 63 - 64 - for (const key in operation.parameters.path) { 65 - const parameter = operation.parameters.path[key]!; 66 - properties[parameter.name] = parameter.schema; 67 - if (parameter.required) { 68 - required.push(parameter.name); 69 - requiredProperties.add('path'); 70 - } 71 - } 72 - 73 - if (Object.keys(properties).length) { 74 - schemaData.properties!.path = { 75 - properties, 76 - required, 77 - type: 'object', 78 - }; 79 - } 80 - } 81 - 82 - if (operation.parameters.query) { 83 - const properties: Record<string, IR.SchemaObject> = {}; 84 - const required: Array<string> = []; 85 - 86 - for (const key in operation.parameters.query) { 87 - const parameter = operation.parameters.query[key]!; 88 - properties[parameter.name] = parameter.schema; 89 - if (parameter.required) { 90 - required.push(parameter.name); 91 - requiredProperties.add('query'); 92 - } 93 - } 94 - 95 - if (Object.keys(properties).length) { 96 - schemaData.properties!.query = { 97 - properties, 98 - required, 99 - type: 'object', 100 - }; 101 - } 102 - } 103 - } 104 - 105 - if (operation.body) { 106 - schemaData.properties!.body = operation.body.schema; 20 + const { schema } = buildOperationSchema(operation); 107 21 108 - if (operation.body.required) { 109 - requiredProperties.add('body'); 110 - } 111 - } 112 - 113 - schemaData.required = [...requiredProperties]; 114 - 115 - const ast = getAst(schemaData, fromRef(state.path)); 116 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.requests), { 22 + processor.process({ 117 23 meta: { 118 - category: 'schema', 119 - path: fromRef(state.path), 120 24 resource: 'operation', 121 25 resourceId: operation.id, 122 26 role: 'data', 123 - tags: fromRef(state.tags), 124 - tool: 'zod', 125 27 }, 126 - }); 127 - const typeInferSymbol = plugin.config.requests.types.infer.enabled 128 - ? plugin.symbol(applyNaming(operation.id, plugin.config.requests.types.infer), { 129 - meta: { 130 - category: 'type', 131 - path: fromRef(state.path), 132 - resource: 'operation', 133 - resourceId: operation.id, 134 - role: 'data', 135 - tags: fromRef(state.tags), 136 - tool: 'zod', 137 - variant: 'infer', 138 - }, 139 - }) 140 - : undefined; 141 - exportAst({ 142 - ast, 28 + naming: plugin.config.requests, 29 + namingAnchor: operation.id, 30 + path, 143 31 plugin, 144 - schema: schemaData, 145 - symbol, 146 - typeInferSymbol, 32 + schema, 33 + tags, 147 34 }); 148 35 } 149 36 ··· 152 39 const { response } = operationResponsesMap(operation); 153 40 154 41 if (response) { 155 - const path = [...fromRef(state.path), 'responses']; 156 - const ast = getAst(response, path); 157 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.responses), { 42 + processor.process({ 158 43 meta: { 159 - category: 'schema', 160 - path, 161 44 resource: 'operation', 162 45 resourceId: operation.id, 163 46 role: 'responses', 164 - tags: fromRef(state.tags), 165 - tool: 'zod', 166 47 }, 167 - }); 168 - const typeInferSymbol = plugin.config.responses.types.infer.enabled 169 - ? plugin.symbol(applyNaming(operation.id, plugin.config.responses.types.infer), { 170 - meta: { 171 - category: 'type', 172 - path, 173 - resource: 'operation', 174 - resourceId: operation.id, 175 - role: 'responses', 176 - tags: fromRef(state.tags), 177 - tool: 'zod', 178 - variant: 'infer', 179 - }, 180 - }) 181 - : undefined; 182 - exportAst({ 183 - ast, 48 + naming: plugin.config.responses, 49 + namingAnchor: operation.id, 50 + path: [...path, 'responses'], 184 51 plugin, 185 52 schema: response, 186 - symbol, 187 - typeInferSymbol, 53 + tags, 188 54 }); 189 55 } 190 56 } 191 57 } 192 - }; 58 + }
+16
packages/openapi-ts/src/plugins/zod/shared/processor.ts
··· 1 + import type { 2 + IR, 3 + NamingConfig, 4 + SchemaProcessorContext, 5 + SchemaProcessorResult, 6 + } from '@hey-api/shared'; 7 + 8 + import type { IrSchemaToAstOptions, TypeOptions } from './types'; 9 + 10 + export type ProcessorContext = Pick<IrSchemaToAstOptions, 'plugin'> & 11 + SchemaProcessorContext & { 12 + naming: NamingConfig & TypeOptions; 13 + schema: IR.SchemaObject; 14 + }; 15 + 16 + export type ProcessorResult = SchemaProcessorResult<ProcessorContext>;
+14 -1
packages/openapi-ts/src/plugins/zod/shared/types.ts
··· 1 1 import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { FeatureToggle, IR, NamingOptions, SchemaExtractor } from '@hey-api/shared'; 3 3 import type ts from 'typescript'; 4 4 5 5 import type { $ } from '../../../ts-dsl'; 6 6 import type { ZodPlugin } from '../types'; 7 + import type { ProcessorContext } from './processor'; 7 8 8 9 export type Ast = { 9 10 expression: ReturnType<typeof $.expr | typeof $.call>; ··· 12 13 }; 13 14 14 15 export type IrSchemaToAstOptions = { 16 + /** The plugin instance. */ 15 17 plugin: ZodPlugin['Instance']; 18 + /** Optional schema extractor function. */ 19 + schemaExtractor?: SchemaExtractor<ProcessorContext>; 20 + /** The plugin state references. */ 16 21 state: Refs<PluginState>; 17 22 }; 18 23 ··· 20 25 Pick<Partial<SymbolMeta>, 'tags'> & { 21 26 hasLazyExpression: boolean; 22 27 }; 28 + 29 + export type TypeOptions = { 30 + /** Configuration for TypeScript type generation from Zod schemas. */ 31 + types: { 32 + /** Configuration for `infer` types. */ 33 + infer: NamingOptions & FeatureToggle; 34 + }; 35 + }; 23 36 24 37 export type ValidatorArgs = { 25 38 operation: IR.OperationObject;
+20 -134
packages/openapi-ts/src/plugins/zod/shared/webhook.ts
··· 1 - import { fromRef } from '@hey-api/codegen-core'; 2 1 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming } from '@hey-api/shared'; 4 2 5 - import { exportAst } from './export'; 6 - import type { Ast, IrSchemaToAstOptions } from './types'; 3 + import { buildOperationSchema } from './operation-schema'; 4 + import type { ProcessorContext, ProcessorResult } from './processor'; 5 + import type { IrSchemaToAstOptions } from './types'; 7 6 8 - export const irWebhookToAst = ({ 9 - getAst, 7 + export function irWebhookToAst({ 10 8 operation, 9 + path, 11 10 plugin, 12 - state, 13 - }: IrSchemaToAstOptions & { 14 - getAst: (schema: IR.SchemaObject, path: ReadonlyArray<string | number>) => Ast; 15 - operation: IR.OperationObject; 16 - }) => { 11 + processor, 12 + tags, 13 + }: Pick<IrSchemaToAstOptions, 'plugin'> & 14 + Pick<ProcessorContext, 'path' | 'tags'> & { 15 + operation: IR.OperationObject; 16 + processor: ProcessorResult; 17 + }): void { 17 18 if (plugin.config.webhooks.enabled) { 18 - const requiredProperties = new Set<string>(); 19 - 20 - const schemaData: IR.SchemaObject = { 21 - properties: { 22 - body: { 23 - type: 'never', 24 - }, 25 - path: { 26 - type: 'never', 27 - }, 28 - query: { 29 - type: 'never', 30 - }, 31 - }, 32 - type: 'object', 33 - }; 34 - 35 - if (operation.parameters) { 36 - // TODO: add support for cookies 37 - 38 - if (operation.parameters.header) { 39 - const properties: Record<string, IR.SchemaObject> = {}; 40 - const required: Array<string> = []; 41 - 42 - for (const key in operation.parameters.header) { 43 - const parameter = operation.parameters.header[key]!; 44 - properties[parameter.name] = parameter.schema; 45 - if (parameter.required) { 46 - required.push(parameter.name); 47 - requiredProperties.add('headers'); 48 - } 49 - } 50 - 51 - if (Object.keys(properties).length) { 52 - schemaData.properties!.headers = { 53 - properties, 54 - required, 55 - type: 'object', 56 - }; 57 - } 58 - } 59 - 60 - if (operation.parameters.path) { 61 - const properties: Record<string, IR.SchemaObject> = {}; 62 - const required: Array<string> = []; 63 - 64 - for (const key in operation.parameters.path) { 65 - const parameter = operation.parameters.path[key]!; 66 - properties[parameter.name] = parameter.schema; 67 - if (parameter.required) { 68 - required.push(parameter.name); 69 - requiredProperties.add('path'); 70 - } 71 - } 72 - 73 - if (Object.keys(properties).length) { 74 - schemaData.properties!.path = { 75 - properties, 76 - required, 77 - type: 'object', 78 - }; 79 - } 80 - } 81 - 82 - if (operation.parameters.query) { 83 - const properties: Record<string, IR.SchemaObject> = {}; 84 - const required: Array<string> = []; 19 + const { schema } = buildOperationSchema(operation); 85 20 86 - for (const key in operation.parameters.query) { 87 - const parameter = operation.parameters.query[key]!; 88 - properties[parameter.name] = parameter.schema; 89 - if (parameter.required) { 90 - required.push(parameter.name); 91 - requiredProperties.add('query'); 92 - } 93 - } 94 - 95 - if (Object.keys(properties).length) { 96 - schemaData.properties!.query = { 97 - properties, 98 - required, 99 - type: 'object', 100 - }; 101 - } 102 - } 103 - } 104 - 105 - if (operation.body) { 106 - schemaData.properties!.body = operation.body.schema; 107 - 108 - if (operation.body.required) { 109 - requiredProperties.add('body'); 110 - } 111 - } 112 - 113 - schemaData.required = [...requiredProperties]; 114 - 115 - const ast = getAst(schemaData, fromRef(state.path)); 116 - const symbol = plugin.symbol(applyNaming(operation.id, plugin.config.webhooks), { 21 + processor.process({ 117 22 meta: { 118 - category: 'schema', 119 - path: fromRef(state.path), 120 23 resource: 'webhook', 121 24 resourceId: operation.id, 122 25 role: 'data', 123 - tags: fromRef(state.tags), 124 - tool: 'zod', 125 26 }, 126 - }); 127 - const typeInferSymbol = plugin.config.webhooks.types.infer.enabled 128 - ? plugin.symbol(applyNaming(operation.id, plugin.config.webhooks.types.infer), { 129 - meta: { 130 - category: 'type', 131 - path: fromRef(state.path), 132 - resource: 'webhook', 133 - resourceId: operation.id, 134 - role: 'data', 135 - tags: fromRef(state.tags), 136 - tool: 'zod', 137 - variant: 'infer', 138 - }, 139 - }) 140 - : undefined; 141 - exportAst({ 142 - ast, 27 + naming: plugin.config.webhooks, 28 + namingAnchor: operation.id, 29 + path, 143 30 plugin, 144 - schema: schemaData, 145 - symbol, 146 - typeInferSymbol, 31 + schema, 32 + tags, 147 33 }); 148 34 } 149 - }; 35 + }
+5 -32
packages/openapi-ts/src/plugins/zod/types.ts
··· 9 9 10 10 import type { IApi } from './api'; 11 11 import type { Resolvers } from './resolvers'; 12 + import type { TypeOptions } from './shared/types'; 12 13 13 14 export type UserConfig = Plugin.Name<'zod'> & 14 15 Plugin.Hooks & ··· 422 423 offset: boolean; 423 424 }; 424 425 /** Configuration for reusable schema definitions. */ 425 - definitions: NamingOptions & 426 - FeatureToggle & { 427 - /** Configuration for TypeScript type generation from Zod schemas. */ 428 - types: { 429 - /** Configuration for `infer` types. */ 430 - infer: NamingOptions & FeatureToggle; 431 - }; 432 - }; 426 + definitions: NamingOptions & FeatureToggle & TypeOptions; 433 427 /** Enable Zod metadata support? */ 434 428 metadata: boolean; 435 429 /** Configuration for request-specific Zod schemas. */ 436 - requests: NamingOptions & 437 - FeatureToggle & { 438 - /** Configuration for TypeScript type generation from Zod schemas. */ 439 - types: { 440 - /** Configuration for `infer` types. */ 441 - infer: NamingOptions & FeatureToggle; 442 - }; 443 - }; 430 + requests: NamingOptions & FeatureToggle & TypeOptions; 444 431 /** Configuration for response-specific Zod schemas. */ 445 - responses: NamingOptions & 446 - FeatureToggle & { 447 - /** Configuration for TypeScript type generation from Zod schemas. */ 448 - types: { 449 - /** Configuration for `infer` types. */ 450 - infer: NamingOptions & FeatureToggle; 451 - }; 452 - }; 432 + responses: NamingOptions & FeatureToggle & TypeOptions; 453 433 /** Configuration for TypeScript type generation from Zod schemas. */ 454 434 types: { 455 435 /** Configuration for `infer` types. */ ··· 459 439 }; 460 440 }; 461 441 /** Configuration for webhook-specific Zod schemas. */ 462 - webhooks: NamingOptions & 463 - FeatureToggle & { 464 - /** Configuration for TypeScript type generation from Zod schemas. */ 465 - types: { 466 - /** Configuration for `infer` types. */ 467 - infer: NamingOptions & FeatureToggle; 468 - }; 469 - }; 442 + webhooks: NamingOptions & FeatureToggle & TypeOptions; 470 443 }; 471 444 472 445 export type ZodPlugin = DefinePlugin<UserConfig, Config, IApi>;
+51 -75
packages/openapi-ts/src/plugins/zod/v3/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref, refs } from '@hey-api/codegen-core'; 2 + import { fromRef, ref } from '@hey-api/codegen-core'; 3 3 import type { IR, SchemaWithType } from '@hey-api/shared'; 4 - import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 4 + import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 5 5 6 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 7 7 import { $ } from '../../../ts-dsl'; 8 8 import { identifiers } from '../constants'; 9 - import { exportAst } from '../shared/export'; 10 9 import { getZodModule } from '../shared/module'; 11 10 import { irOperationToAst } from '../shared/operation'; 12 - import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; 11 + import type { Ast, IrSchemaToAstOptions } from '../shared/types'; 13 12 import { irWebhookToAst } from '../shared/webhook'; 14 13 import type { ZodPlugin } from '../types'; 14 + import { createProcessor } from './processor'; 15 15 import { irSchemaWithTypeToAst } from './toAst'; 16 16 17 17 export function irSchemaToAst({ 18 18 optional, 19 19 plugin, 20 20 schema, 21 + schemaExtractor, 21 22 state, 22 23 }: IrSchemaToAstOptions & { 23 24 /** ··· 28 29 optional?: boolean; 29 30 schema: IR.SchemaObject; 30 31 }): Ast { 32 + if (schemaExtractor && !schema.$ref) { 33 + const extracted = schemaExtractor({ 34 + meta: { 35 + resource: 'definition', 36 + resourceId: pathToJsonPointer(fromRef(state.path)), 37 + }, 38 + naming: plugin.config.definitions, 39 + path: fromRef(state.path), 40 + plugin, 41 + schema, 42 + }); 43 + if (extracted !== schema) schema = extracted; 44 + } 45 + 31 46 let ast: Partial<Ast> = {}; 32 47 33 48 const z = plugin.external('zod.z'); ··· 153 168 return ast as Ast; 154 169 } 155 170 156 - function handleComponent({ 157 - plugin, 158 - schema, 159 - state, 160 - }: IrSchemaToAstOptions & { 161 - schema: IR.SchemaObject; 162 - }): void { 163 - const $ref = pathToJsonPointer(fromRef(state.path)); 164 - const ast = irSchemaToAst({ plugin, schema, state }); 165 - const baseName = refToName($ref); 166 - const symbol = plugin.symbol(applyNaming(baseName, plugin.config.definitions), { 167 - meta: { 168 - category: 'schema', 169 - path: fromRef(state.path), 170 - resource: 'definition', 171 - resourceId: $ref, 172 - tags: fromRef(state.tags), 173 - tool: 'zod', 174 - }, 175 - }); 176 - const typeInferSymbol = plugin.config.definitions.types.infer.enabled 177 - ? plugin.symbol(applyNaming(baseName, plugin.config.definitions.types.infer), { 178 - meta: { 179 - category: 'type', 180 - path: fromRef(state.path), 181 - resource: 'definition', 182 - resourceId: $ref, 183 - tags: fromRef(state.tags), 184 - tool: 'zod', 185 - variant: 'infer', 186 - }, 187 - }) 188 - : undefined; 189 - exportAst({ 190 - ast, 191 - plugin, 192 - schema, 193 - symbol, 194 - typeInferSymbol, 195 - }); 196 - } 197 - 198 171 export const handlerV3: ZodPlugin['Handler'] = ({ plugin }) => { 199 172 plugin.symbol('z', { 200 173 external: getZodModule({ plugin }), ··· 204 177 }, 205 178 }); 206 179 180 + const processor = createProcessor(plugin); 181 + 207 182 plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { 208 - const state = refs<PluginState>({ 209 - hasLazyExpression: false, 210 - path: event._path, 211 - tags: event.tags, 212 - }); 213 183 switch (event.type) { 214 184 case 'operation': 215 185 irOperationToAst({ 216 - getAst: (schema, path) => { 217 - const state = refs<PluginState>({ 218 - hasLazyExpression: false, 219 - path, 220 - tags: event.tags, 221 - }); 222 - return irSchemaToAst({ plugin, schema, state }); 223 - }, 224 186 operation: event.operation, 187 + path: event._path, 225 188 plugin, 226 - state, 189 + processor, 190 + tags: event.tags, 227 191 }); 228 192 break; 229 193 case 'parameter': 230 - handleComponent({ 194 + processor.process({ 195 + meta: { 196 + resource: 'definition', 197 + resourceId: pathToJsonPointer(event._path), 198 + }, 199 + naming: plugin.config.definitions, 200 + path: event._path, 231 201 plugin, 232 202 schema: event.parameter.schema, 233 - state, 203 + tags: event.tags, 234 204 }); 235 205 break; 236 206 case 'requestBody': 237 - handleComponent({ 207 + processor.process({ 208 + meta: { 209 + resource: 'definition', 210 + resourceId: pathToJsonPointer(event._path), 211 + }, 212 + naming: plugin.config.definitions, 213 + path: event._path, 238 214 plugin, 239 215 schema: event.requestBody.schema, 240 - state, 216 + tags: event.tags, 241 217 }); 242 218 break; 243 219 case 'schema': 244 - handleComponent({ 220 + processor.process({ 221 + meta: { 222 + resource: 'definition', 223 + resourceId: pathToJsonPointer(event._path), 224 + }, 225 + naming: plugin.config.definitions, 226 + path: event._path, 245 227 plugin, 246 228 schema: event.schema, 247 - state, 229 + tags: event.tags, 248 230 }); 249 231 break; 250 232 case 'webhook': 251 233 irWebhookToAst({ 252 - getAst: (schema, path) => { 253 - const state = refs<PluginState>({ 254 - hasLazyExpression: false, 255 - path, 256 - tags: event.tags, 257 - }); 258 - return irSchemaToAst({ plugin, schema, state }); 259 - }, 260 234 operation: event.operation, 235 + path: event._path, 261 236 plugin, 262 - state, 237 + processor, 238 + tags: event.tags, 263 239 }); 264 240 break; 265 241 }
+58
packages/openapi-ts/src/plugins/zod/v3/processor.ts
··· 1 + import { refs } from '@hey-api/codegen-core'; 2 + import type { IR } from '@hey-api/shared'; 3 + import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { exportAst } from '../shared/export'; 6 + import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 + import type { PluginState } from '../shared/types'; 8 + import type { ZodPlugin } from '../types'; 9 + import { irSchemaToAst } from './plugin'; 10 + 11 + export function createProcessor(plugin: ZodPlugin['Instance']): ProcessorResult { 12 + const processor = createSchemaProcessor(); 13 + 14 + const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 15 + 16 + function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 + if (processor.hasEmitted(ctx.path)) { 18 + return ctx.schema; 19 + } 20 + 21 + for (const hook of hooks) { 22 + const result = hook?.shouldExtract?.(ctx); 23 + if (result) { 24 + process({ 25 + namingAnchor: processor.context.anchor, 26 + tags: processor.context.tags, 27 + ...ctx, 28 + }); 29 + return { $ref: pathToJsonPointer(ctx.path) }; 30 + } 31 + } 32 + 33 + return ctx.schema; 34 + } 35 + 36 + function process(ctx: ProcessorContext): void { 37 + if (!processor.markEmitted(ctx.path)) return; 38 + 39 + processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 + const state = refs<PluginState>({ 41 + hasLazyExpression: false, 42 + path: ctx.path, 43 + tags: ctx.tags, 44 + }); 45 + 46 + const ast = irSchemaToAst({ 47 + plugin, 48 + schema: ctx.schema, 49 + schemaExtractor: extractor, 50 + state, 51 + }); 52 + 53 + exportAst({ ...ctx, ast, plugin, state }); 54 + }); 55 + } 56 + 57 + return { process }; 58 + }
+51 -75
packages/openapi-ts/src/plugins/zod/v4/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref, refs } from '@hey-api/codegen-core'; 2 + import { fromRef, ref } from '@hey-api/codegen-core'; 3 3 import type { IR, SchemaWithType } from '@hey-api/shared'; 4 - import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 4 + import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 5 5 6 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 7 7 import { $ } from '../../../ts-dsl'; 8 8 import { identifiers } from '../constants'; 9 - import { exportAst } from '../shared/export'; 10 9 import { getZodModule } from '../shared/module'; 11 10 import { irOperationToAst } from '../shared/operation'; 12 - import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; 11 + import type { Ast, IrSchemaToAstOptions } from '../shared/types'; 13 12 import { irWebhookToAst } from '../shared/webhook'; 14 13 import type { ZodPlugin } from '../types'; 14 + import { createProcessor } from './processor'; 15 15 import { irSchemaWithTypeToAst } from './toAst'; 16 16 17 17 export function irSchemaToAst({ 18 18 optional, 19 19 plugin, 20 20 schema, 21 + schemaExtractor, 21 22 state, 22 23 }: IrSchemaToAstOptions & { 23 24 /** ··· 28 29 optional?: boolean; 29 30 schema: IR.SchemaObject; 30 31 }): Ast { 32 + if (schemaExtractor && !schema.$ref) { 33 + const extracted = schemaExtractor({ 34 + meta: { 35 + resource: 'definition', 36 + resourceId: pathToJsonPointer(fromRef(state.path)), 37 + }, 38 + naming: plugin.config.definitions, 39 + path: fromRef(state.path), 40 + plugin, 41 + schema, 42 + }); 43 + if (extracted !== schema) schema = extracted; 44 + } 45 + 31 46 let ast: Partial<Ast> = {}; 32 47 33 48 const z = plugin.external('zod.z'); ··· 157 172 return ast as Ast; 158 173 } 159 174 160 - function handleComponent({ 161 - plugin, 162 - schema, 163 - state, 164 - }: IrSchemaToAstOptions & { 165 - schema: IR.SchemaObject; 166 - }): void { 167 - const $ref = pathToJsonPointer(fromRef(state.path)); 168 - const ast = irSchemaToAst({ plugin, schema, state }); 169 - const baseName = refToName($ref); 170 - const symbol = plugin.symbol(applyNaming(baseName, plugin.config.definitions), { 171 - meta: { 172 - category: 'schema', 173 - path: fromRef(state.path), 174 - resource: 'definition', 175 - resourceId: $ref, 176 - tags: fromRef(state.tags), 177 - tool: 'zod', 178 - }, 179 - }); 180 - const typeInferSymbol = plugin.config.definitions.types.infer.enabled 181 - ? plugin.symbol(applyNaming(baseName, plugin.config.definitions.types.infer), { 182 - meta: { 183 - category: 'type', 184 - path: fromRef(state.path), 185 - resource: 'definition', 186 - resourceId: $ref, 187 - tags: fromRef(state.tags), 188 - tool: 'zod', 189 - variant: 'infer', 190 - }, 191 - }) 192 - : undefined; 193 - exportAst({ 194 - ast, 195 - plugin, 196 - schema, 197 - symbol, 198 - typeInferSymbol, 199 - }); 200 - } 201 - 202 175 export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { 203 176 plugin.symbol('z', { 204 177 external: getZodModule({ plugin }), ··· 209 182 }, 210 183 }); 211 184 185 + const processor = createProcessor(plugin); 186 + 212 187 plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { 213 - const state = refs<PluginState>({ 214 - hasLazyExpression: false, 215 - path: event._path, 216 - tags: event.tags, 217 - }); 218 188 switch (event.type) { 219 189 case 'operation': 220 190 irOperationToAst({ 221 - getAst: (schema, path) => { 222 - const state = refs<PluginState>({ 223 - hasLazyExpression: false, 224 - path, 225 - tags: event.tags, 226 - }); 227 - return irSchemaToAst({ plugin, schema, state }); 228 - }, 229 191 operation: event.operation, 192 + path: event._path, 230 193 plugin, 231 - state, 194 + processor, 195 + tags: event.tags, 232 196 }); 233 197 break; 234 198 case 'parameter': 235 - handleComponent({ 199 + processor.process({ 200 + meta: { 201 + resource: 'definition', 202 + resourceId: pathToJsonPointer(event._path), 203 + }, 204 + naming: plugin.config.definitions, 205 + path: event._path, 236 206 plugin, 237 207 schema: event.parameter.schema, 238 - state, 208 + tags: event.tags, 239 209 }); 240 210 break; 241 211 case 'requestBody': 242 - handleComponent({ 212 + processor.process({ 213 + meta: { 214 + resource: 'definition', 215 + resourceId: pathToJsonPointer(event._path), 216 + }, 217 + naming: plugin.config.definitions, 218 + path: event._path, 243 219 plugin, 244 220 schema: event.requestBody.schema, 245 - state, 221 + tags: event.tags, 246 222 }); 247 223 break; 248 224 case 'schema': 249 - handleComponent({ 225 + processor.process({ 226 + meta: { 227 + resource: 'definition', 228 + resourceId: pathToJsonPointer(event._path), 229 + }, 230 + naming: plugin.config.definitions, 231 + path: event._path, 250 232 plugin, 251 233 schema: event.schema, 252 - state, 234 + tags: event.tags, 253 235 }); 254 236 break; 255 237 case 'webhook': 256 238 irWebhookToAst({ 257 - getAst: (schema, path) => { 258 - const state = refs<PluginState>({ 259 - hasLazyExpression: false, 260 - path, 261 - tags: event.tags, 262 - }); 263 - return irSchemaToAst({ plugin, schema, state }); 264 - }, 265 239 operation: event.operation, 240 + path: event._path, 266 241 plugin, 267 - state, 242 + processor, 243 + tags: event.tags, 268 244 }); 269 245 break; 270 246 }
+58
packages/openapi-ts/src/plugins/zod/v4/processor.ts
··· 1 + import { refs } from '@hey-api/codegen-core'; 2 + import type { IR } from '@hey-api/shared'; 3 + import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; 4 + 5 + import { exportAst } from '../shared/export'; 6 + import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 + import type { PluginState } from '../shared/types'; 8 + import type { ZodPlugin } from '../types'; 9 + import { irSchemaToAst } from './plugin'; 10 + 11 + export function createProcessor(plugin: ZodPlugin['Instance']): ProcessorResult { 12 + const processor = createSchemaProcessor(); 13 + 14 + const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 15 + 16 + function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 + if (processor.hasEmitted(ctx.path)) { 18 + return ctx.schema; 19 + } 20 + 21 + for (const hook of hooks) { 22 + const result = hook?.shouldExtract?.(ctx); 23 + if (result) { 24 + process({ 25 + namingAnchor: processor.context.anchor, 26 + tags: processor.context.tags, 27 + ...ctx, 28 + }); 29 + return { $ref: pathToJsonPointer(ctx.path) }; 30 + } 31 + } 32 + 33 + return ctx.schema; 34 + } 35 + 36 + function process(ctx: ProcessorContext): void { 37 + if (!processor.markEmitted(ctx.path)) return; 38 + 39 + processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 + const state = refs<PluginState>({ 41 + hasLazyExpression: false, 42 + path: ctx.path, 43 + tags: ctx.tags, 44 + }); 45 + 46 + const ast = irSchemaToAst({ 47 + plugin, 48 + schema: ctx.schema, 49 + schemaExtractor: extractor, 50 + state, 51 + }); 52 + 53 + exportAst({ ...ctx, ast, plugin, state }); 54 + }); 55 + } 56 + 57 + return { process }; 58 + }
+10 -3
packages/shared/src/index.ts
··· 59 59 } from './ir/parameter'; 60 60 export { deduplicateSchema } from './ir/schema'; 61 61 export type { IR } from './ir/types'; 62 - export type { SchemaExtractor, SchemaExtractorContext } from './ir/utils'; 63 - export { addItemsToSchema, createSchemaExtractor, inlineSchema } from './ir/utils'; 62 + export { addItemsToSchema } from './ir/utils'; 64 63 export { parseOpenApiSpec } from './openApi'; 65 64 export type { OpenApiV2_0_X, OpenApiV2_0_XTypes } from './openApi/2.0.x'; 66 65 export { parseV2_0_X } from './openApi/2.0.x'; ··· 88 87 OpenApiSchemaObject, 89 88 } from './openApi/types'; 90 89 export type { Hooks } from './parser/hooks'; 90 + export type { 91 + SchemaExtractor, 92 + SchemaProcessor, 93 + SchemaProcessorContext, 94 + SchemaProcessorResult, 95 + } from './plugins/schema-processor'; 96 + export { createSchemaProcessor } from './plugins/schema-processor'; 91 97 export type { SchemaWithType } from './plugins/shared/types/schema'; 92 98 export { definePluginConfig, mappers } from './plugins/shared/utils/config'; 93 99 export type { PluginInstanceTypes } from './plugins/shared/utils/instance'; ··· 110 116 export { MinHeap } from './utils/minHeap'; 111 117 export { applyNaming, resolveNaming, toCase } from './utils/naming/naming'; 112 118 export type { Casing, NameTransformer, NamingConfig, NamingRule } from './utils/naming/types'; 119 + export { pathToName } from './utils/path'; 113 120 export { 114 121 encodeJsonPointerSegment, 115 - isTopLevelComponentRef, 122 + isTopLevelComponent, 116 123 jsonPointerToPath, 117 124 normalizeJsonPointer, 118 125 pathToJsonPointer,
-35
packages/shared/src/ir/utils.ts
··· 1 - import { pathToJsonPointer } from '../utils/ref'; 2 1 import type { IR } from './types'; 3 2 4 3 /** ··· 43 42 schema.items = items; 44 43 return schema; 45 44 } 46 - 47 - export type SchemaExtractorContext = { 48 - path: ReadonlyArray<string | number>; 49 - schema: IR.SchemaObject; 50 - }; 51 - 52 - export type SchemaExtractor = (ctx: SchemaExtractorContext) => IR.SchemaObject; 53 - 54 - export const inlineSchema: SchemaExtractor = (ctx) => ctx.schema; 55 - 56 - export function createSchemaExtractor({ 57 - callback, 58 - shouldExtract, 59 - }: { 60 - /** Called when a schema should be extracted. Should call irSchemaToAst with the provided path to extract the schema and register the symbol. */ 61 - callback: (ctx: SchemaExtractorContext) => void; 62 - /** Determines whether a schema at a given path should be extracted. */ 63 - shouldExtract: (ctx: SchemaExtractorContext) => boolean; 64 - }): SchemaExtractor { 65 - // track pointers to prevent infinite recursion 66 - const extractedPointers = new Set<string>(); 67 - 68 - const extractor: SchemaExtractor = (ctx) => { 69 - const pointer = pathToJsonPointer(ctx.path); 70 - if (extractedPointers.has(pointer) || !shouldExtract(ctx)) { 71 - return ctx.schema; 72 - } 73 - extractedPointers.add(pointer); 74 - callback(ctx); 75 - return { $ref: pointer }; 76 - }; 77 - 78 - return extractor; 79 - }
+2 -2
packages/shared/src/openApi/2.0.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '../../../openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '../../../openApi/shared/utils/discriminator'; 10 - import { isTopLevelComponentRef, refToName } from '../../../utils/ref'; 10 + import { isTopLevelComponent, refToName } from '../../../utils/ref'; 11 11 import type { SchemaObject } from '../types/spec'; 12 12 13 13 export const getSchemaType = ({ ··· 531 531 const irSchema: IR.SchemaObject = {}; 532 532 // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/definitions/Foo/properties/bar) 533 533 // to avoid generating orphaned named types or referencing unregistered symbols 534 - const isComponentsRef = isTopLevelComponentRef(schema.$ref); 534 + const isComponentsRef = isTopLevelComponent(schema.$ref); 535 535 if (!isComponentsRef) { 536 536 if (!state.circularReferenceTracker.has(schema.$ref)) { 537 537 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+2 -2
packages/shared/src/openApi/3.0.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '../../../openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '../../../openApi/shared/utils/discriminator'; 10 - import { isTopLevelComponentRef, refToName } from '../../../utils/ref'; 10 + import { isTopLevelComponent, refToName } from '../../../utils/ref'; 11 11 import type { ReferenceObject, SchemaObject } from '../types/spec'; 12 12 13 13 export const getSchemaType = ({ ··· 924 924 }): IR.SchemaObject => { 925 925 // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/components/schemas/Foo/properties/bar) 926 926 // to avoid generating orphaned named types or referencing unregistered symbols 927 - const isComponentsRef = isTopLevelComponentRef(schema.$ref); 927 + const isComponentsRef = isTopLevelComponent(schema.$ref); 928 928 if (!isComponentsRef) { 929 929 if (!state.circularReferenceTracker.has(schema.$ref)) { 930 930 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+2 -2
packages/shared/src/openApi/3.1.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '../../../openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '../../../openApi/shared/utils/discriminator'; 10 - import { isTopLevelComponentRef, refToName } from '../../../utils/ref'; 10 + import { isTopLevelComponent, refToName } from '../../../utils/ref'; 11 11 import type { SchemaObject } from '../types/spec'; 12 12 13 13 export const getSchemaTypes = ({ ··· 984 984 }): IR.SchemaObject => { 985 985 // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/components/schemas/Foo/properties/bar) 986 986 // to avoid generating orphaned named types or referencing unregistered symbols 987 - const isComponentsRef = isTopLevelComponentRef(schema.$ref); 987 + const isComponentsRef = isTopLevelComponent(schema.$ref); 988 988 if (!isComponentsRef) { 989 989 if (!state.circularReferenceTracker.has(schema.$ref)) { 990 990 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+23
packages/shared/src/parser/hooks.ts
··· 1 1 import type { Node, Symbol, SymbolIn } from '@hey-api/codegen-core'; 2 2 3 3 import type { IROperationObject } from '../ir/types'; 4 + import type { SchemaProcessorContext } from '../plugins/schema-processor'; 4 5 import type { PluginInstance } from '../plugins/shared/utils/instance'; 5 6 6 7 export type Hooks = { ··· 164 165 * ``` 165 166 */ 166 167 isQuery?: (operation: IROperationObject) => boolean | undefined; 168 + }; 169 + schemas?: { 170 + /** 171 + * Whether to extract the given schema into a separate symbol. 172 + * 173 + * This affects how schemas are processed and output. 174 + * 175 + * **Default behavior:** No schemas are extracted. 176 + * 177 + * @param ctx - The processing context for the schema. 178 + * @returns true to extract the schema, false to keep it inline, or undefined to fallback to default behavior. 179 + * @example 180 + * ```ts 181 + * shouldExtract: (ctx) => { 182 + * if (ctx.meta.resource === 'requestBody') { 183 + * return true; 184 + * } 185 + * return; // fallback to default behavior 186 + * } 187 + * ``` 188 + */ 189 + shouldExtract?: (ctx: SchemaProcessorContext) => boolean; 167 190 }; 168 191 /** 169 192 * Hooks specifically for overriding symbols behavior.
-2
packages/shared/src/plugins/index.ts
··· 1 - export type { SchemaWithType } from './shared/types/schema'; 2 - export type { DefinePlugin, Plugin } from './types';
+70
packages/shared/src/plugins/schema-processor.ts
··· 1 + import type { IR } from '../ir/types'; 2 + import { pathToJsonPointer } from '../utils/ref'; 3 + 4 + export interface SchemaProcessor { 5 + /** Current inherited context (set by withContext) */ 6 + readonly context: { 7 + readonly anchor: string | undefined; 8 + readonly tags: ReadonlyArray<string> | undefined; 9 + }; 10 + /** Check if pointer was already emitted */ 11 + hasEmitted: (path: ReadonlyArray<string | number>) => boolean; 12 + /** Mark pointer as emitted. Returns false if already emitted. */ 13 + markEmitted: (path: ReadonlyArray<string | number>) => boolean; 14 + /** Execute with inherited context for nested extractions */ 15 + withContext: <T>(ctx: { anchor?: string; tags?: ReadonlyArray<string> }, fn: () => T) => T; 16 + } 17 + 18 + export interface SchemaProcessorContext { 19 + meta: { resource: string; resourceId: string; role?: string }; 20 + namingAnchor?: string; 21 + path: ReadonlyArray<string | number>; 22 + schema: IR.SchemaObject; 23 + tags?: ReadonlyArray<string>; 24 + } 25 + 26 + export interface SchemaProcessorResult< 27 + Context extends SchemaProcessorContext = SchemaProcessorContext, 28 + > { 29 + process: (ctx: Context) => void; 30 + } 31 + 32 + export type SchemaExtractor<Context extends SchemaProcessorContext = SchemaProcessorContext> = ( 33 + ctx: Context, 34 + ) => IR.SchemaObject; 35 + 36 + export function createSchemaProcessor(): SchemaProcessor { 37 + const emitted = new Set<string>(); 38 + let contextTags: ReadonlyArray<string> | undefined; 39 + let contextAnchor: string | undefined; 40 + 41 + return { 42 + get context() { 43 + return { 44 + anchor: contextAnchor, 45 + tags: contextTags, 46 + }; 47 + }, 48 + hasEmitted(path) { 49 + return emitted.has(pathToJsonPointer(path)); 50 + }, 51 + markEmitted(path) { 52 + const pointer = pathToJsonPointer(path); 53 + if (emitted.has(pointer)) return false; 54 + emitted.add(pointer); 55 + return true; 56 + }, 57 + withContext(ctx, fn) { 58 + const prevTags = contextTags; 59 + const prevAnchor = contextAnchor; 60 + contextTags = ctx.tags; 61 + contextAnchor = ctx.anchor; 62 + try { 63 + return fn(); 64 + } finally { 65 + contextTags = prevTags; 66 + contextAnchor = prevAnchor; 67 + } 68 + }, 69 + }; 70 + }
+188
packages/shared/src/utils/__tests__/path.test.ts
··· 1 + import { pathToName } from '../path'; 2 + 3 + describe('pathToName', () => { 4 + // ── OpenAPI 3.x component schemas ── 5 + 6 + it('handles top-level schema', () => { 7 + expect(pathToName(['components', 'schemas', 'User'])).toBe('User'); 8 + }); 9 + 10 + it('handles nested property', () => { 11 + expect(pathToName(['components', 'schemas', 'User', 'properties', 'address'])).toBe( 12 + 'User-address', 13 + ); 14 + }); 15 + 16 + it('handles deeply nested properties', () => { 17 + expect( 18 + pathToName(['components', 'schemas', 'User', 'properties', 'address', 'properties', 'city']), 19 + ).toBe('User-address-city'); 20 + }); 21 + 22 + it('handles property literally named "properties"', () => { 23 + expect(pathToName(['components', 'schemas', 'Foo', 'properties', 'properties'])).toBe( 24 + 'Foo-properties', 25 + ); 26 + }); 27 + 28 + it('handles property named "properties" with children', () => { 29 + expect( 30 + pathToName([ 31 + 'components', 32 + 'schemas', 33 + 'Foo', 34 + 'properties', 35 + 'properties', 36 + 'properties', 37 + 'items', 38 + ]), 39 + ).toBe('Foo-properties-items'); 40 + }); 41 + 42 + it('handles property named "items"', () => { 43 + expect(pathToName(['components', 'schemas', 'Foo', 'properties', 'items'])).toBe('Foo-items'); 44 + }); 45 + 46 + // ── additionalProperties ── 47 + 48 + it('handles additionalProperties', () => { 49 + expect(pathToName(['components', 'schemas', 'Pet', 'additionalProperties'])).toBe('Pet-Value'); 50 + }); 51 + 52 + it('handles nested additionalProperties', () => { 53 + expect( 54 + pathToName([ 55 + 'components', 56 + 'schemas', 57 + 'Pet', 58 + 'properties', 59 + 'metadata', 60 + 'additionalProperties', 61 + ]), 62 + ).toBe('Pet-metadata-Value'); 63 + }); 64 + 65 + // ── Array items ── 66 + 67 + it('handles array items (skips index)', () => { 68 + expect( 69 + pathToName(['components', 'schemas', 'Order', 'properties', 'line_items', 'items', 0]), 70 + ).toBe('Order-line_items'); 71 + }); 72 + 73 + it('handles items without numeric index', () => { 74 + expect(pathToName(['components', 'schemas', 'Result', 'items', 0])).toBe('Result'); 75 + }); 76 + 77 + // ── Tuple items ── 78 + 79 + it('handles tuple items at different indices', () => { 80 + expect(pathToName(['components', 'schemas', 'Pair', 'items', 0])).toBe('Pair'); 81 + 82 + expect(pathToName(['components', 'schemas', 'Pair', 'items', 1])).toBe('Pair'); 83 + }); 84 + 85 + // ── patternProperties ── 86 + 87 + it('handles patternProperties', () => { 88 + expect(pathToName(['components', 'schemas', 'Config', 'patternProperties', '^x-'])).toBe( 89 + 'Config-^x-', 90 + ); 91 + }); 92 + 93 + // ── OpenAPI 2.0 ── 94 + 95 + it('handles definitions (OpenAPI 2.0)', () => { 96 + expect(pathToName(['definitions', 'User'])).toBe('User'); 97 + }); 98 + 99 + it('handles definitions with nested properties', () => { 100 + expect(pathToName(['definitions', 'User', 'properties', 'address'])).toBe('User-address'); 101 + }); 102 + 103 + // ── Paths (operations) ── 104 + 105 + it('handles simple path', () => { 106 + expect(pathToName(['paths', '/event', 'get', 'properties', 'query'])).toBe('Event-get-query'); 107 + }); 108 + 109 + it('handles path with multiple segments', () => { 110 + expect(pathToName(['paths', '/api/v1/users', 'post', 'properties', 'body'])).toBe( 111 + 'ApiV1Users-post-body', 112 + ); 113 + }); 114 + 115 + it('handles path with parameter', () => { 116 + expect(pathToName(['paths', '/users/{id}/posts', 'get', 'properties', 'query'])).toBe( 117 + 'UsersIdPosts-get-query', 118 + ); 119 + }); 120 + 121 + it('handles path without properties', () => { 122 + expect(pathToName(['paths', '/event', 'get'])).toBe('Event-get'); 123 + }); 124 + 125 + // ── Webhooks ── 126 + 127 + it('handles webhooks', () => { 128 + expect(pathToName(['webhooks', 'onEvent', 'post', 'properties', 'body'])).toBe( 129 + 'onEvent-post-body', 130 + ); 131 + }); 132 + 133 + // ── Component types beyond schemas ── 134 + 135 + it('handles component parameters', () => { 136 + expect(pathToName(['components', 'parameters', 'UserId'])).toBe('UserId'); 137 + }); 138 + 139 + it('handles component requestBodies', () => { 140 + expect(pathToName(['components', 'requestBodies', 'CreateUser', 'properties', 'name'])).toBe( 141 + 'CreateUser-name', 142 + ); 143 + }); 144 + 145 + // ── Encoded characters ── 146 + 147 + it('handles URI-encoded names', () => { 148 + expect(pathToName(['components', 'schemas', 'My%20Schema'])).toBe('My Schema'); 149 + }); 150 + 151 + // ── Anchor option ── 152 + 153 + it('uses anchor for component schema', () => { 154 + expect( 155 + pathToName(['components', 'schemas', 'User', 'properties', 'address'], { 156 + anchor: 'UserDTO', 157 + }), 158 + ).toBe('UserDTO-address'); 159 + }); 160 + 161 + it('uses anchor for paths', () => { 162 + expect( 163 + pathToName(['paths', '/event', 'get', 'properties', 'query'], { 164 + anchor: 'event.subscribe', 165 + }), 166 + ).toBe('event.subscribe-query'); 167 + }); 168 + 169 + it('uses anchor and preserves structural suffix', () => { 170 + expect( 171 + pathToName(['components', 'schemas', 'Pet', 'additionalProperties'], { 172 + anchor: 'PetMap', 173 + }), 174 + ).toBe('PetMap-Value'); 175 + }); 176 + 177 + it('uses anchor with deeply nested properties', () => { 178 + expect( 179 + pathToName(['components', 'schemas', 'User', 'properties', 'address', 'properties', 'city'], { 180 + anchor: 'UserInput', 181 + }), 182 + ).toBe('UserInput-address-city'); 183 + }); 184 + 185 + it('uses anchor for unknown root', () => { 186 + expect(pathToName(['foo', 'bar', 'baz'], { anchor: 'Root' })).toBe('Root'); 187 + }); 188 + });
+15 -15
packages/shared/src/utils/__tests__/ref.test.ts
··· 1 - import { isTopLevelComponentRef, jsonPointerToPath, pathToJsonPointer } from '../ref'; 1 + import { isTopLevelComponent, jsonPointerToPath, pathToJsonPointer } from '../ref'; 2 2 3 3 describe('jsonPointerToPath', () => { 4 4 it('parses root pointer', () => { ··· 32 32 }); 33 33 }); 34 34 35 - describe('isTopLevelComponentRef', () => { 35 + describe('isTopLevelComponent', () => { 36 36 describe('OpenAPI 3.x refs', () => { 37 37 it('returns true for top-level component refs', () => { 38 - expect(isTopLevelComponentRef('#/components/schemas/Foo')).toBe(true); 39 - expect(isTopLevelComponentRef('#/components/parameters/Bar')).toBe(true); 40 - expect(isTopLevelComponentRef('#/components/responses/Error')).toBe(true); 41 - expect(isTopLevelComponentRef('#/components/requestBodies/Body')).toBe(true); 38 + expect(isTopLevelComponent('#/components/schemas/Foo')).toBe(true); 39 + expect(isTopLevelComponent('#/components/parameters/Bar')).toBe(true); 40 + expect(isTopLevelComponent('#/components/responses/Error')).toBe(true); 41 + expect(isTopLevelComponent('#/components/requestBodies/Body')).toBe(true); 42 42 }); 43 43 44 44 it('returns false for deep path refs', () => { 45 - expect(isTopLevelComponentRef('#/components/schemas/Foo/properties/bar')).toBe(false); 46 - expect(isTopLevelComponentRef('#/components/schemas/Foo/properties/bar/items')).toBe(false); 47 - expect(isTopLevelComponentRef('#/components/schemas/Foo/allOf/0')).toBe(false); 45 + expect(isTopLevelComponent('#/components/schemas/Foo/properties/bar')).toBe(false); 46 + expect(isTopLevelComponent('#/components/schemas/Foo/properties/bar/items')).toBe(false); 47 + expect(isTopLevelComponent('#/components/schemas/Foo/allOf/0')).toBe(false); 48 48 }); 49 49 }); 50 50 51 51 describe('OpenAPI 2.0 refs', () => { 52 52 it('returns true for top-level definitions refs', () => { 53 - expect(isTopLevelComponentRef('#/definitions/Foo')).toBe(true); 54 - expect(isTopLevelComponentRef('#/definitions/Bar')).toBe(true); 53 + expect(isTopLevelComponent('#/definitions/Foo')).toBe(true); 54 + expect(isTopLevelComponent('#/definitions/Bar')).toBe(true); 55 55 }); 56 56 57 57 it('returns false for deep path refs', () => { 58 - expect(isTopLevelComponentRef('#/definitions/Foo/properties/bar')).toBe(false); 59 - expect(isTopLevelComponentRef('#/definitions/Foo/properties/bar/items')).toBe(false); 58 + expect(isTopLevelComponent('#/definitions/Foo/properties/bar')).toBe(false); 59 + expect(isTopLevelComponent('#/definitions/Foo/properties/bar/items')).toBe(false); 60 60 }); 61 61 }); 62 62 63 63 describe('non-component refs', () => { 64 64 it('returns false for path refs', () => { 65 - expect(isTopLevelComponentRef('#/paths/~1users/get')).toBe(false); 65 + expect(isTopLevelComponent('#/paths/~1users/get')).toBe(false); 66 66 }); 67 67 68 68 it('returns false for other refs', () => { 69 - expect(isTopLevelComponentRef('#/info/title')).toBe(false); 69 + expect(isTopLevelComponent('#/info/title')).toBe(false); 70 70 }); 71 71 }); 72 72 });
+142
packages/shared/src/utils/path.ts
··· 1 + /** 2 + * After these structural segments, the next segment has a known role. 3 + * This is what makes a property literally named "properties" safe — 4 + * it occupies the name position, never the structural position. 5 + */ 6 + const STRUCTURAL_ROLE: Record<string, 'name' | 'index'> = { 7 + items: 'index', 8 + patternProperties: 'name', 9 + properties: 'name', 10 + }; 11 + 12 + /** 13 + * These structural segments have no following name/index — 14 + * they are the terminal structural node. Append a suffix 15 + * to disambiguate from the parent. 16 + */ 17 + const STRUCTURAL_SUFFIX: Record<string, string> = { 18 + additionalProperties: 'Value', 19 + }; 20 + 21 + type RootContextConfig = { 22 + /** How many consecutive semantic segments follow before structural walking begins */ 23 + names: number; 24 + /** How many leading segments to skip (the root keyword + any category segment) */ 25 + skip: number; 26 + }; 27 + 28 + /** 29 + * Root context configuration. 30 + */ 31 + const ROOT_CONTEXT: Record<string | number, RootContextConfig> = { 32 + components: { names: 1, skip: 2 }, // components/schemas/{name} 33 + definitions: { names: 1, skip: 1 }, // definitions/{name} 34 + paths: { names: 2, skip: 1 }, // paths/{path}/{method} 35 + webhooks: { names: 2, skip: 1 }, // webhooks/{name}/{method} 36 + }; 37 + 38 + /** 39 + * Sanitizes a path segment for use in a derived name. 40 + * 41 + * Handles API path segments like `/api/v1/users/{id}` → `ApiV1UsersId`. 42 + */ 43 + function sanitizeSegment(segment: string | number): string { 44 + const str = String(segment); 45 + if (str.startsWith('/')) { 46 + return str 47 + .split('/') 48 + .filter(Boolean) 49 + .map((part) => { 50 + const clean = part.replace(/[{}]/g, ''); 51 + return clean.charAt(0).toUpperCase() + clean.slice(1); 52 + }) 53 + .join(''); 54 + } 55 + return str; 56 + } 57 + 58 + export interface PathToNameOptions { 59 + /** 60 + * When provided, replaces the root semantic segments with this anchor. 61 + * Structural suffixes are still derived from path. 62 + */ 63 + anchor?: string; 64 + } 65 + 66 + /** 67 + * Derives a composite name from a path. 68 + * 69 + * Examples: 70 + * .../User → 'User' 71 + * .../User/properties/address → 'UserAddress' 72 + * .../User/properties/properties → 'UserProperties' 73 + * .../User/properties/address/properties/city → 'UserAddressCity' 74 + * .../Pet/additionalProperties → 'PetValue' 75 + * .../Order/properties/items/items/0 → 'OrderItems' 76 + * paths//event/get/properties/query → 'EventGetQuery' 77 + * 78 + * With anchor: 79 + * paths//event/get/properties/query, { anchor: 'event.subscribe' } 80 + * → 'event.subscribe-Query' 81 + */ 82 + export function pathToName( 83 + path: ReadonlyArray<string | number>, 84 + options?: PathToNameOptions, 85 + ): string { 86 + const names: Array<string> = []; 87 + let index = 0; 88 + 89 + const rootContext = ROOT_CONTEXT[path[0]!]; 90 + if (rootContext) { 91 + index = rootContext.skip; 92 + 93 + if (options?.anchor) { 94 + // Use anchor as base name, skip past root semantic segments 95 + names.push(options.anchor); 96 + index += rootContext.names; 97 + } else { 98 + // Collect consecutive semantic name segments 99 + for (let n = 0; n < rootContext.names && index < path.length; n++) { 100 + names.push(sanitizeSegment(path[index]!)); 101 + index++; 102 + } 103 + } 104 + } else { 105 + // Unknown root 106 + if (options?.anchor) { 107 + names.push(options.anchor); 108 + index++; 109 + } else if (index < path.length) { 110 + names.push(sanitizeSegment(path[index]!)); 111 + index++; 112 + } 113 + } 114 + 115 + while (index < path.length) { 116 + const segment = String(path[index]); 117 + 118 + const role = STRUCTURAL_ROLE[segment]; 119 + if (role === 'name') { 120 + // Next segment is a semantic name — collect it 121 + index++; 122 + if (index < path.length) { 123 + names.push(sanitizeSegment(path[index]!)); 124 + } 125 + } else if (role === 'index') { 126 + // Next segment is a numeric index — skip it 127 + index++; 128 + if (index < path.length && typeof path[index] === 'number') { 129 + index++; 130 + } 131 + continue; 132 + } else if (STRUCTURAL_SUFFIX[segment]) { 133 + names.push(STRUCTURAL_SUFFIX[segment]); 134 + } 135 + 136 + index++; 137 + } 138 + 139 + // refs using unicode characters become encoded, didn't investigate why 140 + // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` 141 + return decodeURI(names.join('-')); 142 + }
+4 -4
packages/shared/src/utils/ref.ts
··· 92 92 } 93 93 94 94 /** 95 - * Checks if a $ref points to a top-level component (not a deep path reference). 95 + * Checks if a $ref or path points to a top-level component (not a deep path reference). 96 96 * 97 97 * Top-level component references: 98 98 * - OpenAPI 3.x: #/components/{type}/{name} (3 segments) ··· 101 101 * Deep path references (4+ segments for 3.x, 3+ for 2.0) should be inlined 102 102 * because they don't have corresponding registered symbols. 103 103 * 104 - * @param $ref - The $ref string to check 104 + * @param refOrPath - The $ref string or path array to check 105 105 * @returns true if the ref points to a top-level component, false otherwise 106 106 */ 107 - export function isTopLevelComponentRef($ref: string): boolean { 108 - const path = jsonPointerToPath($ref); 107 + export function isTopLevelComponent(refOrPath: string | ReadonlyArray<string | number>): boolean { 108 + const path = refOrPath instanceof Array ? refOrPath : jsonPointerToPath(refOrPath); 109 109 110 110 // OpenAPI 3.x: #/components/{type}/{name} = 3 segments 111 111 if (path[0] === 'components') {
+7 -1
turbo.json
··· 3 3 "tasks": { 4 4 "build": { 5 5 "dependsOn": ["^build"], 6 - "inputs": ["src/**", "package.json", "tsdown.config.ts", "tsconfig.json"], 6 + "inputs": [ 7 + "src/**", 8 + "!src/**/*.test.ts", 9 + "package.json", 10 + "tsconfig.json", 11 + "tsdown.config.ts" 12 + ], 7 13 "outputs": [ 8 14 ".next/**", 9 15 "!.next/cache/**",