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: zod plugin

Lubos 946684ef 947860e4

+1254 -1366
+9
CLAUDE.md
··· 88 88 - Object/interface keys sorted alphabetically 89 89 - Destructured keys sorted alphabetically 90 90 91 + ## Refactoring Guidelines 92 + 93 + When refactoring existing code: 94 + 95 + - **Preserve all JSDoc comments** - Read the original file first and keep all existing documentation 96 + - **Match existing code patterns** - Look at similar files in the codebase for conventions 97 + - **Prefer `edit` over `write`** - Making targeted edits preserves comments better than rewriting files 98 + - **Check reference implementations** - For plugin work, use Valibot as a reference for proper patterns 99 + 91 100 ## Pre-commit Checklist 92 101 93 102 Run before committing (Husky runs format + lint automatically, but also verify):
+8 -18
packages/openapi-ts/src/plugins/zod/mini/processor.ts
··· 1 - import { ref, refs } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 3 import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; 6 6 import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 - import type { PluginState, ZodAppliedResult } from '../shared/types'; 7 + import type { ZodFinal } from '../shared/types'; 8 8 import type { ZodPlugin } from '../types'; 9 9 import { createVisitor } from './walker'; 10 10 ··· 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 39 return 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 40 const visitor = createVisitor({ 47 41 schemaExtractor: extractor, 48 - state, 49 42 }); 50 43 const walk = createSchemaWalker(visitor); 51 44 ··· 53 46 path: ref(ctx.path), 54 47 plugin, 55 48 }); 56 - const ast = 57 - (visitor.applyModifiers(result, { 58 - path: ref(ctx.path), 59 - plugin, 60 - }) as ZodAppliedResult) ?? result.expression; 61 - if (result.hasLazyExpression) { 62 - state.hasLazyExpression['~ref'] = true; 63 - } 49 + 50 + const final = visitor.applyModifiers(result, { 51 + path: ref(ctx.path), 52 + plugin, 53 + }) as ZodFinal; 64 54 65 - exportAst({ ...ctx, ast, plugin, state }); 55 + exportAst({ ...ctx, final, plugin }); 66 56 }); 67 57 } 68 58
+57 -78
packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext, deduplicateSchema } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 import { unknownToAst } from './unknown'; 13 10 14 - export function arrayToAst( 15 - options: IrSchemaToAstOptions & { 16 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 17 - schema: SchemaWithType<'array'>; 18 - }, 19 - ): Omit<Ast, 'typeName'> { 20 - const { applyModifiers, plugin, walk } = options; 21 - let { schema } = options; 22 - 23 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 11 + export function arrayToAst({ 12 + applyModifiers, 13 + plugin, 14 + schema, 15 + walk, 16 + walkerCtx, 17 + }: { 18 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 19 + plugin: ZodPlugin['Instance']; 20 + schema: SchemaWithType<'array'>; 21 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 22 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 23 + }): CompositeHandlerResult { 24 + const childResults: Array<ZodResult> = []; 25 + let schemaCopy = schema; 24 26 25 27 const z = plugin.external('zod.z'); 26 28 const functionName = $(z).attr(identifiers.array); 27 29 28 - if (!schema.items) { 29 - result.expression = functionName.call( 30 + let arrayExpression: ReturnType<typeof $.call> | undefined; 31 + 32 + if (!schemaCopy.items) { 33 + arrayExpression = functionName.call( 30 34 unknownToAst({ 31 - ...options, 32 - schema: { 33 - type: 'unknown', 34 - }, 35 - }).expression, 35 + plugin, 36 + schema: { type: 'unknown' }, 37 + }), 36 38 ); 37 39 } else { 38 - schema = deduplicateSchema({ schema }); 40 + schemaCopy = deduplicateSchema({ schema: schemaCopy }); 39 41 40 - // at least one item is guaranteed 41 - const itemExpressions = schema.items!.map((item, index) => { 42 - const itemResult = walk( 43 - item, 44 - childContext( 45 - { 46 - path: options.state.path, 47 - plugin: options.plugin, 48 - }, 49 - 'items', 50 - index, 51 - ), 52 - ); 53 - if (itemResult.hasLazyExpression) { 54 - result.hasLazyExpression = true; 55 - } 42 + const itemExpressions: Array<Chain> = []; 43 + 44 + schemaCopy.items!.forEach((item, index) => { 45 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 46 + childResults.push(itemResult); 56 47 57 48 const finalExpr = applyModifiers(itemResult, { optional: false }); 58 - return finalExpr.expression; 49 + itemExpressions.push(finalExpr.expression); 59 50 }); 60 51 61 52 if (itemExpressions.length === 1) { 62 - result.expression = functionName.call(...itemExpressions); 53 + arrayExpression = functionName.call(...itemExpressions); 63 54 } else { 64 - if (schema.logicalOperator === 'and') { 65 - const firstSchema = schema.items![0]!; 66 - // we want to add an intersection, but not every schema can use the same API. 67 - // if the first item contains another array or not an object, we cannot use 68 - // `.intersection()` as that does not exist on `.union()` and non-object schemas. 69 - let intersectionExpression: ReturnType<typeof $.expr | typeof $.call>; 70 - if ( 71 - firstSchema.logicalOperator === 'or' || 72 - (firstSchema.type && firstSchema.type !== 'object') 73 - ) { 74 - intersectionExpression = $(z) 55 + if (schemaCopy.logicalOperator === 'and') { 56 + arrayExpression = functionName.call( 57 + $(z) 75 58 .attr(identifiers.intersection) 76 - .call(...itemExpressions); 77 - } else { 78 - intersectionExpression = itemExpressions[0]!; 79 - for (let i = 1; i < itemExpressions.length; i++) { 80 - intersectionExpression = $(z) 81 - .attr(identifiers.intersection) 82 - .call(intersectionExpression, itemExpressions[i]); 83 - } 84 - } 85 - 86 - result.expression = functionName.call(intersectionExpression); 59 + .call(...itemExpressions), 60 + ); 87 61 } else { 88 - result.expression = $(z) 62 + arrayExpression = $(z) 89 63 .attr(identifiers.array) 90 64 .call( 91 65 $(z) ··· 96 70 } 97 71 } 98 72 99 - const checks: Array<ReturnType<typeof $.call>> = []; 73 + if (schemaCopy.minItems === schemaCopy.maxItems && schemaCopy.minItems !== undefined) { 74 + arrayExpression = arrayExpression 75 + .attr(identifiers.length) 76 + .call($.fromValue(schemaCopy.minItems)); 77 + } else { 78 + const checks: Array<ReturnType<typeof $.call>> = []; 100 79 101 - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { 102 - checks.push($(z).attr(identifiers.length).call($.fromValue(schema.minItems))); 103 - } else { 104 - if (schema.minItems !== undefined) { 105 - checks.push($(z).attr(identifiers.minLength).call($.fromValue(schema.minItems))); 80 + if (schemaCopy.minItems !== undefined) { 81 + checks.push($(z).attr(identifiers.minLength).call($.fromValue(schemaCopy.minItems))); 106 82 } 107 83 108 - if (schema.maxItems !== undefined) { 109 - checks.push($(z).attr(identifiers.maxLength).call($.fromValue(schema.maxItems))); 84 + if (schemaCopy.maxItems !== undefined) { 85 + checks.push($(z).attr(identifiers.maxLength).call($.fromValue(schemaCopy.maxItems))); 110 86 } 111 - } 112 87 113 - if (checks.length > 0) { 114 - result.expression = result.expression.attr(identifiers.check).call(...checks); 88 + if (checks.length) { 89 + arrayExpression = arrayExpression.attr(identifiers.check).call(...checks); 90 + } 115 91 } 116 92 117 - return result as Omit<Ast, 'typeName'>; 93 + return { 94 + childResults, 95 + expression: arrayExpression, 96 + }; 118 97 }
+7 -12
packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function booleanToAst({ 8 9 plugin, 9 10 schema, 10 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 11 + }: { 12 + plugin: ZodPlugin['Instance']; 11 13 schema: SchemaWithType<'boolean'>; 12 - }): Omit<Ast, 'typeName'> { 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - let chain: ReturnType<typeof $.call>; 15 - 14 + }): Chain { 16 15 const z = plugin.external('zod.z'); 17 16 18 17 if (typeof schema.const === 'boolean') { 19 - chain = $(z).attr(identifiers.literal).call($.literal(schema.const)); 20 - result.expression = chain; 21 - return result as Omit<Ast, 'typeName'>; 18 + return $(z).attr(identifiers.literal).call($.literal(schema.const)); 22 19 } 23 20 24 - chain = $(z).attr(identifiers.boolean).call(); 25 - result.expression = chain; 26 - return result as Omit<Ast, 'typeName'>; 21 + return $(z).attr(identifiers.boolean).call(); 27 22 }
+23 -37
packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts
··· 4 4 import { identifiers } from '../../constants'; 5 5 import type { EnumResolverContext } from '../../resolvers'; 6 6 import type { Chain } from '../../shared/chain'; 7 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 7 + import type { ZodPlugin } from '../../types'; 8 8 import { unknownToAst } from './unknown'; 9 9 10 10 function itemsNode(ctx: EnumResolverContext): ReturnType<EnumResolverContext['nodes']['items']> { ··· 55 55 return $(z) 56 56 .attr(identifiers.enum) 57 57 .call($.array(...enumMembers)); 58 - } else if (literalMembers.length === 1) { 58 + } 59 + 60 + if (literalMembers.length === 1) { 59 61 return literalMembers[0]!; 60 - } else { 61 - return $(z) 62 - .attr(identifiers.union) 63 - .call($.array(...literalMembers)); 64 62 } 63 + 64 + return $(z) 65 + .attr(identifiers.union) 66 + .call($.array(...literalMembers)); 65 67 } 66 68 67 69 function enumResolver(ctx: EnumResolverContext): Chain { ··· 80 82 export function enumToAst({ 81 83 plugin, 82 84 schema, 83 - state, 84 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 85 + }: { 86 + plugin: ZodPlugin['Instance']; 85 87 schema: SchemaWithType<'enum'>; 86 - }): Omit<Ast, 'typeName'> { 88 + }): Chain { 87 89 const z = plugin.external('zod.z'); 88 90 89 - const { literalMembers } = itemsNode({ 90 - $, 91 - chain: { current: $(z) }, 92 - nodes: { base: baseNode, items: itemsNode }, 93 - plugin, 94 - schema, 95 - symbols: { z }, 96 - utils: { ast: {}, state }, 97 - }); 98 - 99 - if (!literalMembers.length) { 100 - return unknownToAst({ 101 - plugin, 102 - schema: { 103 - type: 'unknown', 104 - }, 105 - }); 106 - } 107 - 108 91 const ctx: EnumResolverContext = { 109 92 $, 110 93 chain: { ··· 119 102 symbols: { 120 103 z, 121 104 }, 122 - utils: { 123 - ast: {}, 124 - state, 125 - }, 126 105 }; 127 106 128 - const resolver = plugin.config['~resolvers']?.enum; 129 - const node = resolver?.(ctx) ?? enumResolver(ctx); 107 + const { literalMembers } = itemsNode(ctx); 130 108 131 - return { 132 - expression: node, 133 - }; 109 + if (!literalMembers.length) { 110 + return unknownToAst({ 111 + plugin, 112 + schema: { 113 + type: 'unknown', 114 + }, 115 + }); 116 + } 117 + 118 + const resolver = plugin.config['~resolvers']?.enum; 119 + return resolver?.(ctx) ?? enumResolver(ctx); 134 120 }
+6 -6
packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function neverToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'never'>; 11 - }): Omit<Ast, 'typeName'> { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - result.expression = $(z).attr(identifiers.never).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.never).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function nullToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'null'>; 11 - }): Omit<Ast, 'typeName'> { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - result.expression = $(z).attr(identifiers.null).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.null).call(); 16 16 }
+9 -14
packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts
··· 5 5 import { $ } from '../../../../ts-dsl'; 6 6 import { identifiers } from '../../constants'; 7 7 import type { NumberResolverContext } from '../../resolvers'; 8 - import type { Chain } from '../../shared/chain'; 9 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 8 + import type { Chain, ChainResult } from '../../shared/chain'; 9 + import type { ZodPlugin } from '../../types'; 10 10 11 11 function baseNode(ctx: NumberResolverContext): Chain { 12 12 const { schema, symbols } = ctx; ··· 21 21 return chain; 22 22 } 23 23 24 - function constNode(ctx: NumberResolverContext): Chain | undefined { 24 + function constNode(ctx: NumberResolverContext): ChainResult { 25 25 const { schema, symbols } = ctx; 26 26 const { z } = symbols; 27 27 if (schema.const === undefined) return; 28 28 return $(z).attr(identifiers.literal).call(ctx.utils.maybeBigInt(schema.const, schema.format)); 29 29 } 30 30 31 - function maxNode(ctx: NumberResolverContext): Chain | undefined { 31 + function maxNode(ctx: NumberResolverContext): ChainResult { 32 32 const { schema, symbols } = ctx; 33 33 const { z } = symbols; 34 34 if (schema.exclusiveMaximum !== undefined) { ··· 51 51 return; 52 52 } 53 53 54 - function minNode(ctx: NumberResolverContext): Chain | undefined { 54 + function minNode(ctx: NumberResolverContext): ChainResult { 55 55 const { schema, symbols } = ctx; 56 56 const { z } = symbols; 57 57 if (schema.exclusiveMinimum !== undefined) { ··· 102 102 export function numberToNode({ 103 103 plugin, 104 104 schema, 105 - state, 106 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 105 + }: { 106 + plugin: ZodPlugin['Instance']; 107 107 schema: SchemaWithType<'integer' | 'number'>; 108 - }): Omit<Ast, 'typeName'> { 109 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 108 + }): Chain { 110 109 const z = plugin.external('zod.z'); 111 110 const ctx: NumberResolverContext = { 112 111 $, ··· 125 124 z, 126 125 }, 127 126 utils: { 128 - ast, 129 127 getIntegerLimit, 130 128 maybeBigInt, 131 129 shouldCoerceToBigInt, 132 - state, 133 130 }, 134 131 }; 135 132 const resolver = plugin.config['~resolvers']?.number; 136 - const node = resolver?.(ctx) ?? numberResolver(ctx); 137 - ast.expression = node; 138 - return ast as Omit<Ast, 'typeName'>; 133 + return resolver?.(ctx) ?? numberResolver(ctx); 139 134 }
+30 -36
packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts
··· 5 5 import { identifiers } from '../../constants'; 6 6 import type { ObjectResolverContext } from '../../resolvers'; 7 7 import type { Chain } from '../../shared/chain'; 8 - import type { 9 - Ast, 10 - IrSchemaToAstOptions, 11 - ZodAppliedResult, 12 - ZodSchemaResult, 13 - } from '../../shared/types'; 8 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 14 9 import type { ZodPlugin } from '../../types'; 15 10 16 11 type WalkerCtx = SchemaVisitorContext<ZodPlugin['Instance']>; 17 12 18 - interface ObjectToAstOptions extends IrSchemaToAstOptions { 19 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 13 + interface ObjectToAstOptions { 14 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 15 + plugin: ZodPlugin['Instance']; 20 16 schema: SchemaWithType<'object'>; 21 - walk: Walker<ZodSchemaResult, ZodPlugin['Instance']>; 17 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 22 18 walkerCtx: WalkerCtx; 23 19 } 24 20 ··· 29 25 }; 30 26 31 27 function additionalPropertiesNode(ctx: ExtendedContext): Chain | null | undefined { 32 - const { applyModifiers, schema, walk, walkerCtx } = ctx; 28 + const { _childResults, applyModifiers, schema, walk, walkerCtx } = ctx; 33 29 34 30 if ( 35 31 !schema.additionalProperties || ··· 42 38 schema.additionalProperties, 43 39 childContext(walkerCtx, 'additionalProperties'), 44 40 ); 45 - if (additionalResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 46 - const ast = applyModifiers(additionalResult, {}); 47 - return ast.expression; 41 + _childResults.push(additionalResult); 42 + const finalExpr = applyModifiers(additionalResult, {}); 43 + return finalExpr.expression; 48 44 } 49 45 50 46 function baseNode(ctx: ExtendedContext): Chain { ··· 62 58 } 63 59 64 60 function objectResolver(ctx: ExtendedContext): Chain { 65 - // TODO: parser - handle constants 66 61 return ctx.nodes.base(ctx); 67 62 } 68 63 69 64 function shapeNode(ctx: ExtendedContext): ReturnType<typeof $.object> { 70 - const { applyModifiers, schema, walk, walkerCtx } = ctx; 65 + const { _childResults, applyModifiers, schema, walk, walkerCtx } = ctx; 71 66 const shape = $.object().pretty(); 72 67 73 68 for (const name in schema.properties) { ··· 75 70 const isOptional = !schema.required?.includes(name); 76 71 77 72 const propertyResult = walk(property, childContext(walkerCtx, 'properties', name)); 78 - if (propertyResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 73 + _childResults.push(propertyResult); 79 74 80 - const ast = applyModifiers(propertyResult, { 75 + const finalExpr = applyModifiers(propertyResult, { 81 76 optional: isOptional, 82 77 }); 83 78 84 - if (ast.hasLazyExpression) { 85 - ctx.utils.ast.hasLazyExpression = true; 86 - shape.getter(name, ast.expression.return()); 87 - } else { 88 - shape.prop(name, ast.expression); 89 - } 79 + shape.prop(name, finalExpr.expression); 90 80 } 91 81 92 82 return shape; 93 83 } 94 84 95 - export function objectToAst(options: ObjectToAstOptions): Omit<Ast, 'typeName'> { 96 - const { plugin } = options; 97 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 98 - const z = plugin.external('zod.z'); 85 + export function objectToAst(options: ObjectToAstOptions): CompositeHandlerResult { 86 + const { applyModifiers, plugin, schema, walk, walkerCtx } = options; 87 + 88 + const childResults: Array<ZodResult> = []; 89 + 99 90 const ctx: ExtendedContext = { 100 - ...options, 101 91 $, 92 + _childResults: childResults, 93 + applyModifiers, 102 94 chain: { 103 - current: $(z), 95 + current: $(plugin.external('zod.z')), 104 96 }, 105 97 nodes: { 106 98 additionalProperties: additionalPropertiesNode, ··· 108 100 shape: shapeNode, 109 101 }, 110 102 plugin, 103 + schema, 111 104 symbols: { 112 - z, 113 - }, 114 - utils: { 115 - ast, 116 - state: options.state, 105 + z: plugin.external('zod.z'), 117 106 }, 107 + walk, 108 + walkerCtx, 118 109 }; 119 110 const resolver = plugin.config['~resolvers']?.object; 120 111 const node = resolver?.(ctx) ?? objectResolver(ctx); 121 - ast.expression = node; 122 - return ast as Omit<Ast, 'typeName'>; 112 + 113 + return { 114 + childResults, 115 + expression: node, 116 + }; 123 117 }
+12 -14
packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts
··· 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 5 import type { StringResolverContext } from '../../resolvers'; 6 - import type { Chain } from '../../shared/chain'; 7 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 6 + import type { Chain, ChainResult } from '../../shared/chain'; 7 + import type { ZodPlugin } from '../../types'; 8 8 9 9 function baseNode(ctx: StringResolverContext): Chain { 10 10 const { z } = ctx.symbols; 11 11 return $(z).attr(identifiers.string).call(); 12 12 } 13 13 14 - function constNode(ctx: StringResolverContext): Chain | undefined { 14 + function constNode(ctx: StringResolverContext): ChainResult { 15 15 const { schema, symbols } = ctx; 16 16 const { z } = symbols; 17 17 if (typeof schema.const !== 'string') return; 18 18 return $(z).attr(identifiers.literal).call($.literal(schema.const)); 19 19 } 20 20 21 - function formatNode(ctx: StringResolverContext): Chain | undefined { 21 + function formatNode(ctx: StringResolverContext): ChainResult { 22 22 const { plugin, schema, symbols } = ctx; 23 23 const { z } = symbols; 24 24 ··· 51 51 return; 52 52 } 53 53 54 - function lengthNode(ctx: StringResolverContext): Chain | undefined { 54 + function lengthNode(ctx: StringResolverContext): ChainResult { 55 55 const { schema, symbols } = ctx; 56 56 const { z } = symbols; 57 57 if (schema.minLength === undefined || schema.minLength !== schema.maxLength) return; 58 58 return $(z).attr(identifiers.length).call($.literal(schema.minLength)); 59 59 } 60 60 61 - function maxLengthNode(ctx: StringResolverContext): Chain | undefined { 61 + function maxLengthNode(ctx: StringResolverContext): ChainResult { 62 62 const { schema, symbols } = ctx; 63 63 const { z } = symbols; 64 64 if (schema.maxLength === undefined) return; 65 65 return $(z).attr(identifiers.maxLength).call($.literal(schema.maxLength)); 66 66 } 67 67 68 - function minLengthNode(ctx: StringResolverContext): Chain | undefined { 68 + function minLengthNode(ctx: StringResolverContext): ChainResult { 69 69 const { schema, symbols } = ctx; 70 70 const { z } = symbols; 71 71 if (schema.minLength === undefined) return; 72 72 return $(z).attr(identifiers.minLength).call($.literal(schema.minLength)); 73 73 } 74 74 75 - function patternNode(ctx: StringResolverContext): Chain | undefined { 75 + function patternNode(ctx: StringResolverContext): ChainResult { 76 76 const { schema, symbols } = ctx; 77 77 const { z } = symbols; 78 78 if (!schema.pattern) return; ··· 119 119 export function stringToNode({ 120 120 plugin, 121 121 schema, 122 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 122 + }: { 123 + plugin: ZodPlugin['Instance']; 123 124 schema: SchemaWithType<'string'>; 124 - }): Omit<Ast, 'typeName'> { 125 + }): Chain { 125 126 const z = plugin.external('zod.z'); 126 127 const ctx: StringResolverContext = { 127 128 $, ··· 144 145 }, 145 146 }; 146 147 const resolver = plugin.config['~resolvers']?.string; 147 - const node = resolver?.(ctx) ?? stringResolver(ctx); 148 - return { 149 - expression: node, 150 - }; 148 + return resolver?.(ctx) ?? stringResolver(ctx); 151 149 }
+33 -40
packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 13 - export function tupleToAst( 14 - options: IrSchemaToAstOptions & { 15 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 16 - schema: SchemaWithType<'tuple'>; 17 - }, 18 - ): Omit<Ast, 'typeName'> { 19 - const { applyModifiers, plugin, schema, walk } = options; 20 - 21 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 10 + export function tupleToAst({ 11 + applyModifiers, 12 + plugin, 13 + schema, 14 + walk, 15 + walkerCtx, 16 + }: { 17 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 18 + plugin: ZodPlugin['Instance']; 19 + schema: SchemaWithType<'tuple'>; 20 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 21 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 22 + }): CompositeHandlerResult { 23 + const childResults: Array<ZodResult> = []; 22 24 23 25 const z = plugin.external('zod.z'); 24 26 ··· 26 28 const tupleElements = schema.const.map((value) => 27 29 $(z).attr(identifiers.literal).call($.fromValue(value)), 28 30 ); 29 - result.expression = $(z) 30 - .attr(identifiers.tuple) 31 - .call($.array(...tupleElements)); 32 - return result as Omit<Ast, 'typeName'>; 31 + return { 32 + childResults, 33 + expression: $(z) 34 + .attr(identifiers.tuple) 35 + .call($.array(...tupleElements)), 36 + }; 33 37 } 34 38 35 - const tupleElements: Array<ReturnType<typeof $.call | typeof $.expr>> = []; 39 + const tupleElements: Array<Chain> = []; 36 40 37 41 if (schema.items) { 38 42 schema.items.forEach((item, index) => { 39 - const itemResult = walk( 40 - item, 41 - childContext( 42 - { 43 - path: options.state.path, 44 - plugin: options.plugin, 45 - }, 46 - 'items', 47 - index, 48 - ), 49 - ); 50 - if (itemResult.hasLazyExpression) { 51 - result.hasLazyExpression = true; 52 - } 43 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 44 + childResults.push(itemResult); 53 45 54 46 const finalExpr = applyModifiers(itemResult, { optional: false }); 55 47 tupleElements.push(finalExpr.expression); 56 48 }); 57 49 } 58 50 59 - result.expression = $(z) 60 - .attr(identifiers.tuple) 61 - .call($.array(...tupleElements)); 62 - 63 - return result as Omit<Ast, 'typeName'>; 51 + return { 52 + childResults, 53 + expression: $(z) 54 + .attr(identifiers.tuple) 55 + .call($.array(...tupleElements)), 56 + }; 64 57 }
+6 -6
packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function undefinedToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'undefined'>; 11 - }): Omit<Ast, 'typeName'> { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - result.expression = $(z).attr(identifiers.undefined).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.undefined).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function unknownToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'unknown'>; 11 - }): Omit<Ast, 'typeName'> { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - result.expression = $(z).attr(identifiers.unknown).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.unknown).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function voidToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'void'>; 11 - }): Omit<Ast, 'typeName'> { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - result.expression = $(z).attr(identifiers.void).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.void).call(); 16 16 }
+160 -155
packages/openapi-ts/src/plugins/zod/mini/walker.ts
··· 1 - import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 1 + import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef } from '@hey-api/codegen-core'; 3 3 import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 4 4 import { pathToJsonPointer } from '@hey-api/shared'; ··· 6 6 import { $ } from '../../../ts-dsl'; 7 7 import { maybeBigInt, shouldCoerceToBigInt } from '../../shared/utils/coerce'; 8 8 import { identifiers } from '../constants'; 9 + import type { Chain } from '../shared/chain'; 10 + import { defaultMeta, inheritMeta } from '../shared/meta'; 9 11 import type { ProcessorContext } from '../shared/processor'; 10 - import type { Ast, PluginState, ZodAppliedResult, ZodSchemaResult } from '../shared/types'; 12 + import type { ZodFinal, ZodResult } from '../shared/types'; 11 13 import type { ZodPlugin } from '../types'; 12 14 import { arrayToAst } from './toAst/array'; 13 15 import { booleanToAst } from './toAst/boolean'; ··· 25 27 export interface VisitorConfig { 26 28 /** Optional schema extractor function. */ 27 29 schemaExtractor?: SchemaExtractor<ProcessorContext>; 28 - /** The plugin state references. */ 29 - state: Refs<PluginState>; 30 30 } 31 31 32 32 export function createVisitor( 33 33 config: VisitorConfig, 34 - ): SchemaVisitor<ZodSchemaResult, ZodPlugin['Instance']> { 35 - const { schemaExtractor, state } = config; 34 + ): SchemaVisitor<ZodResult, ZodPlugin['Instance']> { 35 + const { schemaExtractor } = config; 36 + 36 37 return { 37 - applyModifiers(result, ctx, options = {}): ZodAppliedResult { 38 + applyModifiers(result, ctx, options = {}): ZodFinal { 38 39 const { optional } = options; 39 40 const z = ctx.plugin.external('zod.z'); 40 - let expression = result.expression.expression; 41 + let expression = result.expression; 41 42 42 - if (result.readonly) { 43 + if (result.meta.readonly) { 43 44 expression = $(z).attr(identifiers.readonly).call(expression); 44 45 } 45 46 46 - const hasDefault = result.default !== undefined; 47 - const needsNullable = result.nullable; 47 + const hasDefault = result.meta.default !== undefined; 48 + const needsNullable = result.meta.nullable; 48 49 49 50 if (optional && needsNullable) { 50 51 expression = $(z).attr(identifiers.nullish).call(expression); ··· 59 60 .attr(identifiers._default) 60 61 .call( 61 62 expression, 62 - result.format 63 - ? maybeBigInt(result.default, result.format) 64 - : $.fromValue(result.default), 63 + result.meta.format 64 + ? maybeBigInt(result.meta.default, result.meta.format) 65 + : $.fromValue(result.meta.default), 65 66 ); 66 67 } 67 68 68 - return { expression }; 69 + return { 70 + expression, 71 + }; 69 72 }, 70 73 array(schema, ctx, walk) { 71 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 72 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 73 - const ast = arrayToAst({ 74 - ...ctx, 74 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 75 + this.applyModifiers(result, ctx, opts) as ZodFinal; 76 + const { childResults, expression } = arrayToAst({ 75 77 applyModifiers, 78 + plugin: ctx.plugin, 76 79 schema, 77 - state, 78 80 walk, 81 + walkerCtx: ctx, 79 82 }); 83 + 80 84 return { 81 - default: schema.default, 82 - expression: ast, 83 - hasLazyExpression: state.hasLazyExpression['~ref'], 84 - nullable: false, 85 - readonly: schema.accessScope === 'read', 85 + expression, 86 + meta: inheritMeta(schema, childResults), 86 87 }; 87 88 }, 88 89 boolean(schema, ctx) { 89 - const ast = booleanToAst({ ...ctx, schema }); 90 + const expression = booleanToAst({ plugin: ctx.plugin, schema }); 90 91 return { 91 - default: schema.default, 92 - expression: ast, 93 - nullable: false, 94 - readonly: schema.accessScope === 'read', 92 + expression, 93 + meta: defaultMeta(schema), 95 94 }; 96 95 }, 97 96 enum(schema, ctx) { 98 - const ast = enumToAst({ ...ctx, schema, state }); 97 + const expression = enumToAst({ plugin: ctx.plugin, schema }); 99 98 const hasNull = 100 99 schema.items?.some((item) => item.type === 'null' || item.const === null) ?? false; 101 100 return { 102 - default: schema.default, 103 - expression: ast, 104 - nullable: hasNull, 105 - readonly: schema.accessScope === 'read', 101 + expression, 102 + meta: { 103 + ...defaultMeta(schema), 104 + nullable: hasNull, 105 + }, 106 106 }; 107 107 }, 108 108 integer(schema, ctx) { 109 - const ast = numberToNode({ ...ctx, schema, state }); 109 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 110 110 return { 111 - default: schema.default, 112 - expression: ast, 113 - format: schema.format, 114 - nullable: false, 115 - readonly: schema.accessScope === 'read', 111 + expression, 112 + meta: { 113 + ...defaultMeta(schema), 114 + format: schema.format, 115 + }, 116 116 }; 117 117 }, 118 118 intercept(schema, ctx, walk) { ··· 134 134 }, 135 135 intersection(items, schemas, parentSchema, ctx) { 136 136 const z = ctx.plugin.external('zod.z'); 137 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 137 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 138 138 139 - let expression = items[0]!.expression.expression; 139 + let expression = items[0]!.expression; 140 140 items.slice(1).forEach((item) => { 141 141 expression = $(z) 142 142 .attr(identifiers.intersection) 143 143 .call( 144 144 expression, 145 - item.hasLazyExpression 146 - ? $(z).attr(identifiers.lazy).call($.func().do(item.expression.expression.return())) 147 - : item.expression.expression, 145 + item.meta.hasLazy 146 + ? $(z).attr(identifiers.lazy).call($.func().do(item.expression.return())) 147 + : item.expression, 148 148 ); 149 149 }); 150 150 151 151 return { 152 - default: parentSchema.default, 153 - expression: { expression }, 154 - hasLazyExpression: hasAnyLazy, 155 - nullable: items.some((i) => i.nullable), 156 - readonly: items.some((i) => i.readonly), 152 + expression, 153 + meta: { 154 + default: parentSchema.default, 155 + format: parentSchema.format, 156 + hasLazy: hasAnyLazy, 157 + isLazy: false, 158 + nullable: items.some((i) => i.meta.nullable), 159 + readonly: items.some((i) => i.meta.readonly), 160 + }, 157 161 }; 158 162 }, 159 163 never(schema, ctx) { 160 - const ast = neverToAst({ ...ctx, schema }); 164 + const expression = neverToAst({ plugin: ctx.plugin, schema }); 161 165 return { 162 - default: schema.default, 163 - expression: ast, 164 - nullable: false, 165 - readonly: false, 166 + expression, 167 + meta: { 168 + ...defaultMeta(schema), 169 + nullable: false, 170 + readonly: false, 171 + }, 166 172 }; 167 173 }, 168 174 null(schema, ctx) { 169 - const ast = nullToAst({ ...ctx, schema }); 175 + const expression = nullToAst({ plugin: ctx.plugin, schema }); 170 176 return { 171 - default: schema.default, 172 - expression: ast, 173 - nullable: false, 174 - readonly: false, 177 + expression, 178 + meta: { 179 + ...defaultMeta(schema), 180 + nullable: false, 181 + readonly: false, 182 + }, 175 183 }; 176 184 }, 177 185 number(schema, ctx) { 178 - const ast = numberToNode({ ...ctx, schema, state }); 186 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 179 187 return { 180 - default: schema.default, 181 - expression: ast, 182 - format: schema.format, 183 - nullable: false, 184 - readonly: schema.accessScope === 'read', 188 + expression, 189 + meta: { 190 + ...defaultMeta(schema), 191 + format: schema.format, 192 + }, 185 193 }; 186 194 }, 187 195 object(schema, ctx, walk) { 188 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 189 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 190 - const ast = objectToAst({ 196 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 197 + this.applyModifiers(result, ctx, opts) as ZodFinal; 198 + const { childResults, expression } = objectToAst({ 191 199 applyModifiers, 192 200 plugin: ctx.plugin, 193 201 schema, 194 - state, 195 202 walk, 196 203 walkerCtx: ctx, 197 204 }); 205 + 198 206 return { 199 - default: schema.default, 200 - expression: ast, 201 - hasLazyExpression: state.hasLazyExpression['~ref'], 202 - nullable: false, 203 - readonly: schema.accessScope === 'read', 207 + expression, 208 + meta: inheritMeta(schema, childResults), 204 209 }; 205 210 }, 206 211 postProcess(result, schema, ctx) { 207 - const metadata = ctx.plugin.config.metadata; 212 + const { metadata } = ctx.plugin.config; 213 + 208 214 if (!metadata) { 209 215 return result; 210 216 } 217 + 211 218 const node = $.object(); 212 - if (typeof metadata === 'function') { 213 - metadata({ $, node, schema }); 214 - } else if (schema.description) { 219 + 220 + if (metadata === true) { 221 + if (!schema.description) { 222 + return result; 223 + } 215 224 node.pretty().prop('description', $.literal(schema.description)); 225 + } else { 226 + metadata({ $, node, schema }); 216 227 } 228 + 217 229 if (node.isEmpty) { 218 230 return result; 219 231 } 232 + 220 233 const z = ctx.plugin.external('zod.z'); 234 + 221 235 return { 222 236 ...result, 223 - expression: { 224 - expression: result.expression.expression 225 - .attr(identifiers.register) 226 - .call($(z).attr(identifiers.globalRegistry), node), 227 - }, 237 + expression: result.expression 238 + .attr(identifiers.register) 239 + .call($(z).attr(identifiers.globalRegistry), node), 228 240 }; 229 241 }, 230 242 reference($ref, schema, ctx) { ··· 240 252 241 253 if (ctx.plugin.isSymbolRegistered(query)) { 242 254 return { 243 - default: schema.default, 244 - expression: { 245 - expression: $(refSymbol), 246 - }, 247 - nullable: false, 248 - readonly: schema.accessScope === 'read', 255 + expression: $(refSymbol), 256 + meta: defaultMeta(schema), 249 257 }; 250 258 } 251 259 252 - state.hasLazyExpression['~ref'] = true; 253 260 return { 254 - default: schema.default, 255 - expression: { 256 - expression: $(z) 257 - .attr(identifiers.lazy) 258 - .call($.func().returns('any').do($(refSymbol).return())), 261 + expression: $(z) 262 + .attr(identifiers.lazy) 263 + .call($.func().returns('any').do($(refSymbol).return())), 264 + meta: { 265 + ...defaultMeta(schema), 266 + hasLazy: true, 267 + isLazy: true, 259 268 }, 260 - hasLazyExpression: true, 261 - nullable: false, 262 - readonly: schema.accessScope === 'read', 263 269 }; 264 270 }, 265 271 string(schema, ctx) { 266 272 if (shouldCoerceToBigInt(schema.format)) { 267 - const ast = numberToNode({ 273 + const expression = numberToNode({ 268 274 plugin: ctx.plugin, 269 275 schema: { ...schema, type: 'number' }, 270 - state, 271 276 }); 272 277 return { 273 - default: schema.default, 274 - expression: ast, 275 - nullable: false, 276 - readonly: schema.accessScope === 'read', 278 + expression, 279 + meta: defaultMeta(schema), 277 280 }; 278 281 } 279 282 280 - const ast = stringToNode({ ...ctx, schema }); 283 + const expression = stringToNode({ plugin: ctx.plugin, schema }); 281 284 return { 282 - default: schema.default, 283 - expression: ast, 284 - nullable: false, 285 - readonly: schema.accessScope === 'read', 285 + expression, 286 + meta: defaultMeta(schema), 286 287 }; 287 288 }, 288 289 tuple(schema, ctx, walk) { 289 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 290 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 291 - const ast = tupleToAst({ 292 - ...ctx, 290 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 291 + this.applyModifiers(result, ctx, opts) as ZodFinal; 292 + const { childResults, expression } = tupleToAst({ 293 293 applyModifiers, 294 + plugin: ctx.plugin, 294 295 schema, 295 - state, 296 296 walk, 297 + walkerCtx: ctx, 297 298 }); 299 + 298 300 return { 299 - default: schema.default, 300 - expression: ast, 301 - hasLazyExpression: state.hasLazyExpression['~ref'], 302 - nullable: false, 303 - readonly: schema.accessScope === 'read', 301 + expression, 302 + meta: inheritMeta(schema, childResults), 304 303 }; 305 304 }, 306 305 undefined(schema, ctx) { 307 - const ast = undefinedToAst({ ...ctx, schema }); 306 + const expression = undefinedToAst({ plugin: ctx.plugin, schema }); 308 307 return { 309 - default: schema.default, 310 - expression: ast, 311 - nullable: false, 312 - readonly: false, 308 + expression, 309 + meta: { 310 + ...defaultMeta(schema), 311 + nullable: false, 312 + readonly: false, 313 + }, 313 314 }; 314 315 }, 315 316 union(items, schemas, parentSchema, ctx) { 316 317 const z = ctx.plugin.external('zod.z'); 317 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 318 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 318 319 319 - const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.nullable); 320 + const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.meta.nullable); 320 321 321 322 const nonNullItems: typeof items = []; 322 323 ··· 327 328 } 328 329 }); 329 330 330 - let expression: Ast; 331 + let expression: Chain; 331 332 if (nonNullItems.length === 0) { 332 - expression = { 333 - expression: $(z).attr(identifiers.null).call(), 334 - }; 333 + expression = $(z).attr(identifiers.null).call(); 335 334 } else if (nonNullItems.length === 1) { 336 335 expression = nonNullItems[0]!.expression; 337 336 } else { 338 - expression = { 339 - expression: $(z) 340 - .attr(identifiers.union) 341 - .call( 342 - $.array() 343 - .pretty() 344 - .elements(...nonNullItems.map((item) => item.expression.expression)), 345 - ), 346 - }; 337 + expression = $(z) 338 + .attr(identifiers.union) 339 + .call( 340 + $.array() 341 + .pretty() 342 + .elements(...nonNullItems.map((item) => item.expression)), 343 + ); 347 344 } 348 345 349 346 return { 350 - default: parentSchema.default, 351 347 expression, 352 - hasLazyExpression: hasAnyLazy, 353 - nullable: hasNull, 354 - readonly: items.some((i) => i.readonly), 348 + meta: { 349 + default: parentSchema.default, 350 + format: parentSchema.format, 351 + hasLazy: hasAnyLazy, 352 + isLazy: false, 353 + nullable: hasNull, 354 + readonly: items.some((i) => i.meta.readonly), 355 + }, 355 356 }; 356 357 }, 357 358 unknown(schema, ctx) { 358 - const ast = unknownToAst({ ...ctx, schema }); 359 + const expression = unknownToAst({ plugin: ctx.plugin, schema }); 359 360 return { 360 - default: schema.default, 361 - expression: ast, 362 - nullable: false, 363 - readonly: false, 361 + expression, 362 + meta: { 363 + ...defaultMeta(schema), 364 + nullable: false, 365 + readonly: false, 366 + }, 364 367 }; 365 368 }, 366 369 void(schema, ctx) { 367 - const ast = voidToAst({ ...ctx, schema }); 370 + const expression = voidToAst({ plugin: ctx.plugin, schema }); 368 371 return { 369 - default: schema.default, 370 - expression: ast, 371 - nullable: false, 372 - readonly: false, 372 + expression, 373 + meta: { 374 + ...defaultMeta(schema), 375 + nullable: false, 376 + readonly: false, 377 + }, 373 378 }; 374 379 }, 375 380 };
+69 -42
packages/openapi-ts/src/plugins/zod/resolvers.ts
··· 1 - import type { Refs, Symbol } from '@hey-api/codegen-core'; 1 + import type { Symbol } from '@hey-api/codegen-core'; 2 2 import type { IR, Plugin, SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 3 3 import type { MaybeArray } from '@hey-api/types'; 4 4 import type ts from 'typescript'; ··· 6 6 import type { MaybeBigInt, ShouldCoerceToBigInt } from '../../plugins/shared/utils/coerce'; 7 7 import type { GetIntegerLimit } from '../../plugins/shared/utils/formats'; 8 8 import type { $, DollarTsDsl, TsDsl } from '../../ts-dsl'; 9 - import type { Chain } from './shared/chain'; 10 - import type { Ast, PluginState, ZodSchemaResult } from './shared/types'; 9 + import type { Chain, ChainResult } from './shared/chain'; 10 + import type { ZodFinal, ZodResult } from './shared/types'; 11 11 import type { ZodPlugin } from './types'; 12 12 13 13 export type Resolvers = Plugin.Resolvers<{ ··· 18 18 * 19 19 * Returning `undefined` will execute the default resolver logic. 20 20 */ 21 - enum?: (ctx: EnumResolverContext) => Chain | undefined; 21 + enum?: (ctx: EnumResolverContext) => ChainResult; 22 22 /** 23 23 * Resolver for number schemas. 24 24 * ··· 26 26 * 27 27 * Returning `undefined` will execute the default resolver logic. 28 28 */ 29 - number?: (ctx: NumberResolverContext) => Chain | undefined; 29 + number?: (ctx: NumberResolverContext) => ChainResult; 30 30 /** 31 31 * Resolver for object schemas. 32 32 * ··· 34 34 * 35 35 * Returning `undefined` will execute the default resolver logic. 36 36 */ 37 - object?: (ctx: ObjectResolverContext) => Chain | undefined; 37 + object?: (ctx: ObjectResolverContext) => ChainResult; 38 38 /** 39 39 * Resolver for string schemas. 40 40 * ··· 42 42 * 43 43 * Returning `undefined` will execute the default resolver logic. 44 44 */ 45 - string?: (ctx: StringResolverContext) => Chain | undefined; 45 + string?: (ctx: StringResolverContext) => ChainResult; 46 46 /** 47 47 * Resolvers for request and response validators. 48 48 * ··· 82 82 /** 83 83 * The current chain. 84 84 * 85 - * In Zod, this represents a chain of call expressions ("chains") 86 - * being assembled to form a schema definition. 85 + * In Zod, this represents a chain of method calls being assembled 86 + * to form a schema definition (e.g., `z.string().min(1).max(10)`). 87 87 * 88 - * Each chain can be extended, modified, or replaced to customize 88 + * Each method can be extended, modified, or replaced to customize 89 89 * the resulting schema. 90 90 */ 91 91 current: Chain; ··· 106 106 */ 107 107 nodes: { 108 108 /** 109 - * Returns the base enum expression (z.enum([...]) or z.union([...]) for mixed types). 109 + * Returns the base enum expression (z.enum([...]) or z.union([...])). 110 110 */ 111 111 base: (ctx: EnumResolverContext) => Chain; 112 112 /** ··· 114 114 */ 115 115 items: (ctx: EnumResolverContext) => { 116 116 /** 117 - * Whether all enum items are strings (determines if z.enum can be used). 117 + * Whether all enum members are strings. 118 118 */ 119 119 allStrings: boolean; 120 120 /** ··· 126 126 */ 127 127 isNullable: boolean; 128 128 /** 129 - * z.literal(...) expressions for each non-null enum value. 129 + * Zod literal expressions for each enum member. 130 130 */ 131 131 literalMembers: Array<Chain>; 132 132 }; 133 133 }; 134 134 schema: SchemaWithType<'enum'>; 135 - /** 136 - * Utility functions for enum schema processing. 137 - */ 138 - utils: { 139 - ast: Partial<Omit<Ast, 'typeName'>>; 140 - state: Refs<PluginState>; 141 - }; 142 135 } 143 136 144 137 export interface NumberResolverContext extends BaseContext { ··· 146 139 * Nodes used to build different parts of the result. 147 140 */ 148 141 nodes: { 142 + /** 143 + * Returns the base number expression (z.number() or z.coerce.number()). 144 + */ 149 145 base: (ctx: NumberResolverContext) => Chain; 150 - const: (ctx: NumberResolverContext) => Chain | undefined; 151 - max: (ctx: NumberResolverContext) => Chain | undefined; 152 - min: (ctx: NumberResolverContext) => Chain | undefined; 146 + /** 147 + * Returns a literal expression for the const value, if present. 148 + */ 149 + const: (ctx: NumberResolverContext) => ChainResult; 150 + /** 151 + * Returns the maximum value constraint. 152 + */ 153 + max: (ctx: NumberResolverContext) => ChainResult; 154 + /** 155 + * Returns the minimum value constraint. 156 + */ 157 + min: (ctx: NumberResolverContext) => ChainResult; 153 158 }; 154 159 schema: SchemaWithType<'integer' | 'number'>; 155 160 /** 156 161 * Utility functions for number schema processing. 157 162 */ 158 163 utils: { 159 - ast: Partial<Omit<Ast, 'typeName'>>; 160 164 getIntegerLimit: GetIntegerLimit; 161 165 maybeBigInt: MaybeBigInt; 162 166 shouldCoerceToBigInt: ShouldCoerceToBigInt; 163 - state: Refs<PluginState>; 164 167 }; 165 168 } 166 169 167 170 export interface ObjectResolverContext extends BaseContext { 168 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => Ast; 171 + /** 172 + * Child results from processing object properties. 173 + * Used for metadata composition. 174 + */ 175 + _childResults: Array<ZodResult>; 176 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 169 177 /** 170 178 * Nodes used to build different parts of the result. 171 179 */ 172 180 nodes: { 173 181 /** 174 - * If `additionalProperties` is `false` or `{ type: 'never' }`, returns `null` 175 - * to indicate no additional properties are allowed. 182 + * Returns the additional properties expression, if any. 176 183 */ 177 184 additionalProperties: (ctx: ObjectResolverContext) => Chain | null | undefined; 185 + /** 186 + * Returns the base object expression (z.object({...}) or z.record(...)). 187 + */ 178 188 base: (ctx: ObjectResolverContext) => Chain; 189 + /** 190 + * Returns the object shape (property definitions). 191 + */ 179 192 shape: (ctx: ObjectResolverContext) => ReturnType<typeof $.object>; 180 193 }; 181 194 schema: SchemaWithType<'object'>; 182 - /** 183 - * Utility functions for object schema processing. 184 - */ 185 - utils: { 186 - ast: Partial<Omit<Ast, 'typeName'>>; 187 - state: Refs<PluginState>; 188 - }; 189 - walk: Walker<ZodSchemaResult, ZodPlugin['Instance']>; 195 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 190 196 walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 191 197 } 192 198 ··· 195 201 * Nodes used to build different parts of the result. 196 202 */ 197 203 nodes: { 204 + /** 205 + * Returns the base string expression (z.string()). 206 + */ 198 207 base: (ctx: StringResolverContext) => Chain; 199 - const: (ctx: StringResolverContext) => Chain | undefined; 200 - format: (ctx: StringResolverContext) => Chain | undefined; 201 - length: (ctx: StringResolverContext) => Chain | undefined; 202 - maxLength: (ctx: StringResolverContext) => Chain | undefined; 203 - minLength: (ctx: StringResolverContext) => Chain | undefined; 204 - pattern: (ctx: StringResolverContext) => Chain | undefined; 208 + /** 209 + * Returns a literal expression for the const value, if present. 210 + */ 211 + const: (ctx: StringResolverContext) => ChainResult; 212 + /** 213 + * Returns a format validation expression, if applicable. 214 + */ 215 + format: (ctx: StringResolverContext) => ChainResult; 216 + /** 217 + * Returns a length constraint (when min === max), if applicable. 218 + */ 219 + length: (ctx: StringResolverContext) => ChainResult; 220 + /** 221 + * Returns a maxLength constraint, if applicable. 222 + */ 223 + maxLength: (ctx: StringResolverContext) => ChainResult; 224 + /** 225 + * Returns a minLength constraint, if applicable. 226 + */ 227 + minLength: (ctx: StringResolverContext) => ChainResult; 228 + /** 229 + * Returns a pattern (regex) constraint, if applicable. 230 + */ 231 + pattern: (ctx: StringResolverContext) => ChainResult; 205 232 }; 206 233 schema: SchemaWithType<'string'>; 207 234 }
+1
packages/openapi-ts/src/plugins/zod/shared/chain.ts
··· 1 1 import type { $ } from '../../../ts-dsl'; 2 2 3 3 export type Chain = ReturnType<typeof $.call | typeof $.expr>; 4 + export type ChainResult = Chain | undefined;
+8 -11
packages/openapi-ts/src/plugins/zod/shared/export.ts
··· 4 4 import { $ } from '../../../ts-dsl'; 5 5 import { identifiers } from '../constants'; 6 6 import type { ProcessorContext } from './processor'; 7 - import type { Ast, IrSchemaToAstOptions } from './types'; 7 + import type { ZodFinal } from './types'; 8 8 9 9 export function exportAst({ 10 - ast, 10 + final, 11 11 meta, 12 12 naming, 13 13 namingAnchor, 14 14 path, 15 15 plugin, 16 16 schema, 17 - state, 18 17 tags, 19 - }: Pick<IrSchemaToAstOptions, 'state'> & 20 - ProcessorContext & { 21 - ast: Ast; 22 - }): void { 18 + }: ProcessorContext & { 19 + final: ZodFinal; 20 + meta?: Record<string, unknown>; 21 + }): void { 23 22 const z = plugin.external('zod.z'); 24 23 25 24 const name = pathToName(path, { anchor: namingAnchor }); ··· 49 48 const statement = $.const(symbol) 50 49 .export() 51 50 .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 52 - .$if(state.hasLazyExpression['~ref'] && state.anyType?.['~ref'], (c, v) => 53 - c.type($.type(z).attr(v)), 54 - ) 55 - .assign(ast.expression); 51 + .$if(final.typeName, (c) => c.type($.type(z).attr(final.typeName!))) 52 + .assign(final.expression); 56 53 plugin.node(statement); 57 54 58 55 if (typeInferSymbol) {
+54
packages/openapi-ts/src/plugins/zod/shared/meta.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import type { ZodMeta, ZodResult } from './types'; 4 + 5 + /** 6 + * Creates default metadata from a schema. 7 + */ 8 + export function defaultMeta(schema: IR.SchemaObject): ZodMeta { 9 + return { 10 + default: schema.default, 11 + format: schema.format, 12 + hasLazy: false, 13 + isLazy: false, 14 + isObject: false, 15 + nullable: false, 16 + readonly: schema.accessScope === 'read', 17 + }; 18 + } 19 + 20 + /** 21 + * Composes metadata from child results. 22 + * 23 + * @param children - Results from walking child schemas 24 + * @param overrides - Explicit overrides (e.g., from parent schema) 25 + */ 26 + export function composeMeta( 27 + children: ReadonlyArray<ZodResult>, 28 + overrides?: Partial<ZodMeta>, 29 + ): ZodMeta { 30 + return { 31 + default: overrides?.default, 32 + format: overrides?.format, 33 + hasLazy: overrides?.hasLazy ?? children.some((c) => c.meta.hasLazy), 34 + isLazy: overrides?.isLazy ?? children.some((c) => c.meta.isLazy), 35 + isObject: overrides?.isObject ?? children.some((c) => c.meta.isObject), 36 + nullable: overrides?.nullable ?? children.some((c) => c.meta.nullable), 37 + readonly: overrides?.readonly ?? children.some((c) => c.meta.readonly), 38 + }; 39 + } 40 + 41 + /** 42 + * Merges parent schema metadata with composed child metadata. 43 + * 44 + * @param parent - The parent schema 45 + * @param children - Results from walking child schemas 46 + */ 47 + export function inheritMeta(parent: IR.SchemaObject, children: ReadonlyArray<ZodResult>): ZodMeta { 48 + return composeMeta(children, { 49 + default: parent.default, 50 + format: parent.format, 51 + nullable: false, 52 + readonly: parent.accessScope === 'read', 53 + }); 54 + }
+4 -6
packages/openapi-ts/src/plugins/zod/shared/operation.ts
··· 3 3 4 4 import { buildOperationSchema } from './operation-schema'; 5 5 import type { ProcessorContext, ProcessorResult } from './processor'; 6 - import type { IrSchemaToAstOptions } from './types'; 7 6 8 7 export function irOperationToAst({ 9 8 operation, ··· 11 10 plugin, 12 11 processor, 13 12 tags, 14 - }: Pick<IrSchemaToAstOptions, 'plugin'> & 15 - Pick<ProcessorContext, 'path' | 'tags'> & { 16 - operation: IR.OperationObject; 17 - processor: ProcessorResult; 18 - }): void { 13 + }: Pick<ProcessorContext, 'path' | 'plugin' | 'tags'> & { 14 + operation: IR.OperationObject; 15 + processor: ProcessorResult; 16 + }): void { 19 17 if (plugin.config.requests.enabled) { 20 18 const { schema } = buildOperationSchema(operation); 21 19
+33 -40
packages/openapi-ts/src/plugins/zod/shared/types.ts
··· 1 - import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { FeatureToggle, IR, NamingOptions, Walker } from '@hey-api/shared'; 1 + import type { FeatureToggle, IR, NamingOptions } from '@hey-api/shared'; 3 2 import type ts from 'typescript'; 4 3 5 - import type { $ } from '../../../ts-dsl'; 6 4 import type { ZodPlugin } from '../types'; 5 + import type { Chain } from './chain'; 7 6 8 - export type Ast = { 9 - anyType?: string; 10 - expression: ReturnType<typeof $.expr | typeof $.call>; 11 - hasLazyExpression?: boolean; 12 - typeName?: string | ts.Identifier; 13 - }; 14 - 15 - export type IrSchemaToAstOptions = { 7 + export type ValidatorArgs = { 8 + operation: IR.OperationObject; 16 9 /** The plugin instance. */ 17 10 plugin: ZodPlugin['Instance']; 18 - /** The plugin state references. */ 19 - state: Refs<PluginState>; 20 - walk: Walker<ZodSchemaResult, ZodPlugin['Instance']>; 21 11 }; 22 12 23 - export type PluginState = Pick<Required<SymbolMeta>, 'path'> & 24 - Pick<Partial<SymbolMeta>, 'tags'> & { 25 - anyType?: string; 26 - hasLazyExpression: boolean; 27 - }; 28 - 29 13 export type TypeOptions = { 30 - /** Configuration for TypeScript type generation from Zod schemas. */ 31 14 types: { 32 - /** Configuration for `infer` types. */ 33 15 infer: NamingOptions & FeatureToggle; 34 16 }; 35 17 }; 36 18 37 - export type ValidatorArgs = { 38 - operation: IR.OperationObject; 39 - plugin: ZodPlugin['Instance']; 40 - }; 41 - 42 19 /** 43 - * The result from schema walking. 20 + * Metadata that flows through schema walking. 44 21 */ 45 - export interface ZodSchemaResult { 46 - /** Default value from schema, if any. */ 22 + export interface ZodMeta { 23 + /** Default value from schema. */ 47 24 default?: unknown; 48 - /** The Zod expression AST. */ 49 - expression: { expression: ReturnType<typeof $.expr | typeof $.call> }; 50 - /** The original schema format (for BigInt coercion). */ 25 + /** Original format (for BigInt coercion). */ 51 26 format?: string; 52 - /** Whether any child contains a lazy expression. */ 53 - hasLazyExpression?: boolean; 54 - /** Whether THIS result is itself lazy (not just inherited). */ 55 - isLazy?: boolean; 27 + /** Whether this or any child contains a lazy reference. */ 28 + hasLazy: boolean; 29 + /** Whether this schema itself is emitted as lazy. */ 30 + isLazy: boolean; 31 + /** Whether this schema resolves to an object shape. */ 32 + isObject?: boolean; 56 33 /** Does this schema explicitly allow null? */ 57 34 nullable: boolean; 58 35 /** Is this schema read-only? */ ··· 60 37 } 61 38 62 39 /** 63 - * The finalized expression after applyModifiers. 40 + * Result from walking a schema node. 64 41 */ 65 - export interface ZodAppliedResult { 66 - expression: ReturnType<typeof $.expr | typeof $.call>; 42 + export interface ZodResult { 43 + expression: Chain; 44 + meta: ZodMeta; 45 + } 46 + 47 + /** 48 + * Finalized result after applyModifiers. 49 + */ 50 + export interface ZodFinal extends Pick<ZodResult, 'expression'> { 51 + /** Type annotation for schemas requiring explicit typing (e.g., lazy). */ 52 + typeName?: string | ts.Identifier; 53 + } 54 + 55 + /** 56 + * Result from composite handlers that walk children. 57 + */ 58 + export interface CompositeHandlerResult extends Pick<ZodResult, 'expression'> { 59 + childResults: Array<ZodResult>; 67 60 }
+4 -6
packages/openapi-ts/src/plugins/zod/shared/webhook.ts
··· 2 2 3 3 import { buildOperationSchema } from './operation-schema'; 4 4 import type { ProcessorContext, ProcessorResult } from './processor'; 5 - import type { IrSchemaToAstOptions } from './types'; 6 5 7 6 export function irWebhookToAst({ 8 7 operation, ··· 10 9 plugin, 11 10 processor, 12 11 tags, 13 - }: Pick<IrSchemaToAstOptions, 'plugin'> & 14 - Pick<ProcessorContext, 'path' | 'tags'> & { 15 - operation: IR.OperationObject; 16 - processor: ProcessorResult; 17 - }): void { 12 + }: Pick<ProcessorContext, 'path' | 'plugin' | 'tags'> & { 13 + operation: IR.OperationObject; 14 + processor: ProcessorResult; 15 + }): void { 18 16 if (plugin.config.webhooks.enabled) { 19 17 const { schema } = buildOperationSchema(operation); 20 18
+8 -18
packages/openapi-ts/src/plugins/zod/v3/processor.ts
··· 1 - import { ref, refs } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 3 import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; 6 6 import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 - import type { PluginState, ZodAppliedResult } from '../shared/types'; 7 + import type { ZodFinal } from '../shared/types'; 8 8 import type { ZodPlugin } from '../types'; 9 9 import { createVisitor } from './walker'; 10 10 ··· 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 39 return 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 40 const visitor = createVisitor({ 47 41 schemaExtractor: extractor, 48 - state, 49 42 }); 50 43 const walk = createSchemaWalker(visitor); 51 44 ··· 53 46 path: ref(ctx.path), 54 47 plugin, 55 48 }); 56 - const ast = 57 - (visitor.applyModifiers(result, { 58 - path: ref(ctx.path), 59 - plugin, 60 - }) as ZodAppliedResult) ?? result.expression; 61 - if (result.hasLazyExpression) { 62 - state.hasLazyExpression['~ref'] = true; 63 - } 49 + 50 + const final = visitor.applyModifiers(result, { 51 + path: ref(ctx.path), 52 + plugin, 53 + }) as ZodFinal; 64 54 65 - exportAst({ ...ctx, ast, plugin, state }); 55 + exportAst({ ...ctx, final, plugin }); 66 56 }); 67 57 } 68 58
+48 -50
packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext, deduplicateSchema } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 import { unknownToAst } from './unknown'; 13 10 14 - export function arrayToAst( 15 - options: IrSchemaToAstOptions & { 16 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 17 - schema: SchemaWithType<'array'>; 18 - }, 19 - ): Omit<Ast, 'typeName'> { 20 - const { applyModifiers, plugin, walk } = options; 21 - let { schema } = options; 11 + interface ArrayToAstOptions { 12 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 13 + plugin: ZodPlugin['Instance']; 14 + schema: SchemaWithType<'array'>; 15 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 16 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 17 + } 18 + 19 + type AstExpression = Chain; 20 + 21 + export function arrayToAst({ 22 + applyModifiers, 23 + plugin, 24 + schema, 25 + walk, 26 + walkerCtx, 27 + }: ArrayToAstOptions): CompositeHandlerResult { 28 + const childResults: Array<ZodResult> = []; 29 + let schemaCopy = schema; 22 30 23 31 const z = plugin.external('zod.z'); 24 - 25 32 const functionName = $(z).attr(identifiers.array); 26 33 27 34 let arrayExpression: ReturnType<typeof $.call> | undefined; 28 - let hasLazyExpression = false; 29 35 30 - if (!schema.items) { 36 + if (!schemaCopy.items) { 31 37 arrayExpression = functionName.call( 32 38 unknownToAst({ 33 - ...options, 39 + plugin, 34 40 schema: { 35 41 type: 'unknown', 36 42 }, 37 43 }), 38 44 ); 39 45 } else { 40 - schema = deduplicateSchema({ schema }); 46 + schemaCopy = deduplicateSchema({ schema: schemaCopy }); 41 47 42 - // at least one item is guaranteed 43 - const itemExpressions = schema.items!.map((item, index) => { 44 - const itemResult = walk( 45 - item, 46 - childContext( 47 - { 48 - path: options.state.path, 49 - plugin: options.plugin, 50 - }, 51 - 'items', 52 - index, 53 - ), 54 - ); 55 - if (itemResult.hasLazyExpression) { 56 - hasLazyExpression = true; 57 - } 48 + const itemExpressions: Array<Chain> = []; 49 + 50 + schemaCopy.items!.forEach((item, index) => { 51 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 52 + childResults.push(itemResult); 58 53 59 54 const finalExpr = applyModifiers(itemResult, { optional: false }); 60 - return finalExpr.expression; 55 + itemExpressions.push(finalExpr.expression); 61 56 }); 62 57 63 58 if (itemExpressions.length === 1) { 64 59 arrayExpression = functionName.call(...itemExpressions); 65 60 } else { 66 - if (schema.logicalOperator === 'and') { 67 - const firstSchema = schema.items![0]!; 68 - // we want to add an intersection, but not every schema can use the same API. 69 - // if the first item contains another array or not an object, we cannot use 70 - // `.and()` as that does not exist on `.union()` and non-object schemas. 71 - let intersectionExpression: ReturnType<typeof $.call | typeof $.expr>; 61 + if (schemaCopy.logicalOperator === 'and') { 62 + const firstSchema = schemaCopy.items![0]!; 63 + let intersectionExpression: AstExpression; 72 64 if ( 73 65 firstSchema.logicalOperator === 'or' || 74 66 (firstSchema.type && firstSchema.type !== 'object') ··· 98 90 } 99 91 } 100 92 101 - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { 102 - arrayExpression = arrayExpression.attr(identifiers.length).call($.fromValue(schema.minItems)); 93 + if (schemaCopy.minItems === schemaCopy.maxItems && schemaCopy.minItems !== undefined) { 94 + arrayExpression = arrayExpression 95 + .attr(identifiers.length) 96 + .call($.fromValue(schemaCopy.minItems)); 103 97 } else { 104 - if (schema.minItems !== undefined) { 105 - arrayExpression = arrayExpression.attr(identifiers.min).call($.fromValue(schema.minItems)); 98 + if (schemaCopy.minItems !== undefined) { 99 + arrayExpression = arrayExpression 100 + .attr(identifiers.min) 101 + .call($.fromValue(schemaCopy.minItems)); 106 102 } 107 103 108 - if (schema.maxItems !== undefined) { 109 - arrayExpression = arrayExpression.attr(identifiers.max).call($.fromValue(schema.maxItems)); 104 + if (schemaCopy.maxItems !== undefined) { 105 + arrayExpression = arrayExpression 106 + .attr(identifiers.max) 107 + .call($.fromValue(schemaCopy.maxItems)); 110 108 } 111 109 } 112 110 113 111 return { 112 + childResults, 114 113 expression: arrayExpression, 115 - hasLazyExpression, 116 114 }; 117 115 }
+7 -9
packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function booleanToAst({ 8 9 plugin, 9 10 schema, 10 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 11 + }: { 12 + plugin: ZodPlugin['Instance']; 11 13 schema: SchemaWithType<'boolean'>; 12 - }): ReturnType<typeof $.call> { 13 - let chain: ReturnType<typeof $.call>; 14 - 14 + }): Chain { 15 15 const z = plugin.external('zod.z'); 16 16 17 17 if (typeof schema.const === 'boolean') { 18 - chain = $(z).attr(identifiers.literal).call($.literal(schema.const)); 19 - return chain; 18 + return $(z).attr(identifiers.literal).call($.literal(schema.const)); 20 19 } 21 20 22 - chain = $(z).attr(identifiers.boolean).call(); 23 - return chain; 21 + return $(z).attr(identifiers.boolean).call(); 24 22 }
+3 -27
packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts
··· 4 4 import { identifiers } from '../../constants'; 5 5 import type { EnumResolverContext } from '../../resolvers'; 6 6 import type { Chain } from '../../shared/chain'; 7 - import type { IrSchemaToAstOptions } from '../../shared/types'; 8 - import { unknownToAst } from './unknown'; 7 + import type { ZodPlugin } from '../../types'; 9 8 10 9 function itemsNode(ctx: EnumResolverContext): ReturnType<EnumResolverContext['nodes']['items']> { 11 10 const { schema, symbols } = ctx; ··· 80 79 export function enumToAst({ 81 80 plugin, 82 81 schema, 83 - state, 84 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 82 + }: { 83 + plugin: ZodPlugin['Instance']; 85 84 schema: SchemaWithType<'enum'>; 86 85 }): Chain { 87 86 const z = plugin.external('zod.z'); 88 87 89 - const { literalMembers } = itemsNode({ 90 - $, 91 - chain: { current: $(z) }, 92 - nodes: { base: baseNode, items: itemsNode }, 93 - plugin, 94 - schema, 95 - symbols: { z }, 96 - utils: { ast: {}, state }, 97 - }); 98 - 99 - if (!literalMembers.length) { 100 - return unknownToAst({ 101 - plugin, 102 - schema: { 103 - type: 'unknown', 104 - }, 105 - }); 106 - } 107 - 108 88 const ctx: EnumResolverContext = { 109 89 $, 110 90 chain: { ··· 118 98 schema, 119 99 symbols: { 120 100 z, 121 - }, 122 - utils: { 123 - ast: {}, 124 - state, 125 101 }, 126 102 }; 127 103
+6 -5
packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function neverToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'never'>; 11 - }) { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const expression = $(z).attr(identifiers.never).call(); 14 - return expression; 15 + return $(z).attr(identifiers.never).call(); 15 16 }
+6 -5
packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function nullToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'null'>; 11 - }) { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const expression = $(z).attr(identifiers.null).call(); 14 - return expression; 15 + return $(z).attr(identifiers.null).call(); 15 16 }
+8 -12
packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts
··· 5 5 import { $ } from '../../../../ts-dsl'; 6 6 import { identifiers } from '../../constants'; 7 7 import type { NumberResolverContext } from '../../resolvers'; 8 - import type { Chain } from '../../shared/chain'; 9 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 8 + import type { Chain, ChainResult } from '../../shared/chain'; 9 + import type { ZodPlugin } from '../../types'; 10 10 11 11 function baseNode(ctx: NumberResolverContext): Chain { 12 12 const { schema, symbols } = ctx; ··· 21 21 return chain; 22 22 } 23 23 24 - function constNode(ctx: NumberResolverContext): Chain | undefined { 24 + function constNode(ctx: NumberResolverContext): ChainResult { 25 25 const { schema, symbols } = ctx; 26 26 const { z } = symbols; 27 27 if (schema.const === undefined) return; 28 28 return $(z).attr(identifiers.literal).call(ctx.utils.maybeBigInt(schema.const, schema.format)); 29 29 } 30 30 31 - function maxNode(ctx: NumberResolverContext): Chain | undefined { 31 + function maxNode(ctx: NumberResolverContext): ChainResult { 32 32 const { chain, schema } = ctx; 33 33 if (schema.exclusiveMaximum !== undefined) { 34 34 return chain.current ··· 52 52 return; 53 53 } 54 54 55 - function minNode(ctx: NumberResolverContext): Chain | undefined { 55 + function minNode(ctx: NumberResolverContext): ChainResult { 56 56 const { chain, schema } = ctx; 57 57 if (schema.exclusiveMinimum !== undefined) { 58 58 return chain.current ··· 98 98 export function numberToNode({ 99 99 plugin, 100 100 schema, 101 - state, 102 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 101 + }: { 102 + plugin: ZodPlugin['Instance']; 103 103 schema: SchemaWithType<'integer' | 'number'>; 104 104 }): Chain { 105 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 106 105 const z = plugin.external('zod.z'); 107 106 const ctx: NumberResolverContext = { 108 107 $, ··· 121 120 z, 122 121 }, 123 122 utils: { 124 - ast, 125 123 getIntegerLimit, 126 124 maybeBigInt, 127 125 shouldCoerceToBigInt, 128 - state, 129 126 }, 130 127 }; 131 128 const resolver = plugin.config['~resolvers']?.number; 132 - const node = resolver?.(ctx) ?? numberResolver(ctx); 133 - return node; 129 + return resolver?.(ctx) ?? numberResolver(ctx); 134 130 }
+28 -37
packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts
··· 5 5 import { identifiers } from '../../constants'; 6 6 import type { ObjectResolverContext } from '../../resolvers'; 7 7 import type { Chain } from '../../shared/chain'; 8 - import type { 9 - Ast, 10 - IrSchemaToAstOptions, 11 - ZodAppliedResult, 12 - ZodSchemaResult, 13 - } from '../../shared/types'; 8 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 14 9 import type { ZodPlugin } from '../../types'; 15 10 16 11 type WalkerCtx = SchemaVisitorContext<ZodPlugin['Instance']>; 17 12 18 - interface ObjectToAstOptions extends IrSchemaToAstOptions { 19 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 13 + interface ObjectToAstOptions { 14 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 15 + plugin: ZodPlugin['Instance']; 20 16 schema: SchemaWithType<'object'>; 21 - walk: Walker<ZodSchemaResult, ZodPlugin['Instance']>; 17 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 22 18 walkerCtx: WalkerCtx; 23 19 } 24 20 25 - type ExtendedContext = ObjectResolverContext & { 26 - applyModifiers: ObjectToAstOptions['applyModifiers']; 27 - walk: ObjectToAstOptions['walk']; 28 - walkerCtx: ObjectToAstOptions['walkerCtx']; 29 - }; 21 + type ExtendedContext = ObjectResolverContext; 30 22 31 23 function additionalPropertiesNode(ctx: ExtendedContext): Chain | null | undefined { 32 24 const { applyModifiers, schema, walk, walkerCtx } = ctx; ··· 42 34 schema.additionalProperties, 43 35 childContext(walkerCtx, 'additionalProperties'), 44 36 ); 45 - if (additionalResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 46 - const ast = applyModifiers(additionalResult, {}); 47 - return ast.expression; 37 + ctx._childResults.push(additionalResult); 38 + const finalExpr = applyModifiers(additionalResult, {}); 39 + return finalExpr.expression; 48 40 } 49 41 50 42 function baseNode(ctx: ExtendedContext): Chain { ··· 62 54 } 63 55 64 56 function objectResolver(ctx: ExtendedContext): Chain { 65 - // TODO: parser - handle constants 66 57 return ctx.nodes.base(ctx); 67 58 } 68 59 ··· 75 66 const isOptional = !schema.required?.includes(name); 76 67 77 68 const propertyResult = walk(property, childContext(walkerCtx, 'properties', name)); 78 - if (propertyResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 69 + ctx._childResults.push(propertyResult); 79 70 80 - const ast = applyModifiers(propertyResult, { 71 + const finalExpr = applyModifiers(propertyResult, { 81 72 optional: isOptional, 82 73 }); 83 74 84 - if (ast.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 85 - shape.prop(name, ast.expression); 75 + shape.prop(name, finalExpr.expression); 86 76 } 87 77 88 78 return shape; 89 79 } 90 80 91 - export function objectToAst(options: ObjectToAstOptions): Omit<Ast, 'typeName'> { 92 - const { plugin } = options; 93 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 94 - const z = plugin.external('zod.z'); 81 + export function objectToAst(options: ObjectToAstOptions): CompositeHandlerResult { 82 + const { applyModifiers, plugin, schema, walk, walkerCtx } = options; 83 + 84 + const childResults: Array<ZodResult> = []; 85 + 95 86 const ctx: ExtendedContext = { 96 - ...options, 97 87 $, 88 + _childResults: childResults, 89 + applyModifiers, 98 90 chain: { 99 - current: $(z), 91 + current: $(plugin.external('zod.z')), 100 92 }, 101 93 nodes: { 102 94 additionalProperties: additionalPropertiesNode, ··· 104 96 shape: shapeNode, 105 97 }, 106 98 plugin, 99 + schema, 107 100 symbols: { 108 - z, 109 - }, 110 - utils: { 111 - ast, 112 - state: options.state, 101 + z: plugin.external('zod.z'), 113 102 }, 103 + walk, 104 + walkerCtx, 114 105 }; 115 106 const resolver = plugin.config['~resolvers']?.object; 116 107 const node = resolver?.(ctx) ?? objectResolver(ctx); 117 - ast.expression = node; 108 + 118 109 return { 119 - ...ast, 120 - anyType: identifiers.AnyZodObject, 121 - } as Omit<Ast, 'typeName'>; 110 + childResults, 111 + expression: node, 112 + }; 122 113 }
+11 -11
packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts
··· 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 5 import type { StringResolverContext } from '../../resolvers'; 6 - import type { Chain } from '../../shared/chain'; 7 - import type { IrSchemaToAstOptions } from '../../shared/types'; 6 + import type { Chain, ChainResult } from '../../shared/chain'; 7 + import type { ZodPlugin } from '../../types'; 8 8 9 9 function baseNode(ctx: StringResolverContext): Chain { 10 10 const { z } = ctx.symbols; 11 11 return $(z).attr(identifiers.string).call(); 12 12 } 13 13 14 - function constNode(ctx: StringResolverContext): Chain | undefined { 14 + function constNode(ctx: StringResolverContext): ChainResult { 15 15 const { schema, symbols } = ctx; 16 16 const { z } = symbols; 17 17 if (typeof schema.const !== 'string') return; 18 18 return $(z).attr(identifiers.literal).call($.literal(schema.const)); 19 19 } 20 20 21 - function formatNode(ctx: StringResolverContext): Chain | undefined { 21 + function formatNode(ctx: StringResolverContext): ChainResult { 22 22 const { chain, plugin, schema } = ctx; 23 23 24 24 switch (schema.format) { ··· 46 46 return; 47 47 } 48 48 49 - function lengthNode(ctx: StringResolverContext): Chain | undefined { 49 + function lengthNode(ctx: StringResolverContext): ChainResult { 50 50 const { chain, schema } = ctx; 51 51 if (schema.minLength === undefined || schema.minLength !== schema.maxLength) return; 52 52 return chain.current.attr(identifiers.length).call($.literal(schema.minLength)); 53 53 } 54 54 55 - function maxLengthNode(ctx: StringResolverContext): Chain | undefined { 55 + function maxLengthNode(ctx: StringResolverContext): ChainResult { 56 56 const { chain, schema } = ctx; 57 57 if (schema.maxLength === undefined) return; 58 58 return chain.current.attr(identifiers.max).call($.literal(schema.maxLength)); 59 59 } 60 60 61 - function minLengthNode(ctx: StringResolverContext): Chain | undefined { 61 + function minLengthNode(ctx: StringResolverContext): ChainResult { 62 62 const { chain, schema } = ctx; 63 63 if (schema.minLength === undefined) return; 64 64 return chain.current.attr(identifiers.min).call($.literal(schema.minLength)); 65 65 } 66 66 67 - function patternNode(ctx: StringResolverContext): Chain | undefined { 67 + function patternNode(ctx: StringResolverContext): ChainResult { 68 68 const { chain, schema } = ctx; 69 69 if (!schema.pattern) return; 70 70 const flags = /\\[pP]\{/.test(schema.pattern) ? 'u' : undefined; ··· 104 104 export function stringToNode({ 105 105 plugin, 106 106 schema, 107 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 107 + }: { 108 + plugin: ZodPlugin['Instance']; 108 109 schema: SchemaWithType<'string'>; 109 110 }): Chain { 110 111 const z = plugin.external('zod.z'); ··· 129 130 }, 130 131 }; 131 132 const resolver = plugin.config['~resolvers']?.string; 132 - const node = resolver?.(ctx) ?? stringResolver(ctx); 133 - return node; 133 + return resolver?.(ctx) ?? stringResolver(ctx); 134 134 }
+25 -33
packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 13 - export function tupleToAst( 14 - options: IrSchemaToAstOptions & { 15 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 16 - schema: SchemaWithType<'tuple'>; 17 - }, 18 - ): Omit<Ast, 'typeName'> { 19 - const { applyModifiers, plugin, schema, walk } = options; 10 + interface TupleToAstOptions { 11 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 12 + plugin: ZodPlugin['Instance']; 13 + schema: SchemaWithType<'tuple'>; 14 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 15 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 16 + } 20 17 21 - const z = plugin.external('zod.z'); 18 + export function tupleToAst({ 19 + applyModifiers, 20 + plugin, 21 + schema, 22 + walk, 23 + walkerCtx, 24 + }: TupleToAstOptions): CompositeHandlerResult { 25 + const childResults: Array<ZodResult> = []; 22 26 23 - let hasLazyExpression = false; 27 + const z = plugin.external('zod.z'); 24 28 25 29 if (schema.const && Array.isArray(schema.const)) { 26 30 const tupleElements = schema.const.map((value) => ··· 30 34 .attr(identifiers.tuple) 31 35 .call($.array(...tupleElements)); 32 36 return { 37 + childResults, 33 38 expression, 34 - hasLazyExpression, 35 39 }; 36 40 } 37 41 38 - const tupleElements: Array<ReturnType<typeof $.call | typeof $.expr>> = []; 42 + const tupleElements: Array<Chain> = []; 39 43 40 44 if (schema.items) { 41 45 schema.items.forEach((item, index) => { 42 - const itemResult = walk( 43 - item, 44 - childContext( 45 - { 46 - path: options.state.path, 47 - plugin: options.plugin, 48 - }, 49 - 'items', 50 - index, 51 - ), 52 - ); 53 - if (itemResult.hasLazyExpression) { 54 - hasLazyExpression = true; 55 - } 46 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 47 + childResults.push(itemResult); 56 48 57 49 const finalExpr = applyModifiers(itemResult, { optional: false }); 58 50 tupleElements.push(finalExpr.expression); ··· 64 56 .call($.array(...tupleElements)); 65 57 66 58 return { 59 + childResults, 67 60 expression, 68 - hasLazyExpression, 69 61 }; 70 62 }
+6 -5
packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function undefinedToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'undefined'>; 11 - }) { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const expression = $(z).attr(identifiers.undefined).call(); 14 - return expression; 15 + return $(z).attr(identifiers.undefined).call(); 15 16 }
+6 -5
packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function unknownToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'unknown'>; 11 - }) { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const expression = $(z).attr(identifiers.unknown).call(); 14 - return expression; 15 + return $(z).attr(identifiers.unknown).call(); 15 16 }
+6 -5
packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function voidToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'void'>; 11 - }) { 13 + }): Chain { 12 14 const z = plugin.external('zod.z'); 13 - const expression = $(z).attr(identifiers.void).call(); 14 - return expression; 15 + return $(z).attr(identifiers.void).call(); 15 16 }
+162 -179
packages/openapi-ts/src/plugins/zod/v3/walker.ts
··· 1 - import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref } from '@hey-api/codegen-core'; 1 + import type { SymbolMeta } from '@hey-api/codegen-core'; 2 + import { fromRef } from '@hey-api/codegen-core'; 3 3 import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 4 4 import { pathToJsonPointer } from '@hey-api/shared'; 5 5 6 6 import { $ } from '../../../ts-dsl'; 7 7 import { maybeBigInt, shouldCoerceToBigInt } from '../../shared/utils/coerce'; 8 8 import { identifiers } from '../constants'; 9 + import type { Chain } from '../shared/chain'; 10 + import { defaultMeta, inheritMeta } from '../shared/meta'; 9 11 import type { ProcessorContext } from '../shared/processor'; 10 - import type { PluginState, ZodAppliedResult, ZodSchemaResult } from '../shared/types'; 12 + import type { ZodFinal, ZodResult } from '../shared/types'; 11 13 import type { ZodPlugin } from '../types'; 12 14 import { arrayToAst } from './toAst/array'; 13 15 import { booleanToAst } from './toAst/boolean'; ··· 25 27 export interface VisitorConfig { 26 28 /** Optional schema extractor function. */ 27 29 schemaExtractor?: SchemaExtractor<ProcessorContext>; 28 - /** The plugin state references. */ 29 - state: Refs<PluginState>; 30 30 } 31 31 32 32 export function createVisitor( 33 33 config: VisitorConfig, 34 - ): SchemaVisitor<ZodSchemaResult, ZodPlugin['Instance']> { 35 - const { schemaExtractor, state } = config; 34 + ): SchemaVisitor<ZodResult, ZodPlugin['Instance']> { 35 + const { schemaExtractor } = config; 36 + 36 37 return { 37 - applyModifiers(result, ctx, options = {}): ZodAppliedResult { 38 + applyModifiers(result, ctx, options = {}): ZodFinal { 38 39 const { optional } = options; 39 - let expression = result.expression.expression; 40 + let expression = result.expression; 40 41 41 - if (result.readonly) { 42 + if (result.meta.readonly) { 42 43 expression = expression.attr(identifiers.readonly).call(); 43 44 } 44 45 45 - const hasDefault = result.default !== undefined; 46 - const needsNullable = result.nullable; 46 + const hasDefault = result.meta.default !== undefined; 47 + const needsNullable = result.meta.nullable; 47 48 48 49 if (optional && needsNullable) { 49 50 expression = expression.attr(identifiers.nullish).call(); ··· 57 58 expression = expression 58 59 .attr(identifiers.default) 59 60 .call( 60 - result.format 61 - ? maybeBigInt(result.default, result.format) 62 - : $.fromValue(result.default), 61 + result.meta.format 62 + ? maybeBigInt(result.meta.default, result.meta.format) 63 + : $.fromValue(result.meta.default), 63 64 ); 64 65 } 65 66 66 - return { expression }; 67 + return { 68 + expression, 69 + typeName: result.meta.hasLazy 70 + ? result.meta.isObject 71 + ? identifiers.AnyZodObject 72 + : identifiers.ZodTypeAny 73 + : undefined, 74 + }; 67 75 }, 68 76 array(schema, ctx, walk) { 69 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 70 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 71 - const ast = arrayToAst({ 72 - ...ctx, 77 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 78 + this.applyModifiers(result, ctx, opts) as ZodFinal; 79 + const { childResults, expression } = arrayToAst({ 73 80 applyModifiers, 81 + plugin: ctx.plugin, 74 82 schema, 75 - state, 76 83 walk, 84 + walkerCtx: ctx, 77 85 }); 86 + 78 87 return { 79 - default: schema.default, 80 - expression: ast, 81 - hasLazyExpression: state.hasLazyExpression['~ref'], 82 - nullable: false, 83 - readonly: schema.accessScope === 'read', 88 + expression, 89 + meta: inheritMeta(schema, childResults), 84 90 }; 85 91 }, 86 92 boolean(schema, ctx) { 87 - const ast = booleanToAst({ ...ctx, schema }); 93 + const expression = booleanToAst({ plugin: ctx.plugin, schema }); 88 94 return { 89 - default: schema.default, 90 - expression: { expression: ast }, 91 - nullable: false, 92 - readonly: schema.accessScope === 'read', 95 + expression, 96 + meta: defaultMeta(schema), 93 97 }; 94 98 }, 95 99 enum(schema, ctx) { 96 - const ast = enumToAst({ ...ctx, schema, state }); 100 + const expression = enumToAst({ plugin: ctx.plugin, schema }); 97 101 const hasNull = 98 102 schema.items?.some((item) => item.type === 'null' || item.const === null) ?? false; 99 103 return { 100 - default: schema.default, 101 - expression: { expression: ast }, 102 - nullable: hasNull, 103 - readonly: schema.accessScope === 'read', 104 + expression, 105 + meta: { 106 + ...defaultMeta(schema), 107 + nullable: hasNull, 108 + }, 104 109 }; 105 110 }, 106 111 integer(schema, ctx) { 107 - const ast = numberToNode({ ...ctx, schema, state }); 112 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 108 113 return { 109 - default: schema.default, 110 - expression: { expression: ast }, 111 - format: schema.format, 112 - nullable: false, 113 - readonly: schema.accessScope === 'read', 114 + expression, 115 + meta: { 116 + ...defaultMeta(schema), 117 + format: schema.format, 118 + }, 114 119 }; 115 120 }, 116 121 intercept(schema, ctx, walk) { ··· 132 137 }, 133 138 intersection(items, schemas, parentSchema, ctx) { 134 139 const z = ctx.plugin.external('zod.z'); 135 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 136 - 137 - if (hasAnyLazy) { 138 - state.anyType = ref(identifiers.ZodTypeAny); 139 - } 140 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 140 141 141 142 const firstSchema = schemas[0]; 142 - let expression: ZodSchemaResult['expression']; 143 + let expression: Chain; 143 144 144 - // If first item is a union or non-object, use z.intersection() 145 - // Otherwise use .and() chaining for better type inference 146 145 if ( 147 146 firstSchema?.logicalOperator === 'or' || 148 147 (firstSchema?.type && firstSchema.type !== 'object') 149 148 ) { 150 - expression = { 151 - expression: $(z) 152 - .attr(identifiers.intersection) 153 - .call(...items.map((item) => item.expression.expression)), 154 - }; 149 + expression = $(z) 150 + .attr(identifiers.intersection) 151 + .call(...items.map((item) => item.expression)); 155 152 } else { 156 153 expression = items[0]!.expression; 157 154 items.slice(1).forEach((item) => { 158 - expression = { 159 - expression: expression.expression 160 - .attr(identifiers.and) 161 - .call( 162 - item.hasLazyExpression && !item.isLazy 163 - ? $(z) 164 - .attr(identifiers.lazy) 165 - .call($.func().do(item.expression.expression.return())) 166 - : item.expression.expression, 167 - ), 168 - }; 155 + expression = expression 156 + .attr(identifiers.and) 157 + .call( 158 + item.meta.hasLazy && !item.meta.isLazy 159 + ? $(z).attr(identifiers.lazy).call($.func().do(item.expression.return())) 160 + : item.expression, 161 + ); 169 162 }); 170 163 } 171 164 172 165 return { 173 - default: parentSchema.default, 174 166 expression, 175 - hasLazyExpression: hasAnyLazy, 176 - nullable: items.some((i) => i.nullable), 177 - readonly: items.some((i) => i.readonly), 167 + meta: { 168 + default: parentSchema.default, 169 + format: parentSchema.format, 170 + hasLazy: hasAnyLazy, 171 + isLazy: false, 172 + nullable: items.some((i) => i.meta.nullable), 173 + readonly: items.some((i) => i.meta.readonly), 174 + }, 178 175 }; 179 176 }, 180 177 never(schema, ctx) { 181 - const ast = neverToAst({ ...ctx, schema }); 178 + const expression = neverToAst({ plugin: ctx.plugin, schema }); 182 179 return { 183 - default: schema.default, 184 - expression: { expression: ast }, 185 - nullable: false, 186 - readonly: false, 180 + expression, 181 + meta: { 182 + ...defaultMeta(schema), 183 + nullable: false, 184 + readonly: false, 185 + }, 187 186 }; 188 187 }, 189 188 null(schema, ctx) { 190 - const ast = nullToAst({ ...ctx, schema }); 189 + const expression = nullToAst({ plugin: ctx.plugin, schema }); 191 190 return { 192 - default: schema.default, 193 - expression: { expression: ast }, 194 - nullable: false, 195 - readonly: false, 191 + expression, 192 + meta: { 193 + ...defaultMeta(schema), 194 + nullable: false, 195 + readonly: false, 196 + }, 196 197 }; 197 198 }, 198 199 number(schema, ctx) { 199 - const ast = numberToNode({ ...ctx, schema, state }); 200 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 200 201 return { 201 - default: schema.default, 202 - expression: { expression: ast }, 203 - format: schema.format, 204 - nullable: false, 205 - readonly: schema.accessScope === 'read', 202 + expression, 203 + meta: { 204 + ...defaultMeta(schema), 205 + format: schema.format, 206 + }, 206 207 }; 207 208 }, 208 209 object(schema, ctx, walk) { 209 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 210 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 211 - const ast = objectToAst({ 210 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 211 + this.applyModifiers(result, ctx, opts) as ZodFinal; 212 + const { childResults, expression } = objectToAst({ 212 213 applyModifiers, 213 214 plugin: ctx.plugin, 214 215 schema, 215 - state, 216 216 walk, 217 217 walkerCtx: ctx, 218 218 }); 219 - if (state.hasLazyExpression['~ref'] && ast.anyType) { 220 - state.anyType = ref(ast.anyType); 221 - } 219 + 222 220 return { 223 - default: schema.default, 224 - expression: ast, 225 - hasLazyExpression: state.hasLazyExpression['~ref'], 226 - nullable: false, 227 - readonly: schema.accessScope === 'read', 221 + expression, 222 + meta: { 223 + ...inheritMeta(schema, childResults), 224 + isObject: true, 225 + }, 228 226 }; 229 227 }, 230 228 postProcess(result, schema, ctx) { 231 229 if (ctx.plugin.config.metadata && schema.description) { 232 230 return { 233 231 ...result, 234 - expression: { 235 - expression: result.expression.expression 236 - .attr(identifiers.describe) 237 - .call($.literal(schema.description)), 238 - }, 232 + expression: result.expression 233 + .attr(identifiers.describe) 234 + .call($.literal(schema.description)), 239 235 }; 240 236 } 241 237 return result; ··· 253 249 254 250 if (ctx.plugin.isSymbolRegistered(query)) { 255 251 return { 256 - default: schema.default, 257 - expression: { 258 - expression: $(refSymbol), 259 - }, 260 - nullable: false, 261 - readonly: schema.accessScope === 'read', 252 + expression: $(refSymbol), 253 + meta: defaultMeta(schema), 262 254 }; 263 255 } 264 256 265 - state.hasLazyExpression['~ref'] = true; 266 - state.anyType = ref(identifiers.ZodTypeAny); 267 257 return { 268 - default: schema.default, 269 - expression: { 270 - expression: $(z) 271 - .attr(identifiers.lazy) 272 - .call($.func().do($(refSymbol).return())), 258 + expression: $(z) 259 + .attr(identifiers.lazy) 260 + .call($.func().do($(refSymbol).return())), 261 + meta: { 262 + ...defaultMeta(schema), 263 + hasLazy: true, 264 + isLazy: true, 273 265 }, 274 - hasLazyExpression: true, 275 - isLazy: true, 276 - nullable: false, 277 - readonly: schema.accessScope === 'read', 278 266 }; 279 267 }, 280 268 string(schema, ctx) { 281 269 if (shouldCoerceToBigInt(schema.format)) { 282 - const ast = numberToNode({ 270 + const expression = numberToNode({ 283 271 plugin: ctx.plugin, 284 272 schema: { ...schema, type: 'number' }, 285 - state, 286 273 }); 287 274 return { 288 - default: schema.default, 289 - expression: { expression: ast }, 290 - nullable: false, 291 - readonly: schema.accessScope === 'read', 275 + expression, 276 + meta: defaultMeta(schema), 292 277 }; 293 278 } 294 279 295 - const ast = stringToNode({ ...ctx, schema }); 280 + const expression = stringToNode({ plugin: ctx.plugin, schema }); 296 281 return { 297 - default: schema.default, 298 - expression: { expression: ast }, 299 - nullable: false, 300 - readonly: schema.accessScope === 'read', 282 + expression, 283 + meta: defaultMeta(schema), 301 284 }; 302 285 }, 303 286 tuple(schema, ctx, walk) { 304 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 305 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 306 - const ast = tupleToAst({ 307 - ...ctx, 287 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 288 + this.applyModifiers(result, ctx, opts) as ZodFinal; 289 + const { childResults, expression } = tupleToAst({ 308 290 applyModifiers, 291 + plugin: ctx.plugin, 309 292 schema, 310 - state, 311 293 walk, 294 + walkerCtx: ctx, 312 295 }); 296 + 313 297 return { 314 - default: schema.default, 315 - expression: ast, 316 - hasLazyExpression: state.hasLazyExpression['~ref'], 317 - nullable: false, 318 - readonly: schema.accessScope === 'read', 298 + expression, 299 + meta: inheritMeta(schema, childResults), 319 300 }; 320 301 }, 321 302 undefined(schema, ctx) { 322 - const ast = undefinedToAst({ ...ctx, schema }); 303 + const expression = undefinedToAst({ plugin: ctx.plugin, schema }); 323 304 return { 324 - default: schema.default, 325 - expression: { expression: ast }, 326 - nullable: false, 327 - readonly: false, 305 + expression, 306 + meta: { 307 + ...defaultMeta(schema), 308 + nullable: false, 309 + readonly: false, 310 + }, 328 311 }; 329 312 }, 330 313 union(items, schemas, parentSchema, ctx) { 331 314 const z = ctx.plugin.external('zod.z'); 332 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 315 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 333 316 334 - const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.nullable); 335 - 336 - if (hasAnyLazy) { 337 - state.anyType = ref(identifiers.ZodTypeAny); 338 - } 317 + const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.meta.nullable); 339 318 340 319 const nonNullItems: typeof items = []; 341 320 ··· 346 325 } 347 326 }); 348 327 349 - let expression: ZodSchemaResult['expression']; 328 + let expression: Chain; 350 329 if (nonNullItems.length === 0) { 351 - expression = { 352 - expression: $(z).attr(identifiers.null).call(), 353 - }; 330 + expression = $(z).attr(identifiers.null).call(); 354 331 } else if (nonNullItems.length === 1) { 355 332 expression = nonNullItems[0]!.expression; 356 333 } else { 357 - expression = { 358 - expression: $(z) 359 - .attr(identifiers.union) 360 - .call( 361 - $.array() 362 - .pretty() 363 - .elements(...nonNullItems.map((item) => item.expression.expression)), 364 - ), 365 - }; 334 + expression = $(z) 335 + .attr(identifiers.union) 336 + .call( 337 + $.array() 338 + .pretty() 339 + .elements(...nonNullItems.map((item) => item.expression)), 340 + ); 366 341 } 367 342 368 343 return { 369 - default: parentSchema.default, 370 344 expression, 371 - hasLazyExpression: hasAnyLazy, 372 - nullable: hasNull, 373 - readonly: items.some((i) => i.readonly), 345 + meta: { 346 + default: parentSchema.default, 347 + format: parentSchema.format, 348 + hasLazy: hasAnyLazy, 349 + isLazy: false, 350 + nullable: hasNull, 351 + readonly: items.some((i) => i.meta.readonly), 352 + }, 374 353 }; 375 354 }, 376 355 unknown(schema, ctx) { 377 - const ast = unknownToAst({ ...ctx, schema }); 356 + const expression = unknownToAst({ plugin: ctx.plugin, schema }); 378 357 return { 379 - default: schema.default, 380 - expression: { expression: ast }, 381 - nullable: false, 382 - readonly: false, 358 + expression, 359 + meta: { 360 + ...defaultMeta(schema), 361 + nullable: false, 362 + readonly: false, 363 + }, 383 364 }; 384 365 }, 385 366 void(schema, ctx) { 386 - const ast = voidToAst({ ...ctx, schema }); 367 + const expression = voidToAst({ plugin: ctx.plugin, schema }); 387 368 return { 388 - default: schema.default, 389 - expression: { expression: ast }, 390 - nullable: false, 391 - readonly: false, 369 + expression, 370 + meta: { 371 + ...defaultMeta(schema), 372 + nullable: false, 373 + readonly: false, 374 + }, 392 375 }; 393 376 }, 394 377 };
+8 -18
packages/openapi-ts/src/plugins/zod/v4/processor.ts
··· 1 - import { ref, refs } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 3 import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; 6 6 import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 - import type { PluginState, ZodAppliedResult } from '../shared/types'; 7 + import type { ZodFinal } from '../shared/types'; 8 8 import type { ZodPlugin } from '../types'; 9 9 import { createVisitor } from './walker'; 10 10 ··· 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 39 return 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 40 const visitor = createVisitor({ 47 41 schemaExtractor: extractor, 48 - state, 49 42 }); 50 43 const walk = createSchemaWalker(visitor); 51 44 ··· 53 46 path: ref(ctx.path), 54 47 plugin, 55 48 }); 56 - const ast = 57 - (visitor.applyModifiers(result, { 58 - path: ref(ctx.path), 59 - plugin, 60 - }) as ZodAppliedResult) ?? result.expression; 61 - if (result.hasLazyExpression) { 62 - state.hasLazyExpression['~ref'] = true; 63 - } 49 + 50 + const final = visitor.applyModifiers(result, { 51 + path: ref(ctx.path), 52 + plugin, 53 + }) as ZodFinal; 64 54 65 - exportAst({ ...ctx, ast, plugin, state }); 55 + exportAst({ ...ctx, final, plugin }); 66 56 }); 67 57 } 68 58
+54 -57
packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext, deduplicateSchema } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 import { unknownToAst } from './unknown'; 13 10 14 - export function arrayToAst( 15 - options: IrSchemaToAstOptions & { 16 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 17 - schema: SchemaWithType<'array'>; 18 - }, 19 - ): Omit<Ast, 'typeName'> { 20 - const { applyModifiers, plugin, walk } = options; 21 - let { schema } = options; 11 + interface ArrayToAstOptions { 12 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 13 + plugin: ZodPlugin['Instance']; 14 + schema: SchemaWithType<'array'>; 15 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 16 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 17 + } 22 18 23 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 19 + type AstExpression = Chain; 20 + 21 + export function arrayToAst({ 22 + applyModifiers, 23 + plugin, 24 + schema, 25 + walk, 26 + walkerCtx, 27 + }: ArrayToAstOptions): CompositeHandlerResult { 28 + const childResults: Array<ZodResult> = []; 29 + let schemaCopy = schema; 24 30 25 31 const z = plugin.external('zod.z'); 26 32 const functionName = $(z).attr(identifiers.array); 27 33 28 - if (!schema.items) { 29 - result.expression = functionName.call( 34 + let arrayExpression: ReturnType<typeof $.call> | undefined; 35 + 36 + if (!schemaCopy.items) { 37 + arrayExpression = functionName.call( 30 38 unknownToAst({ 31 - ...options, 39 + plugin, 32 40 schema: { 33 41 type: 'unknown', 34 42 }, 35 - }).expression, 43 + }), 36 44 ); 37 45 } else { 38 - schema = deduplicateSchema({ schema }); 46 + schemaCopy = deduplicateSchema({ schema: schemaCopy }); 39 47 40 - // at least one item is guaranteed 41 - const itemExpressions = schema.items!.map((item, index) => { 42 - const itemResult = walk( 43 - item, 44 - childContext( 45 - { 46 - path: options.state.path, 47 - plugin: options.plugin, 48 - }, 49 - 'items', 50 - index, 51 - ), 52 - ); 53 - if (itemResult.hasLazyExpression) { 54 - result.hasLazyExpression = true; 55 - } 48 + const itemExpressions: Array<AstExpression> = []; 49 + 50 + schemaCopy.items!.forEach((item, index) => { 51 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 52 + childResults.push(itemResult); 56 53 57 54 const finalExpr = applyModifiers(itemResult, { optional: false }); 58 - return finalExpr.expression; 55 + itemExpressions.push(finalExpr.expression); 59 56 }); 60 57 61 58 if (itemExpressions.length === 1) { 62 - result.expression = functionName.call(...itemExpressions); 59 + arrayExpression = functionName.call(...itemExpressions); 63 60 } else { 64 - if (schema.logicalOperator === 'and') { 65 - const firstSchema = schema.items![0]!; 66 - // we want to add an intersection, but not every schema can use the same API. 67 - // if the first item contains another array or not an object, we cannot use 68 - // `.and()` as that does not exist on `.union()` and non-object schemas. 69 - let intersectionExpression: ReturnType<typeof $.call | typeof $.expr>; 61 + if (schemaCopy.logicalOperator === 'and') { 62 + const firstSchema = schemaCopy.items![0]!; 63 + let intersectionExpression: AstExpression; 70 64 if ( 71 65 firstSchema.logicalOperator === 'or' || 72 66 (firstSchema.type && firstSchema.type !== 'object') ··· 83 77 } 84 78 } 85 79 86 - result.expression = functionName.call(intersectionExpression); 80 + arrayExpression = functionName.call(intersectionExpression); 87 81 } else { 88 - result.expression = $(z) 82 + arrayExpression = $(z) 89 83 .attr(identifiers.array) 90 84 .call( 91 85 $(z) ··· 96 90 } 97 91 } 98 92 99 - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { 100 - result.expression = result.expression 93 + if (schemaCopy.minItems === schemaCopy.maxItems && schemaCopy.minItems !== undefined) { 94 + arrayExpression = arrayExpression 101 95 .attr(identifiers.length) 102 - .call($.fromValue(schema.minItems)); 96 + .call($.fromValue(schemaCopy.minItems)); 103 97 } else { 104 - if (schema.minItems !== undefined) { 105 - result.expression = result.expression 98 + if (schemaCopy.minItems !== undefined) { 99 + arrayExpression = arrayExpression 106 100 .attr(identifiers.min) 107 - .call($.fromValue(schema.minItems)); 101 + .call($.fromValue(schemaCopy.minItems)); 108 102 } 109 103 110 - if (schema.maxItems !== undefined) { 111 - result.expression = result.expression 104 + if (schemaCopy.maxItems !== undefined) { 105 + arrayExpression = arrayExpression 112 106 .attr(identifiers.max) 113 - .call($.fromValue(schema.maxItems)); 107 + .call($.fromValue(schemaCopy.maxItems)); 114 108 } 115 109 } 116 110 117 - return result as Omit<Ast, 'typeName'>; 111 + return { 112 + childResults, 113 + expression: arrayExpression, 114 + }; 118 115 }
+7 -12
packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function booleanToAst({ 8 9 plugin, 9 10 schema, 10 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 11 + }: { 12 + plugin: ZodPlugin['Instance']; 11 13 schema: SchemaWithType<'boolean'>; 12 - }): Omit<Ast, 'typeName'> { 13 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 14 - let chain: ReturnType<typeof $.call>; 15 - 14 + }): Chain { 16 15 const z = plugin.external('zod.z'); 17 16 18 17 if (typeof schema.const === 'boolean') { 19 - chain = $(z).attr(identifiers.literal).call($.literal(schema.const)); 20 - result.expression = chain; 21 - return result as Omit<Ast, 'typeName'>; 18 + return $(z).attr(identifiers.literal).call($.literal(schema.const)); 22 19 } 23 20 24 - chain = $(z).attr(identifiers.boolean).call(); 25 - result.expression = chain; 26 - return result as Omit<Ast, 'typeName'>; 21 + return $(z).attr(identifiers.boolean).call(); 27 22 }
+23 -37
packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts
··· 4 4 import { identifiers } from '../../constants'; 5 5 import type { EnumResolverContext } from '../../resolvers'; 6 6 import type { Chain } from '../../shared/chain'; 7 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 7 + import type { ZodPlugin } from '../../types'; 8 8 import { unknownToAst } from './unknown'; 9 9 10 10 function itemsNode(ctx: EnumResolverContext): ReturnType<EnumResolverContext['nodes']['items']> { ··· 55 55 return $(z) 56 56 .attr(identifiers.enum) 57 57 .call($.array(...enumMembers)); 58 - } else if (literalMembers.length === 1) { 58 + } 59 + 60 + if (literalMembers.length === 1) { 59 61 return literalMembers[0]!; 60 - } else { 61 - return $(z) 62 - .attr(identifiers.union) 63 - .call($.array(...literalMembers)); 64 62 } 63 + 64 + return $(z) 65 + .attr(identifiers.union) 66 + .call($.array(...literalMembers)); 65 67 } 66 68 67 69 function enumResolver(ctx: EnumResolverContext): Chain { ··· 80 82 export function enumToAst({ 81 83 plugin, 82 84 schema, 83 - state, 84 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 85 + }: { 86 + plugin: ZodPlugin['Instance']; 85 87 schema: SchemaWithType<'enum'>; 86 - }): Omit<Ast, 'typeName'> { 88 + }): Chain { 87 89 const z = plugin.external('zod.z'); 88 90 89 - const { literalMembers } = itemsNode({ 90 - $, 91 - chain: { current: $(z) }, 92 - nodes: { base: baseNode, items: itemsNode }, 93 - plugin, 94 - schema, 95 - symbols: { z }, 96 - utils: { ast: {}, state }, 97 - }); 98 - 99 - if (!literalMembers.length) { 100 - return unknownToAst({ 101 - plugin, 102 - schema: { 103 - type: 'unknown', 104 - }, 105 - }); 106 - } 107 - 108 91 const ctx: EnumResolverContext = { 109 92 $, 110 93 chain: { ··· 119 102 symbols: { 120 103 z, 121 104 }, 122 - utils: { 123 - ast: {}, 124 - state, 125 - }, 126 105 }; 127 106 128 - const resolver = plugin.config['~resolvers']?.enum; 129 - const node = resolver?.(ctx) ?? enumResolver(ctx); 107 + const { literalMembers } = itemsNode(ctx); 130 108 131 - return { 132 - expression: node, 133 - }; 109 + if (!literalMembers.length) { 110 + return unknownToAst({ 111 + plugin, 112 + schema: { 113 + type: 'unknown', 114 + }, 115 + }); 116 + } 117 + 118 + const resolver = plugin.config['~resolvers']?.enum; 119 + return resolver?.(ctx) ?? enumResolver(ctx); 134 120 }
+6 -6
packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function neverToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'never'>; 11 - }): Omit<Ast, 'typeName'> { 12 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 13 + }): Chain { 13 14 const z = plugin.external('zod.z'); 14 - result.expression = $(z).attr(identifiers.never).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.never).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function nullToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'null'>; 11 - }): Omit<Ast, 'typeName'> { 12 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 13 + }): Chain { 13 14 const z = plugin.external('zod.z'); 14 - result.expression = $(z).attr(identifiers.null).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.null).call(); 16 16 }
+9 -14
packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts
··· 5 5 import { $ } from '../../../../ts-dsl'; 6 6 import { identifiers } from '../../constants'; 7 7 import type { NumberResolverContext } from '../../resolvers'; 8 - import type { Chain } from '../../shared/chain'; 9 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 8 + import type { Chain, ChainResult } from '../../shared/chain'; 9 + import type { ZodPlugin } from '../../types'; 10 10 11 11 function baseNode(ctx: NumberResolverContext): Chain { 12 12 const { schema, symbols } = ctx; ··· 21 21 return chain; 22 22 } 23 23 24 - function constNode(ctx: NumberResolverContext): Chain | undefined { 24 + function constNode(ctx: NumberResolverContext): ChainResult { 25 25 const { schema, symbols } = ctx; 26 26 const { z } = symbols; 27 27 if (schema.const === undefined) return; 28 28 return $(z).attr(identifiers.literal).call(ctx.utils.maybeBigInt(schema.const, schema.format)); 29 29 } 30 30 31 - function maxNode(ctx: NumberResolverContext): Chain | undefined { 31 + function maxNode(ctx: NumberResolverContext): ChainResult { 32 32 const { chain, schema } = ctx; 33 33 if (schema.exclusiveMaximum !== undefined) { 34 34 return chain.current ··· 52 52 return; 53 53 } 54 54 55 - function minNode(ctx: NumberResolverContext): Chain | undefined { 55 + function minNode(ctx: NumberResolverContext): ChainResult { 56 56 const { chain, schema } = ctx; 57 57 if (schema.exclusiveMinimum !== undefined) { 58 58 return chain.current ··· 98 98 export function numberToNode({ 99 99 plugin, 100 100 schema, 101 - state, 102 - }: Pick<IrSchemaToAstOptions, 'plugin' | 'state'> & { 101 + }: { 102 + plugin: ZodPlugin['Instance']; 103 103 schema: SchemaWithType<'integer' | 'number'>; 104 - }): Omit<Ast, 'typeName'> { 105 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 104 + }): Chain { 106 105 const z = plugin.external('zod.z'); 107 106 const ctx: NumberResolverContext = { 108 107 $, ··· 121 120 z, 122 121 }, 123 122 utils: { 124 - ast, 125 123 getIntegerLimit, 126 124 maybeBigInt, 127 125 shouldCoerceToBigInt, 128 - state, 129 126 }, 130 127 }; 131 128 const resolver = plugin.config['~resolvers']?.number; 132 - const node = resolver?.(ctx) ?? numberResolver(ctx); 133 - ast.expression = node; 134 - return ast as Omit<Ast, 'typeName'>; 129 + return resolver?.(ctx) ?? numberResolver(ctx); 135 130 }
+30 -36
packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts
··· 5 5 import { identifiers } from '../../constants'; 6 6 import type { ObjectResolverContext } from '../../resolvers'; 7 7 import type { Chain } from '../../shared/chain'; 8 - import type { 9 - Ast, 10 - IrSchemaToAstOptions, 11 - ZodAppliedResult, 12 - ZodSchemaResult, 13 - } from '../../shared/types'; 8 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 14 9 import type { ZodPlugin } from '../../types'; 15 10 16 11 type WalkerCtx = SchemaVisitorContext<ZodPlugin['Instance']>; 17 12 18 - interface ObjectToAstOptions extends IrSchemaToAstOptions { 19 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 13 + interface ObjectToAstOptions { 14 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 15 + plugin: ZodPlugin['Instance']; 20 16 schema: SchemaWithType<'object'>; 21 - walk: Walker<ZodSchemaResult, ZodPlugin['Instance']>; 17 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 22 18 walkerCtx: WalkerCtx; 23 19 } 24 20 ··· 29 25 }; 30 26 31 27 function additionalPropertiesNode(ctx: ExtendedContext): Chain | null | undefined { 32 - const { applyModifiers, schema, walk, walkerCtx } = ctx; 28 + const { _childResults, applyModifiers, schema, walk, walkerCtx } = ctx; 33 29 34 30 if ( 35 31 !schema.additionalProperties || ··· 42 38 schema.additionalProperties, 43 39 childContext(walkerCtx, 'additionalProperties'), 44 40 ); 45 - if (additionalResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 46 - const ast = applyModifiers(additionalResult, {}); 47 - return ast.expression; 41 + _childResults.push(additionalResult); 42 + const finalExpr = applyModifiers(additionalResult, {}); 43 + return finalExpr.expression; 48 44 } 49 45 50 46 function baseNode(ctx: ExtendedContext): Chain { ··· 62 58 } 63 59 64 60 function objectResolver(ctx: ExtendedContext): Chain { 65 - // TODO: parser - handle constants 66 61 return ctx.nodes.base(ctx); 67 62 } 68 63 69 64 function shapeNode(ctx: ExtendedContext): ReturnType<typeof $.object> { 70 - const { applyModifiers, schema, walk, walkerCtx } = ctx; 65 + const { _childResults, applyModifiers, schema, walk, walkerCtx } = ctx; 71 66 const shape = $.object().pretty(); 72 67 73 68 for (const name in schema.properties) { ··· 75 70 const isOptional = !schema.required?.includes(name); 76 71 77 72 const propertyResult = walk(property, childContext(walkerCtx, 'properties', name)); 78 - if (propertyResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; 73 + _childResults.push(propertyResult); 79 74 80 - const ast = applyModifiers(propertyResult, { 75 + const finalExpr = applyModifiers(propertyResult, { 81 76 optional: isOptional, 82 77 }); 83 78 84 - if (ast.hasLazyExpression) { 85 - ctx.utils.ast.hasLazyExpression = true; 86 - shape.getter(name, ast.expression.return()); 87 - } else { 88 - shape.prop(name, ast.expression); 89 - } 79 + shape.prop(name, finalExpr.expression); 90 80 } 91 81 92 82 return shape; 93 83 } 94 84 95 - export function objectToAst(options: ObjectToAstOptions): Omit<Ast, 'typeName'> { 96 - const { plugin } = options; 97 - const ast: Partial<Omit<Ast, 'typeName'>> = {}; 98 - const z = plugin.external('zod.z'); 85 + export function objectToAst(options: ObjectToAstOptions): CompositeHandlerResult { 86 + const { applyModifiers, plugin, schema, walk, walkerCtx } = options; 87 + 88 + const childResults: Array<ZodResult> = []; 89 + 99 90 const ctx: ExtendedContext = { 100 - ...options, 101 91 $, 92 + _childResults: childResults, 93 + applyModifiers, 102 94 chain: { 103 - current: $(z), 95 + current: $(plugin.external('zod.z')), 104 96 }, 105 97 nodes: { 106 98 additionalProperties: additionalPropertiesNode, ··· 108 100 shape: shapeNode, 109 101 }, 110 102 plugin, 103 + schema, 111 104 symbols: { 112 - z, 113 - }, 114 - utils: { 115 - ast, 116 - state: options.state, 105 + z: plugin.external('zod.z'), 117 106 }, 107 + walk, 108 + walkerCtx, 118 109 }; 119 110 const resolver = plugin.config['~resolvers']?.object; 120 111 const node = resolver?.(ctx) ?? objectResolver(ctx); 121 - ast.expression = node; 122 - return ast as Omit<Ast, 'typeName'>; 112 + 113 + return { 114 + childResults, 115 + expression: node, 116 + }; 123 117 }
+12 -14
packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts
··· 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 5 import type { StringResolverContext } from '../../resolvers'; 6 - import type { Chain } from '../../shared/chain'; 7 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 6 + import type { Chain, ChainResult } from '../../shared/chain'; 7 + import type { ZodPlugin } from '../../types'; 8 8 9 9 function baseNode(ctx: StringResolverContext): Chain { 10 10 const { z } = ctx.symbols; 11 11 return $(z).attr(identifiers.string).call(); 12 12 } 13 13 14 - function constNode(ctx: StringResolverContext): Chain | undefined { 14 + function constNode(ctx: StringResolverContext): ChainResult { 15 15 const { schema, symbols } = ctx; 16 16 const { z } = symbols; 17 17 if (typeof schema.const !== 'string') return; 18 18 return $(z).attr(identifiers.literal).call($.literal(schema.const)); 19 19 } 20 20 21 - function formatNode(ctx: StringResolverContext): Chain | undefined { 21 + function formatNode(ctx: StringResolverContext): ChainResult { 22 22 const { plugin, schema, symbols } = ctx; 23 23 const { z } = symbols; 24 24 ··· 51 51 return; 52 52 } 53 53 54 - function lengthNode(ctx: StringResolverContext): Chain | undefined { 54 + function lengthNode(ctx: StringResolverContext): ChainResult { 55 55 const { chain, schema } = ctx; 56 56 if (schema.minLength === undefined || schema.minLength !== schema.maxLength) return; 57 57 return chain.current.attr(identifiers.length).call($.literal(schema.minLength)); 58 58 } 59 59 60 - function maxLengthNode(ctx: StringResolverContext): Chain | undefined { 60 + function maxLengthNode(ctx: StringResolverContext): ChainResult { 61 61 const { chain, schema } = ctx; 62 62 if (schema.maxLength === undefined) return; 63 63 return chain.current.attr(identifiers.max).call($.literal(schema.maxLength)); 64 64 } 65 65 66 - function minLengthNode(ctx: StringResolverContext): Chain | undefined { 66 + function minLengthNode(ctx: StringResolverContext): ChainResult { 67 67 const { chain, schema } = ctx; 68 68 if (schema.minLength === undefined) return; 69 69 return chain.current.attr(identifiers.min).call($.literal(schema.minLength)); 70 70 } 71 71 72 - function patternNode(ctx: StringResolverContext): Chain | undefined { 72 + function patternNode(ctx: StringResolverContext): ChainResult { 73 73 const { chain, schema } = ctx; 74 74 if (!schema.pattern) return; 75 75 const flags = /\\[pP]\{/.test(schema.pattern) ? 'u' : undefined; ··· 109 109 export function stringToNode({ 110 110 plugin, 111 111 schema, 112 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 112 + }: { 113 + plugin: ZodPlugin['Instance']; 113 114 schema: SchemaWithType<'string'>; 114 - }): Omit<Ast, 'typeName'> { 115 + }): Chain { 115 116 const z = plugin.external('zod.z'); 116 117 const ctx: StringResolverContext = { 117 118 $, ··· 134 135 }, 135 136 }; 136 137 const resolver = plugin.config['~resolvers']?.string; 137 - const node = resolver?.(ctx) ?? stringResolver(ctx); 138 - return { 139 - expression: node, 140 - }; 138 + return resolver?.(ctx) ?? stringResolver(ctx); 141 139 }
+34 -39
packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 2 import { childContext } from '@hey-api/shared'; 3 3 4 4 import { $ } from '../../../../ts-dsl'; 5 5 import { identifiers } from '../../constants'; 6 - import type { 7 - Ast, 8 - IrSchemaToAstOptions, 9 - ZodAppliedResult, 10 - ZodSchemaResult, 11 - } from '../../shared/types'; 6 + import type { Chain } from '../../shared/chain'; 7 + import type { CompositeHandlerResult, ZodFinal, ZodResult } from '../../shared/types'; 8 + import type { ZodPlugin } from '../../types'; 12 9 13 - export function tupleToAst( 14 - options: IrSchemaToAstOptions & { 15 - applyModifiers: (result: ZodSchemaResult, opts: { optional?: boolean }) => ZodAppliedResult; 16 - schema: SchemaWithType<'tuple'>; 17 - }, 18 - ): Omit<Ast, 'typeName'> { 19 - const { applyModifiers, plugin, schema, walk } = options; 10 + interface TupleToAstOptions { 11 + applyModifiers: (result: ZodResult, opts: { optional?: boolean }) => ZodFinal; 12 + plugin: ZodPlugin['Instance']; 13 + schema: SchemaWithType<'tuple'>; 14 + walk: Walker<ZodResult, ZodPlugin['Instance']>; 15 + walkerCtx: SchemaVisitorContext<ZodPlugin['Instance']>; 16 + } 20 17 21 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 18 + export function tupleToAst({ 19 + applyModifiers, 20 + plugin, 21 + schema, 22 + walk, 23 + walkerCtx, 24 + }: TupleToAstOptions): CompositeHandlerResult { 25 + const childResults: Array<ZodResult> = []; 22 26 23 27 const z = plugin.external('zod.z'); 24 28 ··· 26 30 const tupleElements = schema.const.map((value) => 27 31 $(z).attr(identifiers.literal).call($.fromValue(value)), 28 32 ); 29 - result.expression = $(z) 30 - .attr(identifiers.tuple) 31 - .call($.array(...tupleElements)); 32 - return result as Omit<Ast, 'typeName'>; 33 + return { 34 + childResults, 35 + expression: $(z) 36 + .attr(identifiers.tuple) 37 + .call($.array(...tupleElements)), 38 + }; 33 39 } 34 40 35 - const tupleElements: Array<ReturnType<typeof $.call | typeof $.expr>> = []; 41 + const tupleElements: Array<Chain> = []; 36 42 37 43 if (schema.items) { 38 44 schema.items.forEach((item, index) => { 39 - const itemResult = walk( 40 - item, 41 - childContext( 42 - { 43 - path: options.state.path, 44 - plugin: options.plugin, 45 - }, 46 - 'items', 47 - index, 48 - ), 49 - ); 50 - if (itemResult.hasLazyExpression) { 51 - result.hasLazyExpression = true; 52 - } 45 + const itemResult = walk(item, childContext(walkerCtx, 'items', index)); 46 + childResults.push(itemResult); 53 47 54 48 const finalExpr = applyModifiers(itemResult, { optional: false }); 55 49 tupleElements.push(finalExpr.expression); 56 50 }); 57 51 } 58 52 59 - result.expression = $(z) 60 - .attr(identifiers.tuple) 61 - .call($.array(...tupleElements)); 62 - 63 - return result as Omit<Ast, 'typeName'>; 53 + return { 54 + childResults, 55 + expression: $(z) 56 + .attr(identifiers.tuple) 57 + .call($.array(...tupleElements)), 58 + }; 64 59 }
+6 -6
packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function undefinedToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'undefined'>; 11 - }): Omit<Ast, 'typeName'> { 12 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 13 + }): Chain { 13 14 const z = plugin.external('zod.z'); 14 - result.expression = $(z).attr(identifiers.undefined).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.undefined).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function unknownToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'unknown'>; 11 - }): Omit<Ast, 'typeName'> { 12 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 13 + }): Chain { 13 14 const z = plugin.external('zod.z'); 14 - result.expression = $(z).attr(identifiers.unknown).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.unknown).call(); 16 16 }
+6 -6
packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts
··· 2 2 3 3 import { $ } from '../../../../ts-dsl'; 4 4 import { identifiers } from '../../constants'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 5 + import type { Chain } from '../../shared/chain'; 6 + import type { ZodPlugin } from '../../types'; 6 7 7 8 export function voidToAst({ 8 9 plugin, 9 - }: Pick<IrSchemaToAstOptions, 'plugin'> & { 10 + }: { 11 + plugin: ZodPlugin['Instance']; 10 12 schema: SchemaWithType<'void'>; 11 - }): Omit<Ast, 'typeName'> { 12 - const result: Partial<Omit<Ast, 'typeName'>> = {}; 13 + }): Chain { 13 14 const z = plugin.external('zod.z'); 14 - result.expression = $(z).attr(identifiers.void).call(); 15 - return result as Omit<Ast, 'typeName'>; 15 + return $(z).attr(identifiers.void).call(); 16 16 }
+166 -169
packages/openapi-ts/src/plugins/zod/v4/walker.ts
··· 1 - import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; 1 + import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef } from '@hey-api/codegen-core'; 3 3 import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 4 4 import { pathToJsonPointer } from '@hey-api/shared'; ··· 6 6 import { $ } from '../../../ts-dsl'; 7 7 import { maybeBigInt, shouldCoerceToBigInt } from '../../shared/utils/coerce'; 8 8 import { identifiers } from '../constants'; 9 + import type { Chain } from '../shared/chain'; 10 + import { defaultMeta, inheritMeta } from '../shared/meta'; 9 11 import type { ProcessorContext } from '../shared/processor'; 10 - import type { Ast, PluginState, ZodAppliedResult, ZodSchemaResult } from '../shared/types'; 12 + import type { ZodFinal, ZodResult } from '../shared/types'; 11 13 import type { ZodPlugin } from '../types'; 12 14 import { arrayToAst } from './toAst/array'; 13 15 import { booleanToAst } from './toAst/boolean'; ··· 25 27 export interface VisitorConfig { 26 28 /** Optional schema extractor function. */ 27 29 schemaExtractor?: SchemaExtractor<ProcessorContext>; 28 - /** The plugin state references. */ 29 - state: Refs<PluginState>; 30 30 } 31 31 32 32 export function createVisitor( 33 33 config: VisitorConfig, 34 - ): SchemaVisitor<ZodSchemaResult, ZodPlugin['Instance']> { 35 - const { schemaExtractor, state } = config; 34 + ): SchemaVisitor<ZodResult, ZodPlugin['Instance']> { 35 + const { schemaExtractor } = config; 36 + 36 37 return { 37 - applyModifiers(result, ctx, options = {}): ZodAppliedResult { 38 + applyModifiers(result, ctx, options = {}): ZodFinal { 38 39 const { optional } = options; 39 - let expression = result.expression.expression; 40 + let expression = result.expression; 40 41 41 - if (result.readonly) { 42 + if (result.meta.readonly) { 42 43 expression = expression.attr(identifiers.readonly).call(); 43 44 } 44 45 45 - const hasDefault = result.default !== undefined; 46 - const needsNullable = result.nullable; 46 + const hasDefault = result.meta.default !== undefined; 47 + const needsNullable = result.meta.nullable; 47 48 48 49 if (optional && needsNullable) { 49 50 expression = expression.attr(identifiers.nullish).call(); ··· 57 58 expression = expression 58 59 .attr(identifiers.default) 59 60 .call( 60 - result.format 61 - ? maybeBigInt(result.default, result.format) 62 - : $.fromValue(result.default), 61 + result.meta.format 62 + ? maybeBigInt(result.meta.default, result.meta.format) 63 + : $.fromValue(result.meta.default), 63 64 ); 64 65 } 65 66 66 - return { expression }; 67 + return { 68 + expression, 69 + }; 67 70 }, 68 71 array(schema, ctx, walk) { 69 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 70 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 71 - const ast = arrayToAst({ 72 - ...ctx, 72 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 73 + this.applyModifiers(result, ctx, opts) as ZodFinal; 74 + const { childResults, expression } = arrayToAst({ 73 75 applyModifiers, 76 + plugin: ctx.plugin, 74 77 schema, 75 - state, 76 78 walk, 79 + walkerCtx: ctx, 77 80 }); 81 + 78 82 return { 79 - default: schema.default, 80 - expression: ast, 81 - hasLazyExpression: state.hasLazyExpression['~ref'], 82 - nullable: false, 83 - readonly: schema.accessScope === 'read', 83 + expression, 84 + meta: inheritMeta(schema, childResults), 84 85 }; 85 86 }, 86 87 boolean(schema, ctx) { 87 - const ast = booleanToAst({ ...ctx, schema }); 88 + const expression = booleanToAst({ plugin: ctx.plugin, schema }); 88 89 return { 89 - default: schema.default, 90 - expression: ast, 91 - nullable: false, 92 - readonly: schema.accessScope === 'read', 90 + expression, 91 + meta: defaultMeta(schema), 93 92 }; 94 93 }, 95 94 enum(schema, ctx) { 96 - const ast = enumToAst({ ...ctx, schema, state }); 95 + const expression = enumToAst({ plugin: ctx.plugin, schema }); 97 96 const hasNull = 98 97 schema.items?.some((item) => item.type === 'null' || item.const === null) ?? false; 99 98 return { 100 - default: schema.default, 101 - expression: ast, 102 - nullable: hasNull, 103 - readonly: schema.accessScope === 'read', 99 + expression, 100 + meta: { 101 + ...defaultMeta(schema), 102 + nullable: hasNull, 103 + }, 104 104 }; 105 105 }, 106 106 integer(schema, ctx) { 107 - const ast = numberToNode({ ...ctx, schema, state }); 107 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 108 108 return { 109 - default: schema.default, 110 - expression: ast, 111 - format: schema.format, 112 - nullable: false, 113 - readonly: schema.accessScope === 'read', 109 + expression, 110 + meta: { 111 + ...defaultMeta(schema), 112 + format: schema.format, 113 + }, 114 114 }; 115 115 }, 116 116 intercept(schema, ctx, walk) { ··· 132 132 }, 133 133 intersection(items, schemas, parentSchema, ctx) { 134 134 const z = ctx.plugin.external('zod.z'); 135 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 135 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 136 136 137 137 const firstSchema = schemas[0]; 138 - let expression: ZodSchemaResult['expression']; 138 + let expression: Chain; 139 139 140 - // If first item is a union or non-object, use z.intersection() 141 - // Otherwise use .and() chaining for better type inference 142 140 if ( 143 141 firstSchema?.logicalOperator === 'or' || 144 142 (firstSchema?.type && firstSchema.type !== 'object') 145 143 ) { 146 - expression = { 147 - expression: $(z) 148 - .attr(identifiers.intersection) 149 - .call(...items.map((item) => item.expression.expression)), 150 - }; 144 + expression = $(z) 145 + .attr(identifiers.intersection) 146 + .call(...items.map((item) => item.expression)); 151 147 } else { 152 148 expression = items[0]!.expression; 153 149 items.slice(1).forEach((item) => { 154 - expression = { 155 - expression: expression.expression 156 - .attr(identifiers.and) 157 - .call( 158 - item.hasLazyExpression 159 - ? $(z) 160 - .attr(identifiers.lazy) 161 - .call($.func().do(item.expression.expression.return())) 162 - : item.expression.expression, 163 - ), 164 - }; 150 + expression = expression 151 + .attr(identifiers.and) 152 + .call( 153 + item.meta.hasLazy 154 + ? $(z).attr(identifiers.lazy).call($.func().do(item.expression.return())) 155 + : item.expression, 156 + ); 165 157 }); 166 158 } 167 159 168 160 return { 169 - default: parentSchema.default, 170 161 expression, 171 - hasLazyExpression: hasAnyLazy, 172 - nullable: items.some((i) => i.nullable), 173 - readonly: items.some((i) => i.readonly), 162 + meta: { 163 + default: parentSchema.default, 164 + format: parentSchema.format, 165 + hasLazy: hasAnyLazy, 166 + isLazy: false, 167 + nullable: items.some((i) => i.meta.nullable), 168 + readonly: items.some((i) => i.meta.readonly), 169 + }, 174 170 }; 175 171 }, 176 172 never(schema, ctx) { 177 - const ast = neverToAst({ ...ctx, schema }); 173 + const expression = neverToAst({ plugin: ctx.plugin, schema }); 178 174 return { 179 - default: schema.default, 180 - expression: ast, 181 - nullable: false, 182 - readonly: false, 175 + expression, 176 + meta: { 177 + ...defaultMeta(schema), 178 + nullable: false, 179 + readonly: false, 180 + }, 183 181 }; 184 182 }, 185 183 null(schema, ctx) { 186 - const ast = nullToAst({ ...ctx, schema }); 184 + const expression = nullToAst({ plugin: ctx.plugin, schema }); 187 185 return { 188 - default: schema.default, 189 - expression: ast, 190 - nullable: false, 191 - readonly: false, 186 + expression, 187 + meta: { 188 + ...defaultMeta(schema), 189 + nullable: false, 190 + readonly: false, 191 + }, 192 192 }; 193 193 }, 194 194 number(schema, ctx) { 195 - const ast = numberToNode({ ...ctx, schema, state }); 195 + const expression = numberToNode({ plugin: ctx.plugin, schema }); 196 196 return { 197 - default: schema.default, 198 - expression: ast, 199 - format: schema.format, 200 - nullable: false, 201 - readonly: schema.accessScope === 'read', 197 + expression, 198 + meta: { 199 + ...defaultMeta(schema), 200 + format: schema.format, 201 + }, 202 202 }; 203 203 }, 204 204 object(schema, ctx, walk) { 205 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 206 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 207 - const ast = objectToAst({ 205 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 206 + this.applyModifiers(result, ctx, opts) as ZodFinal; 207 + const { childResults, expression } = objectToAst({ 208 208 applyModifiers, 209 209 plugin: ctx.plugin, 210 210 schema, 211 - state, 212 211 walk, 213 212 walkerCtx: ctx, 214 213 }); 214 + 215 215 return { 216 - default: schema.default, 217 - expression: ast, 218 - hasLazyExpression: state.hasLazyExpression['~ref'], 219 - nullable: false, 220 - readonly: schema.accessScope === 'read', 216 + expression, 217 + meta: inheritMeta(schema, childResults), 221 218 }; 222 219 }, 223 220 postProcess(result, schema, ctx) { 224 - const metadata = ctx.plugin.config.metadata; 221 + const { metadata } = ctx.plugin.config; 222 + 225 223 if (!metadata) { 226 224 return result; 227 225 } 226 + 228 227 const node = $.object(); 229 - if (typeof metadata === 'function') { 230 - metadata({ $, node, schema }); 231 - } else if (schema.description) { 228 + 229 + if (metadata === true) { 230 + if (!schema.description) { 231 + return result; 232 + } 232 233 node.pretty().prop('description', $.literal(schema.description)); 234 + } else { 235 + metadata({ $, node, schema }); 233 236 } 237 + 234 238 if (node.isEmpty) { 235 239 return result; 236 240 } 241 + 237 242 const z = ctx.plugin.external('zod.z'); 243 + 238 244 return { 239 245 ...result, 240 - expression: { 241 - expression: result.expression.expression 242 - .attr(identifiers.register) 243 - .call($(z).attr(identifiers.globalRegistry), node), 244 - }, 246 + expression: result.expression 247 + .attr(identifiers.register) 248 + .call($(z).attr(identifiers.globalRegistry), node), 245 249 }; 246 250 }, 247 251 reference($ref, schema, ctx) { ··· 257 261 258 262 if (ctx.plugin.isSymbolRegistered(query)) { 259 263 return { 260 - default: schema.default, 261 - expression: { 262 - expression: $(refSymbol), 263 - }, 264 - nullable: false, 265 - readonly: schema.accessScope === 'read', 264 + expression: $(refSymbol), 265 + meta: defaultMeta(schema), 266 266 }; 267 267 } 268 268 269 - state.hasLazyExpression['~ref'] = true; 270 269 return { 271 - default: schema.default, 272 - expression: { 273 - expression: $(z) 274 - .attr(identifiers.lazy) 275 - .call($.func().returns('any').do($(refSymbol).return())), 270 + expression: $(z) 271 + .attr(identifiers.lazy) 272 + .call($.func().returns('any').do($(refSymbol).return())), 273 + meta: { 274 + ...defaultMeta(schema), 275 + hasLazy: true, 276 + isLazy: true, 276 277 }, 277 - hasLazyExpression: true, 278 - nullable: false, 279 - readonly: schema.accessScope === 'read', 280 278 }; 281 279 }, 282 280 string(schema, ctx) { 283 281 if (shouldCoerceToBigInt(schema.format)) { 284 - const ast = numberToNode({ 282 + const expression = numberToNode({ 285 283 plugin: ctx.plugin, 286 284 schema: { ...schema, type: 'number' }, 287 - state, 288 285 }); 289 286 return { 290 - default: schema.default, 291 - expression: ast, 292 - nullable: false, 293 - readonly: schema.accessScope === 'read', 287 + expression, 288 + meta: defaultMeta(schema), 294 289 }; 295 290 } 296 291 297 - const ast = stringToNode({ ...ctx, schema }); 292 + const expression = stringToNode({ plugin: ctx.plugin, schema }); 298 293 return { 299 - default: schema.default, 300 - expression: ast, 301 - nullable: false, 302 - readonly: schema.accessScope === 'read', 294 + expression, 295 + meta: defaultMeta(schema), 303 296 }; 304 297 }, 305 298 tuple(schema, ctx, walk) { 306 - const applyModifiers = (result: ZodSchemaResult, opts: { optional?: boolean }) => 307 - this.applyModifiers(result, ctx, opts) as ZodAppliedResult; 308 - const ast = tupleToAst({ 309 - ...ctx, 299 + const applyModifiers = (result: ZodResult, opts: { optional?: boolean }) => 300 + this.applyModifiers(result, ctx, opts) as ZodFinal; 301 + const { childResults, expression } = tupleToAst({ 310 302 applyModifiers, 303 + plugin: ctx.plugin, 311 304 schema, 312 - state, 313 305 walk, 306 + walkerCtx: ctx, 314 307 }); 308 + 315 309 return { 316 - default: schema.default, 317 - expression: ast, 318 - hasLazyExpression: state.hasLazyExpression['~ref'], 319 - nullable: false, 320 - readonly: schema.accessScope === 'read', 310 + expression, 311 + meta: inheritMeta(schema, childResults), 321 312 }; 322 313 }, 323 314 undefined(schema, ctx) { 324 - const ast = undefinedToAst({ ...ctx, schema }); 315 + const expression = undefinedToAst({ plugin: ctx.plugin, schema }); 325 316 return { 326 - default: schema.default, 327 - expression: ast, 328 - nullable: false, 329 - readonly: false, 317 + expression, 318 + meta: { 319 + ...defaultMeta(schema), 320 + nullable: false, 321 + readonly: false, 322 + }, 330 323 }; 331 324 }, 332 325 union(items, schemas, parentSchema, ctx) { 333 326 const z = ctx.plugin.external('zod.z'); 334 - const hasAnyLazy = items.some((item) => item.hasLazyExpression); 327 + const hasAnyLazy = items.some((item) => item.meta.hasLazy); 335 328 336 - const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.nullable); 329 + const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.meta.nullable); 337 330 338 331 const nonNullItems: typeof items = []; 339 332 ··· 344 337 } 345 338 }); 346 339 347 - let expression: Ast; 340 + let expression: Chain; 348 341 if (nonNullItems.length === 0) { 349 - expression = { 350 - expression: $(z).attr(identifiers.null).call(), 351 - }; 342 + expression = $(z).attr(identifiers.null).call(); 352 343 } else if (nonNullItems.length === 1) { 353 344 expression = nonNullItems[0]!.expression; 354 345 } else { 355 - expression = { 356 - expression: $(z) 357 - .attr(identifiers.union) 358 - .call( 359 - $.array() 360 - .pretty() 361 - .elements(...nonNullItems.map((item) => item.expression.expression)), 362 - ), 363 - }; 346 + expression = $(z) 347 + .attr(identifiers.union) 348 + .call( 349 + $.array() 350 + .pretty() 351 + .elements(...nonNullItems.map((item) => item.expression)), 352 + ); 364 353 } 365 354 366 355 return { 367 - default: parentSchema.default, 368 356 expression, 369 - hasLazyExpression: hasAnyLazy, 370 - nullable: hasNull, 371 - readonly: items.some((i) => i.readonly), 357 + meta: { 358 + default: parentSchema.default, 359 + format: parentSchema.format, 360 + hasLazy: hasAnyLazy, 361 + isLazy: false, 362 + nullable: hasNull, 363 + readonly: items.some((i) => i.meta.readonly), 364 + }, 372 365 }; 373 366 }, 374 367 unknown(schema, ctx) { 375 - const ast = unknownToAst({ ...ctx, schema }); 368 + const expression = unknownToAst({ plugin: ctx.plugin, schema }); 376 369 return { 377 - default: schema.default, 378 - expression: ast, 379 - nullable: false, 380 - readonly: false, 370 + expression, 371 + meta: { 372 + ...defaultMeta(schema), 373 + nullable: false, 374 + readonly: false, 375 + }, 381 376 }; 382 377 }, 383 378 void(schema, ctx) { 384 - const ast = voidToAst({ ...ctx, schema }); 379 + const expression = voidToAst({ plugin: ctx.plugin, schema }); 385 380 return { 386 - default: schema.default, 387 - expression: ast, 388 - nullable: false, 389 - readonly: false, 381 + expression, 382 + meta: { 383 + ...defaultMeta(schema), 384 + nullable: false, 385 + readonly: false, 386 + }, 390 387 }; 391 388 }, 392 389 };