diff --git a/.changeset/early-frogs-hear.md b/.changeset/early-frogs-hear.md new file mode 100644 index 0000000000..677811a1eb --- /dev/null +++ b/.changeset/early-frogs-hear.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**valibot**: allow generating custom pipes with `~resolvers` diff --git a/.changeset/eight-eagles-shop.md b/.changeset/eight-eagles-shop.md new file mode 100644 index 0000000000..14df9579ef --- /dev/null +++ b/.changeset/eight-eagles-shop.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**zod**: allow generating custom chains with `~resolvers` diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index 18342cece9..49437a31d8 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -41,12 +41,12 @@ export default defineConfig(() => { // 'circular.yaml', // 'dutchie.json', // 'invalid', - 'full.yaml', + // 'full.yaml', // 'openai.yaml', // 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', - // 'transformers.json', + 'transformers.json', // 'type-format.yaml', // 'validators.yaml', // 'validators-circular-ref.json', @@ -269,8 +269,8 @@ export default defineConfig(() => { // signature: 'client', // signature: 'object', // transformer: '@hey-api/transformers', - // transformer: true, - // validator: true, + transformer: true, + validator: 'zod', // validator: { // request: 'zod', // response: 'zod', @@ -291,8 +291,8 @@ export default defineConfig(() => { }, { // bigInt: true, - // dates: true, - // name: '@hey-api/transformers', + dates: true, + name: '@hey-api/transformers', }, { // name: 'fastify', @@ -395,11 +395,19 @@ export default defineConfig(() => { // }, }, }, + '~resolvers': { + string: { + formats: { + // date: ({ $, pipes }) => pipes.push($('v').attr('isoDateTime').call()), + // 'date-time': ({ $, pipes }) => pipes.push($('v').attr('isoDateTime').call()), + }, + }, + }, }, { // case: 'snake_case', // comments: false, - compatibilityVersion: 4, + compatibilityVersion: 3, dates: { // local: true, // offset: true, @@ -447,6 +455,14 @@ export default defineConfig(() => { // }, }, }, + '~resolvers': { + string: { + formats: { + // date: ({ $ }) => $('z').attr('date').call(), + // 'date-time': ({ $ }) => $('z').attr('date').call(), + }, + }, + }, }, { exportFromIndex: true, diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts index 218d9f214a..cd348f8548 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts @@ -412,13 +412,13 @@ export const vModelSquare = v.object({ export const vCompositionWithOneOfDiscriminator = v.union([ v.intersect([ v.object({ - kind: v.literal('circle') + kind: v.literal("circle") }), vModelCircle ]), v.intersect([ v.object({ - kind: v.literal('square') + kind: v.literal("square") }), vModelSquare ]) diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts index f754741b42..03aef51409 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts @@ -415,13 +415,13 @@ export const vModelSquare = v.object({ export const vCompositionWithOneOfDiscriminator = v.union([ v.intersect([ v.object({ - kind: v.literal('circle') + kind: v.literal("circle") }), vModelCircle ]), v.intersect([ v.object({ - kind: v.literal('square') + kind: v.literal("square") }), vModelSquare ]) @@ -467,7 +467,7 @@ export const v3eNum1Период = v.picklist([ 'Dog' ]); -export const vConstValue = v.literal('ConstValue'); +export const vConstValue = v.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -721,10 +721,10 @@ export const vFreeFormObjectWithAdditionalPropertiesEqTrue = v.record(v.string() export const vFreeFormObjectWithAdditionalPropertiesEqEmptyObject = v.record(v.string(), v.unknown()); export const vModelWithConst = v.object({ - String: v.optional(v.literal('String')), + String: v.optional(v.literal("String")), number: v.optional(v.literal(0)), null: v.optional(v.null()), - withType: v.optional(v.literal('Some string')) + withType: v.optional(v.literal("Some string")) }); /** @@ -1660,9 +1660,9 @@ export const vTypesData = v.object({ v.null() ]), parameterEnum: v.union([ - v.literal('Success'), - v.literal('Warning'), - v.literal('Error'), + v.literal("Success"), + v.literal("Warning"), + v.literal("Error"), v.null() ]) }) diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/schema-const/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/schema-const/valibot.gen.ts index 9be77d3e92..0bf447745a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/schema-const/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/schema-const/valibot.gen.ts @@ -3,7 +3,7 @@ import * as v from 'valibot'; export const vFoo = v.object({ - foo: v.optional(v.literal('foo')), + foo: v.optional(v.literal("foo")), bar: v.optional(v.literal(3.2)), baz: v.optional(v.literal(-1)), qux: v.optional(v.literal(true)), diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts index 9b3bf45710..43633f8e7e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -33,14 +33,14 @@ export const zBaz = z.object({ export const zQux = z.union([ z.intersection(z.object({ - type: z.literal('struct') + type: z.literal("struct") }), z.lazy(() => { return z.lazy((): any => { return zCorge; }); })), z.intersection(z.object({ - type: z.literal('array') + type: z.literal("array") }), zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts index 2195593a03..f4abf0f62c 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -452,10 +452,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.intersection(z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }), zModelCircle), z.intersection(z.object({ - kind: z.literal('square') + kind: z.literal("square") }), zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts index bcce4d295c..b498a519e8 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -25,12 +25,12 @@ export const zBaz: z.AnyZodObject = z.object({ export const zQux: z.ZodTypeAny = z.union([ z.object({ - type: z.literal('struct') + type: z.literal("struct") }).and(z.lazy(() => { return zCorge; })), z.object({ - type: z.literal('array') + type: z.literal("array") }).and(zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts index 5d788ebe50..2530b821af 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -450,10 +450,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 073c23da87..908c37eb4a 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -33,14 +33,14 @@ export const zBaz = z.object({ export const zQux = z.union([ z.object({ - type: z.literal('struct') + type: z.literal("struct") }).and(z.lazy(() => { return z.lazy((): any => { return zCorge; }); })), z.object({ - type: z.literal('array') + type: z.literal("array") }).and(zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts index 3d5fb668b5..9266d3be32 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -452,10 +452,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts index 2a97dcfe22..2597f4579e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -455,10 +455,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.intersection(z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }), zModelCircle), z.intersection(z.object({ - kind: z.literal('square') + kind: z.literal("square") }), zModelSquare) ]); @@ -508,7 +508,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -753,10 +753,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.string() export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.string(), z.unknown()); export const zModelWithConst = z.object({ - String: z.optional(z.literal('String')), + String: z.optional(z.literal("String")), number: z.optional(z.literal(0)), null: z.optional(z.null()), - withType: z.optional(z.literal('Some string')) + withType: z.optional(z.literal("Some string")) }); /** @@ -1688,9 +1688,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts index 10480c6a67..a314590d95 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/v4-mini'; export const zFoo = z.object({ - foo: z.optional(z.literal('foo')), + foo: z.optional(z.literal("foo")), bar: z.optional(z.literal(3.2)), baz: z.optional(z.literal(-1)), qux: z.optional(z.literal(true)), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.optional(z.literal(65535)), integerUint32: z.optional(z.literal(4294967295)), integerUint64: z.optional(z.int()), - stringInt64: z.optional(z.literal('-9223372036854775808')), - stringUint64: z.optional(z.literal('18446744073709551615')) + stringInt64: z.optional(z.literal("-9223372036854775808")), + stringUint64: z.optional(z.literal("18446744073709551615")) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index b515009b63..7c15d934a5 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -57,9 +57,7 @@ export const zPatchFooData = z.object({ baz: z.optional(z.string()) })), qux: z.optional(z.iso.date()), - quux: z.optional(z.iso.datetime({ - offset: true - })) + quux: z.optional(z.iso.datetime({ offset: true })) })) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts index e56c1943f4..5cf5419482 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -453,10 +453,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); @@ -506,7 +506,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -751,10 +751,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.unknown( export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.unknown()); export const zModelWithConst = z.object({ - String: z.literal('String').optional(), + String: z.literal("String").optional(), number: z.literal(0).optional(), null: z.null().optional(), - withType: z.literal('Some string').optional() + withType: z.literal("Some string").optional() }); /** @@ -1686,9 +1686,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts index e577fd348b..79075274d6 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const zFoo = z.object({ - foo: z.literal('foo').optional(), + foo: z.literal("foo").optional(), bar: z.literal(3.2).optional(), baz: z.literal(-1).optional(), qux: z.literal(true).optional(), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.literal(65535).optional(), integerUint32: z.literal(4294967295).optional(), integerUint64: z.number().int().optional(), - stringInt64: z.literal('-9223372036854775808').optional(), - stringUint64: z.literal('18446744073709551615').optional() + stringInt64: z.literal("-9223372036854775808").optional(), + stringUint64: z.literal("18446744073709551615").optional() }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 620a0a8640..4404b5b2d7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -53,9 +53,7 @@ export const zPatchFooData = z.object({ baz: z.string().optional() }).optional(), qux: z.string().date().optional(), - quux: z.string().datetime({ - offset: true - }).optional() + quux: z.string().datetime({ offset: true }).optional() }).optional() }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts index 6279534dbd..ad6e8da98b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -455,10 +455,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); @@ -508,7 +508,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -753,10 +753,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.string() export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.string(), z.unknown()); export const zModelWithConst = z.object({ - String: z.optional(z.literal('String')), + String: z.optional(z.literal("String")), number: z.optional(z.literal(0)), null: z.optional(z.null()), - withType: z.optional(z.literal('Some string')) + withType: z.optional(z.literal("Some string")) }); /** @@ -1688,9 +1688,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts index 961bdec15c..152fe92964 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod/v4'; export const zFoo = z.object({ - foo: z.optional(z.literal('foo')), + foo: z.optional(z.literal("foo")), bar: z.optional(z.literal(3.2)), baz: z.optional(z.literal(-1)), qux: z.optional(z.literal(true)), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.optional(z.literal(65535)), integerUint32: z.optional(z.literal(4294967295)), integerUint64: z.optional(z.int()), - stringInt64: z.optional(z.literal('-9223372036854775808')), - stringUint64: z.optional(z.literal('18446744073709551615')) + stringInt64: z.optional(z.literal("-9223372036854775808")), + stringUint64: z.optional(z.literal("18446744073709551615")) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index 36e6f721aa..31f59ff0e6 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -57,9 +57,7 @@ export const zPatchFooData = z.object({ baz: z.optional(z.string()) })), qux: z.optional(z.iso.date()), - quux: z.optional(z.iso.datetime({ - offset: true - })) + quux: z.optional(z.iso.datetime({ offset: true })) })) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts index 0c5d13033f..f7c6a4f81c 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -33,14 +33,14 @@ export const zBaz = z.object({ export const zQux = z.union([ z.intersection(z.object({ - type: z.literal('struct') + type: z.literal("struct") }), z.lazy(() => { return z.lazy((): any => { return zCorge; }); })), z.intersection(z.object({ - type: z.literal('array') + type: z.literal("array") }), zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts index 5b29d62f75..888b82ccb5 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -452,10 +452,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.intersection(z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }), zModelCircle), z.intersection(z.object({ - kind: z.literal('square') + kind: z.literal("square") }), zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts index 6011ab3e7c..f3fb1041d4 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -25,12 +25,12 @@ export const zBaz: z.AnyZodObject = z.object({ export const zQux: z.ZodTypeAny = z.union([ z.object({ - type: z.literal('struct') + type: z.literal("struct") }).and(z.lazy(() => { return zCorge; })), z.object({ - type: z.literal('array') + type: z.literal("array") }).and(zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts index 6b12e930da..6567e45648 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -450,10 +450,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 27ac2c0cc9..f704b6406b 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -33,14 +33,14 @@ export const zBaz = z.object({ export const zQux = z.union([ z.object({ - type: z.literal('struct') + type: z.literal("struct") }).and(z.lazy(() => { return z.lazy((): any => { return zCorge; }); })), z.object({ - type: z.literal('array') + type: z.literal("array") }).and(zFoo) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts index b608099100..4878062a32 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -452,10 +452,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts index 5ffa612787..76572074a6 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -455,10 +455,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.intersection(z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }), zModelCircle), z.intersection(z.object({ - kind: z.literal('square') + kind: z.literal("square") }), zModelSquare) ]); @@ -508,7 +508,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -753,10 +753,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.string() export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.string(), z.unknown()); export const zModelWithConst = z.object({ - String: z.optional(z.literal('String')), + String: z.optional(z.literal("String")), number: z.optional(z.literal(0)), null: z.optional(z.null()), - withType: z.optional(z.literal('Some string')) + withType: z.optional(z.literal("Some string")) }); /** @@ -1688,9 +1688,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts index 1b4e0e1cf1..c6bbe64fa0 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/mini'; export const zFoo = z.object({ - foo: z.optional(z.literal('foo')), + foo: z.optional(z.literal("foo")), bar: z.optional(z.literal(3.2)), baz: z.optional(z.literal(-1)), qux: z.optional(z.literal(true)), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.optional(z.literal(65535)), integerUint32: z.optional(z.literal(4294967295)), integerUint64: z.optional(z.int()), - stringInt64: z.optional(z.literal('-9223372036854775808')), - stringUint64: z.optional(z.literal('18446744073709551615')) + stringInt64: z.optional(z.literal("-9223372036854775808")), + stringUint64: z.optional(z.literal("18446744073709551615")) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index d64c84a813..cc1be0644a 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -57,9 +57,7 @@ export const zPatchFooData = z.object({ baz: z.optional(z.string()) })), qux: z.optional(z.iso.date()), - quux: z.optional(z.iso.datetime({ - offset: true - })) + quux: z.optional(z.iso.datetime({ offset: true })) })) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts index 49aa50ac5d..370c755234 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -453,10 +453,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); @@ -506,7 +506,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -751,10 +751,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.unknown( export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.unknown()); export const zModelWithConst = z.object({ - String: z.literal('String').optional(), + String: z.literal("String").optional(), number: z.literal(0).optional(), null: z.null().optional(), - withType: z.literal('Some string').optional() + withType: z.literal("Some string").optional() }); /** @@ -1686,9 +1686,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts index 2f582cbad9..4994bcc671 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod/v3'; export const zFoo = z.object({ - foo: z.literal('foo').optional(), + foo: z.literal("foo").optional(), bar: z.literal(3.2).optional(), baz: z.literal(-1).optional(), qux: z.literal(true).optional(), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.literal(65535).optional(), integerUint32: z.literal(4294967295).optional(), integerUint64: z.number().int().optional(), - stringInt64: z.literal('-9223372036854775808').optional(), - stringUint64: z.literal('18446744073709551615').optional() + stringInt64: z.literal("-9223372036854775808").optional(), + stringUint64: z.literal("18446744073709551615").optional() }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 746c1c5742..73ff64bc83 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -53,9 +53,7 @@ export const zPatchFooData = z.object({ baz: z.string().optional() }).optional(), qux: z.string().date().optional(), - quux: z.string().datetime({ - offset: true - }).optional() + quux: z.string().datetime({ offset: true }).optional() }).optional() }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts index 8bafc886b2..efe34e8670 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -455,10 +455,10 @@ export const zModelSquare = z.object({ */ export const zCompositionWithOneOfDiscriminator = z.union([ z.object({ - kind: z.literal('circle') + kind: z.literal("circle") }).and(zModelCircle), z.object({ - kind: z.literal('square') + kind: z.literal("square") }).and(zModelSquare) ]); @@ -508,7 +508,7 @@ export const z3eNum1Период = z.enum([ 'Dog' ]); -export const zConstValue = z.literal('ConstValue'); +export const zConstValue = z.literal("ConstValue"); /** * This is a model with one property with a 'any of' relationship where the options are not $ref @@ -753,10 +753,10 @@ export const zFreeFormObjectWithAdditionalPropertiesEqTrue = z.record(z.string() export const zFreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.record(z.string(), z.unknown()); export const zModelWithConst = z.object({ - String: z.optional(z.literal('String')), + String: z.optional(z.literal("String")), number: z.optional(z.literal(0)), null: z.optional(z.null()), - withType: z.optional(z.literal('Some string')) + withType: z.optional(z.literal("Some string")) }); /** @@ -1688,9 +1688,9 @@ export const zTypesData = z.object({ z.null() ]), parameterEnum: z.union([ - z.literal('Success'), - z.literal('Warning'), - z.literal('Error'), + z.literal("Success"), + z.literal("Warning"), + z.literal("Error"), z.null() ]) }) diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts index 2b7e1c4622..46ea47c1df 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/schema-const/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const zFoo = z.object({ - foo: z.optional(z.literal('foo')), + foo: z.optional(z.literal("foo")), bar: z.optional(z.literal(3.2)), baz: z.optional(z.literal(-1)), qux: z.optional(z.literal(true)), @@ -32,6 +32,6 @@ export const zFoo = z.object({ integerUint16: z.optional(z.literal(65535)), integerUint32: z.optional(z.literal(4294967295)), integerUint64: z.optional(z.int()), - stringInt64: z.optional(z.literal('-9223372036854775808')), - stringUint64: z.optional(z.literal('18446744073709551615')) + stringInt64: z.optional(z.literal("-9223372036854775808")), + stringUint64: z.optional(z.literal("18446744073709551615")) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index bc6bd1d71e..44c798c08f 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -57,9 +57,7 @@ export const zPatchFooData = z.object({ baz: z.optional(z.string()) })), qux: z.optional(z.iso.date()), - quux: z.optional(z.iso.datetime({ - offset: true - })) + quux: z.optional(z.iso.datetime({ offset: true })) })) }); diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 9388b7700c..4ccbe3007a 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -76,7 +76,7 @@ import type { LazyOrAsync, MaybeArray } from '~/types/utils'; colors.enabled = colorSupport().hasBasic; -export { createClient } from './generate'; +export { createClient } from '~/generate'; /** * Type helper for openapi-ts.config.ts, returns {@link MaybeArray} object(s) @@ -85,9 +85,9 @@ export const defineConfig = async >( config: LazyOrAsync, ): Promise => (typeof config === 'function' ? await config() : config); -export { defaultPaginationKeywords } from './config/parser'; -export { defaultPlugins } from './config/plugins'; -export type { IR } from './ir/types'; +export { defaultPaginationKeywords } from '~/config/parser'; +export { defaultPlugins } from '~/config/plugins'; +export type { IR } from '~/ir/types'; export type { OpenApi, OpenApiMetaObject, @@ -96,24 +96,25 @@ export type { OpenApiRequestBodyObject, OpenApiResponseObject, OpenApiSchemaObject, -} from './openApi/types'; -export type { DefinePlugin, Plugin } from './plugins'; -export type { AngularClient } from './plugins/@hey-api/client-angular'; -export type { AxiosClient } from './plugins/@hey-api/client-axios'; +} from '~/openApi/types'; +export type { DefinePlugin, Plugin } from '~/plugins'; +export type { AngularClient } from '~/plugins/@hey-api/client-angular'; +export type { AxiosClient } from '~/plugins/@hey-api/client-axios'; export { clientDefaultConfig, clientDefaultMeta, -} from './plugins/@hey-api/client-core/config'; -export { clientPluginHandler } from './plugins/@hey-api/client-core/plugin'; -export type { Client } from './plugins/@hey-api/client-core/types'; -export type { FetchClient } from './plugins/@hey-api/client-fetch'; -export type { NextClient } from './plugins/@hey-api/client-next'; -export type { NuxtClient } from './plugins/@hey-api/client-nuxt'; -export type { OfetchClient } from './plugins/@hey-api/client-ofetch'; -export type { ExpressionTransformer } from './plugins/@hey-api/transformers/expressions'; -export type { TypeTransformer } from './plugins/@hey-api/transformers/types'; -export { definePluginConfig } from './plugins/shared/utils/config'; -export { compiler, tsc } from './tsc'; -export type { UserConfig } from './types/config'; -export { utils } from './utils/exports'; -export { Logger } from './utils/logger'; +} from '~/plugins/@hey-api/client-core/config'; +export { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +export type { Client } from '~/plugins/@hey-api/client-core/types'; +export type { FetchClient } from '~/plugins/@hey-api/client-fetch'; +export type { NextClient } from '~/plugins/@hey-api/client-next'; +export type { NuxtClient } from '~/plugins/@hey-api/client-nuxt'; +export type { OfetchClient } from '~/plugins/@hey-api/client-ofetch'; +export type { ExpressionTransformer } from '~/plugins/@hey-api/transformers/expressions'; +export type { TypeTransformer } from '~/plugins/@hey-api/transformers/types'; +export { definePluginConfig } from '~/plugins/shared/utils/config'; +export * from '~/ts-dsl'; +export { compiler, tsc } from '~/tsc'; +export type { UserConfig } from '~/types/config'; +export { utils } from '~/utils/exports'; +export { Logger } from '~/utils/logger'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts index df25ddd2c3..b1d30ea3a8 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts @@ -271,7 +271,7 @@ const createQueryKeyLiteral = ({ : plugin.config.queryKeys; let tagsArray: TsDsl | undefined; if (config.tags && operation.tags && operation.tags.length > 0) { - tagsArray = $.array().items(...operation.tags); + tagsArray = $.array().elements(...operation.tags); } const symbolCreateQueryKey = plugin.referenceSymbol({ category: 'utility', diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 064647a2a4..b0fbcfbe06 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -105,6 +105,25 @@ export namespace Plugin { name: Name; } + /** + * Generic wrapper for plugin resolvers. + * + * Provides a namespaced configuration entry (`~resolvers`) + * where plugins can define how specific schema constructs + * should be resolved or overridden. + */ + export type Resolvers< + T extends Record = Record, + > = { + /** + * Custom behavior resolvers for a plugin. + * + * Used to define how specific schema constructs are + * resolved into AST or runtime logic. + */ + '~resolvers'?: T; + }; + export type Types< Config extends BaseConfig = BaseConfig, ResolvedConfig extends BaseConfig = Config, diff --git a/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts b/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts index d629fe9d16..f0f805e600 100644 --- a/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/pipesToAst.ts @@ -1,5 +1,6 @@ import type ts from 'typescript'; +import { CallTsDsl } from '~/ts-dsl'; import { $ } from '~/ts-dsl'; import type { ValibotPlugin } from '../types'; @@ -9,11 +10,11 @@ export const pipesToAst = ({ pipes, plugin, }: { - pipes: Array; + pipes: ReadonlyArray; plugin: ValibotPlugin['Instance']; }): ts.Expression => { if (pipes.length === 1) { - return pipes[0]!; + return pipes[0] instanceof CallTsDsl ? pipes[0].$render() : pipes[0]!; } const v = plugin.referenceSymbol({ diff --git a/packages/openapi-ts/src/plugins/valibot/types.d.ts b/packages/openapi-ts/src/plugins/valibot/types.d.ts index 137aa8fd0d..6aed5ae4d7 100644 --- a/packages/openapi-ts/src/plugins/valibot/types.d.ts +++ b/packages/openapi-ts/src/plugins/valibot/types.d.ts @@ -1,10 +1,13 @@ +import type { IR } from '~/ir/types'; import type { DefinePlugin, Plugin } from '~/plugins'; +import type { CallTsDsl, DollarTsDsl } from '~/ts-dsl'; import type { StringCase, StringName } from '~/types/case'; import type { IApi } from './api'; export type UserConfig = Plugin.Name<'valibot'> & - Plugin.Hooks & { + Plugin.Hooks & + Resolvers & { /** * The casing convention to use for generated names. * @@ -176,7 +179,8 @@ export type UserConfig = Plugin.Name<'valibot'> & }; export type Config = Plugin.Name<'valibot'> & - Plugin.Hooks & { + Plugin.Hooks & + Resolvers & { /** * The casing convention to use for generated names. * @@ -313,4 +317,46 @@ export type Config = Plugin.Name<'valibot'> & }; }; +export type FormatResolverArgs = DollarTsDsl & { + /** + * The current builder state being processed by this resolver. + * + * In Valibot, this represents the current list of call expressions ("pipes") + * being assembled to form a schema definition. + * + * Each pipe can be extended, modified, or replaced to customize how the + * resulting schema is constructed. Returning `undefined` from a resolver will + * use the default generation behavior. + */ + pipes: Array; + plugin: ValibotPlugin['Instance']; + schema: IR.SchemaObject; +}; + +type Resolvers = Plugin.Resolvers<{ + /** + * Resolvers for string schemas. + * + * Allows customization of how string types are rendered, including + * per-format handling. + */ + string?: { + /** + * Resolvers for string formats (e.g., `uuid`, `email`, `date-time`). + * + * Each key represents a specific format name with a custom + * resolver function that controls how that format is rendered. + * + * Example path: `~resolvers.string.formats.uuid` + * + * Returning `undefined` from a resolver will apply the default + * generation behavior for that format. + */ + formats?: Record< + string, + (args: FormatResolverArgs) => boolean | number | undefined + >; + }; +}>; + export type ValibotPlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts index b4ac13fe10..523c288d29 100644 --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts @@ -1,6 +1,10 @@ +import type ts from 'typescript'; + import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; +import { pipesToAst } from '../../shared/pipesToAst'; import type { IrSchemaToAstOptions } from '../../shared/types'; import { identifiers } from '../constants'; @@ -9,28 +13,23 @@ export const booleanToAst = ({ schema, }: IrSchemaToAstOptions & { schema: SchemaWithType<'boolean'>; -}) => { +}): ts.Expression => { + const pipes: Array = []; + const v = plugin.referenceSymbol({ category: 'external', resource: 'valibot.v', }); if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; + pipes.push( + $(v.placeholder) + .attr(identifiers.schemas.literal) + .call($.literal(schema.const)), + ); + return pipesToAst({ pipes, plugin }); } - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.schemas.boolean, - }), - }); - return expression; + pipes.push($(v.placeholder).attr(identifiers.schemas.boolean).call()); + return pipesToAst({ pipes, plugin }); }; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts index bdd4493de3..0ba85f9002 100644 --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts @@ -1,153 +1,114 @@ import type ts from 'typescript'; import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { pipesToAst } from '../../shared/pipesToAst'; import type { IrSchemaToAstOptions } from '../../shared/types'; +import type { FormatResolverArgs } from '../../types'; import { identifiers } from '../constants'; +const defaultFormatResolver = ({ + pipes, + plugin, + schema, +}: FormatResolverArgs): boolean | number => { + const v = plugin.referenceSymbol({ + category: 'external', + resource: 'valibot.v', + }); + + switch (schema.format) { + case 'date': + return pipes.push( + $(v.placeholder).attr(identifiers.actions.isoDate).call(), + ); + case 'date-time': + return pipes.push( + $(v.placeholder).attr(identifiers.actions.isoTimestamp).call(), + ); + case 'email': + return pipes.push( + $(v.placeholder).attr(identifiers.actions.email).call(), + ); + case 'ipv4': + case 'ipv6': + return pipes.push($(v.placeholder).attr(identifiers.actions.ip).call()); + case 'time': + return pipes.push( + $(v.placeholder).attr(identifiers.actions.isoTimeSecond).call(), + ); + case 'uri': + return pipes.push($(v.placeholder).attr(identifiers.actions.url).call()); + case 'uuid': + return pipes.push($(v.placeholder).attr(identifiers.actions.uuid).call()); + } + + return true; +}; + export const stringToAst = ({ plugin, schema, }: IrSchemaToAstOptions & { schema: SchemaWithType<'string'>; -}) => { +}): ts.Expression => { + const pipes: Array = []; + const v = plugin.referenceSymbol({ category: 'external', resource: 'valibot.v', }); if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; + pipes.push( + $(v.placeholder) + .attr(identifiers.schemas.literal) + .call($.literal(schema.const)), + ); + return pipesToAst({ pipes, plugin }); } - const pipes: Array = []; - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.schemas.string, - }), - }); - pipes.push(expression); + pipes.push($(v.placeholder).attr(identifiers.schemas.string).call()); if (schema.format) { - switch (schema.format) { - case 'date': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.isoDate, - }), - }), - ); - break; - case 'date-time': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.isoTimestamp, - }), - }), - ); - break; - case 'ipv4': - case 'ipv6': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.ip, - }), - }), - ); - break; - case 'time': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.isoTimeSecond, - }), - }), - ); - break; - case 'uri': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.url, - }), - }), - ); - break; - case 'email': - case 'uuid': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: tsc.identifier({ text: schema.format }), - }), - }), - ); - break; - } + const args: FormatResolverArgs = { $, pipes, plugin, schema }; + const resolver = + plugin.config['~resolvers']?.string?.formats?.[schema.format]; + if (!resolver?.(args)) defaultFormatResolver(args); } if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); + pipes.push( + $(v.placeholder) + .attr(identifiers.actions.length) + .call($.literal(schema.minLength)), + ); } else { if (schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); + pipes.push( + $(v.placeholder) + .attr(identifiers.actions.minLength) + .call($.literal(schema.minLength)), + ); } if (schema.maxLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - pipes.push(expression); + pipes.push( + $(v.placeholder) + .attr(identifiers.actions.maxLength) + .call($.literal(schema.maxLength)), + ); } } if (schema.pattern) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: v.placeholder, - name: identifiers.actions.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - pipes.push(expression); + pipes.push( + $(v.placeholder) + .attr(identifiers.actions.regex) + .call($.regexp(schema.pattern)), + ); } return pipesToAst({ pipes, plugin }); diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts index 13a0516c29..3d3675535f 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts @@ -1,5 +1,6 @@ import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; @@ -10,29 +11,23 @@ export const booleanToAst = ({ }: IrSchemaToAstOptions & { schema: SchemaWithType<'boolean'>; }): Omit => { + const result: Partial> = {}; + let chain: CallTsDsl; + const z = plugin.referenceSymbol({ category: 'external', resource: 'zod.z', }); - const result: Partial> = {}; - if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + result.expression = chain.$render(); return result as Omit; } - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.boolean, - }), - }); + chain = $(z.placeholder).attr(identifiers.boolean).call(); + result.expression = chain.$render(); return result as Omit; }; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts index 39ef1a6511..f7d0824f07 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts @@ -1,10 +1,59 @@ -import type ts from 'typescript'; - import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import type { FormatResolverArgs } from '../../types'; + +const defaultFormatResolver = ({ + chain, + plugin, + schema, +}: FormatResolverArgs): CallTsDsl => { + const z = plugin.referenceSymbol({ + category: 'external', + resource: 'zod.z', + }); + + switch (schema.format) { + case 'date': + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.date) + .call(); + case 'date-time': { + const obj = $.object() + .$if(plugin.config.dates.offset, (o) => + o.prop('offset', $.literal(true)), + ) + .$if(plugin.config.dates.local, (o) => + o.prop('local', $.literal(true)), + ); + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.datetime) + .call(obj.hasProps() ? obj : undefined); + } + case 'email': + return $(z.placeholder).attr(identifiers.email).call(); + case 'ipv4': + return $(z.placeholder).attr(identifiers.ipv4).call(); + case 'ipv6': + return $(z.placeholder).attr(identifiers.ipv6).call(); + case 'time': + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.time) + .call(); + case 'uri': + return $(z.placeholder).attr(identifiers.url).call(); + case 'uuid': + return $(z.placeholder).attr(identifiers.uuid).call(); + default: + return chain; + } +}; export const stringToAst = ({ plugin, @@ -12,185 +61,67 @@ export const stringToAst = ({ }: IrSchemaToAstOptions & { schema: SchemaWithType<'string'>; }): Omit => { + const result: Partial> = {}; + let chain: CallTsDsl; + const z = plugin.referenceSymbol({ category: 'external', resource: 'zod.z', }); - const result: Partial> = {}; - if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + result.expression = chain.$render(); return result as Omit; } - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } + chain = $(z.placeholder).attr(identifiers.string).call(); if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.uuid, - }), - }); - break; - } + const args: FormatResolverArgs = { $, chain, plugin, schema }; + const resolver = + plugin.config['~resolvers']?.string?.formats?.[schema.format]; + chain = resolver?.(args) ?? defaultFormatResolver(args); } - const checks: Array = []; + const checks: Array = []; if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), + $(z.placeholder) + .attr(identifiers.length) + .call($.literal(schema.minLength)), ); } else { if (schema.minLength !== undefined) { checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), + $(z.placeholder) + .attr(identifiers.minLength) + .call($.literal(schema.minLength)), ); } if (schema.maxLength !== undefined) { checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }), + $(z.placeholder) + .attr(identifiers.maxLength) + .call($.literal(schema.maxLength)), ); } } if (schema.pattern) { checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }), + $(z.placeholder).attr(identifiers.regex).call($.regexp(schema.pattern)), ); } if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); + chain = chain.attr(identifiers.check).call(...checks); } + result.expression = chain.$render(); return result as Omit; }; diff --git a/packages/openapi-ts/src/plugins/zod/shared/export.ts b/packages/openapi-ts/src/plugins/zod/shared/export.ts index 3b95952d19..aeac65e575 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/export.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/export.ts @@ -1,9 +1,8 @@ import type { Symbol } from '@hey-api/codegen-core'; -import type ts from 'typescript'; import type { IR } from '~/ir/types'; import { createSchemaComment } from '~/plugins/shared/utils/schema'; -import { tsc } from '~/tsc'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../constants'; import type { ZodPlugin } from '../types'; @@ -27,38 +26,24 @@ export const exportAst = ({ resource: 'zod.z', }); - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createSchemaComment({ schema }) - : undefined, - exportConst: symbol.exported, - expression: ast.expression, - name: symbol.placeholder, - typeName: ast.typeName - ? (tsc.propertyAccessExpression({ - expression: z.placeholder, - name: ast.typeName, - }) as unknown as ts.TypeNode) - : undefined, - }); + const statement = $.const(symbol.placeholder) + .export(symbol.exported) + .$if(plugin.config.comments && createSchemaComment({ schema }), (c, v) => + c.describe(v as ReadonlyArray), + ) + .$if(ast.typeName, (c, v) => c.type($.type(z.placeholder).attr(v))) + .assign(ast.expression); plugin.setSymbolValue(symbol, statement); if (typeInferSymbol) { - const inferType = tsc.typeAliasDeclaration({ - exportType: typeInferSymbol.exported, - name: typeInferSymbol.placeholder, - type: tsc.typeReferenceNode({ - typeArguments: [ - tsc.typeOfExpression({ - text: symbol.placeholder, - }) as unknown as ts.TypeNode, - ], - typeName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.infer, - }) as unknown as string, - }), - }); + const inferType = $.type + .alias(typeInferSymbol.placeholder) + .export(typeInferSymbol.exported) + .type( + $.type(z.placeholder) + .attr(identifiers.infer) + .generic($(symbol.placeholder).typeof()), + ); plugin.setSymbolValue(typeInferSymbol, inferType); } }; diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index 2544c634e9..13b2191d36 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -1,10 +1,13 @@ +import type { IR } from '~/ir/types'; import type { DefinePlugin, Plugin } from '~/plugins'; +import type { CallTsDsl, DollarTsDsl } from '~/ts-dsl'; import type { StringCase, StringName } from '~/types/case'; import type { IApi } from './api'; export type UserConfig = Plugin.Name<'zod'> & - Plugin.Hooks & { + Plugin.Hooks & + Resolvers & { /** * The casing convention to use for generated names. * @@ -417,7 +420,8 @@ export type UserConfig = Plugin.Name<'zod'> & }; export type Config = Plugin.Name<'zod'> & - Plugin.Hooks & { + Plugin.Hooks & + Resolvers & { /** * The casing convention to use for generated names. * @@ -739,4 +743,45 @@ export type Config = Plugin.Name<'zod'> & }; }; +export type FormatResolverArgs = DollarTsDsl & { + /** + * The current fluent builder chain under construction for this resolver. + * + * Represents the in-progress call sequence (e.g., a Zod or DSL chain) + * that defines the current schema or expression being generated. + * + * This chain can be extended, transformed, or replaced entirely to customize + * the resulting output of the resolver. + */ + chain: CallTsDsl; + plugin: ZodPlugin['Instance']; + schema: IR.SchemaObject; +}; + +type Resolvers = Plugin.Resolvers<{ + /** + * Resolvers for string schemas. + * + * Allows customization of how string types are rendered, including + * per-format handling. + */ + string?: { + /** + * Resolvers for string formats (e.g., `uuid`, `email`, `date-time`). + * + * Each key represents a specific format name with a custom + * resolver function that controls how that format is rendered. + * + * Example path: `~resolvers.string.formats.uuid` + * + * Returning `undefined` from a resolver will apply the default + * generation logic for that format. + */ + formats?: Record< + string, + (args: FormatResolverArgs) => CallTsDsl | undefined + >; + }; +}>; + export type ZodPlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts index 33ad7887ad..0de62c8723 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts @@ -1,5 +1,8 @@ +import type ts from 'typescript'; + import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { IrSchemaToAstOptions } from '../../shared/types'; @@ -9,28 +12,21 @@ export const booleanToAst = ({ schema, }: IrSchemaToAstOptions & { schema: SchemaWithType<'boolean'>; -}) => { +}): ts.CallExpression => { + let chain: CallTsDsl; + const z = plugin.referenceSymbol({ category: 'external', resource: 'zod.z', }); if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + return chain.$render(); } - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.boolean, - }), - }); - return expression; + chain = $(z.placeholder).attr(identifiers.boolean).call(); + return chain.$render(); }; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts index bb24aa9114..da49d31134 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts @@ -1,156 +1,93 @@ +import type ts from 'typescript'; + import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { IrSchemaToAstOptions } from '../../shared/types'; +import type { FormatResolverArgs } from '../../types'; + +const defaultFormatResolver = ({ + chain, + plugin, + schema, +}: FormatResolverArgs): CallTsDsl => { + switch (schema.format) { + case 'date': + return chain.attr(identifiers.date).call(); + case 'date-time': { + const obj = $.object() + .$if(plugin.config.dates.offset, (o) => + o.prop('offset', $.literal(true)), + ) + .$if(plugin.config.dates.local, (o) => + o.prop('local', $.literal(true)), + ); + return chain + .attr(identifiers.datetime) + .call(obj.hasProps() ? obj : undefined); + } + case 'email': + return chain.attr(identifiers.email).call(); + case 'ipv4': + case 'ipv6': + return chain.attr(identifiers.ip).call(); + case 'time': + return chain.attr(identifiers.time).call(); + case 'uri': + return chain.attr(identifiers.url).call(); + case 'uuid': + return chain.attr(identifiers.uuid).call(); + default: + return chain; + } +}; export const stringToAst = ({ plugin, schema, }: IrSchemaToAstOptions & { schema: SchemaWithType<'string'>; -}) => { +}): ts.CallExpression => { + let chain: CallTsDsl; + const z = plugin.referenceSymbol({ category: 'external', resource: 'zod.z', }); if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + return chain.$render(); } - let stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } + chain = $(z.placeholder).attr(identifiers.string).call(); if (schema.format) { - switch (schema.format) { - case 'date': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.date, - }), - }); - break; - case 'date-time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - case 'ipv6': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.ip, - }), - }); - break; - case 'time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.time, - }), - }); - break; - case 'uri': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.url, - }), - }); - break; - case 'uuid': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.uuid, - }), - }); - break; - } + const args: FormatResolverArgs = { $, chain, plugin, schema }; + const resolver = + plugin.config['~resolvers']?.string?.formats?.[schema.format]; + chain = resolver?.(args) ?? defaultFormatResolver(args); } if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); + chain = chain.attr(identifiers.length).call($.literal(schema.minLength)); } else { if (schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); + chain = chain.attr(identifiers.min).call($.literal(schema.minLength)); } if (schema.maxLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); + chain = chain.attr(identifiers.max).call($.literal(schema.maxLength)); } } if (schema.pattern) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); + chain = chain.attr(identifiers.regex).call($.regexp(schema.pattern)); } - return stringExpression; + return chain.$render(); }; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts index 215d5eca3d..3d3675535f 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts @@ -1,5 +1,6 @@ import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; @@ -11,6 +12,7 @@ export const booleanToAst = ({ schema: SchemaWithType<'boolean'>; }): Omit => { const result: Partial> = {}; + let chain: CallTsDsl; const z = plugin.referenceSymbol({ category: 'external', @@ -18,21 +20,14 @@ export const booleanToAst = ({ }); if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + result.expression = chain.$render(); return result as Omit; } - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.boolean, - }), - }); + chain = $(z.placeholder).attr(identifiers.boolean).call(); + result.expression = chain.$render(); return result as Omit; }; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts index cd78fdb130..f416bbef26 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts @@ -1,8 +1,59 @@ import type { SchemaWithType } from '~/plugins'; -import { tsc } from '~/tsc'; +import type { CallTsDsl } from '~/ts-dsl'; +import { $ } from '~/ts-dsl'; import { identifiers } from '../../constants'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import type { FormatResolverArgs } from '../../types'; + +const defaultFormatResolver = ({ + chain, + plugin, + schema, +}: FormatResolverArgs): CallTsDsl => { + const z = plugin.referenceSymbol({ + category: 'external', + resource: 'zod.z', + }); + + switch (schema.format) { + case 'date': + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.date) + .call(); + case 'date-time': { + const obj = $.object() + .$if(plugin.config.dates.offset, (o) => + o.prop('offset', $.literal(true)), + ) + .$if(plugin.config.dates.local, (o) => + o.prop('local', $.literal(true)), + ); + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.datetime) + .call(obj.hasProps() ? obj : undefined); + } + case 'email': + return $(z.placeholder).attr(identifiers.email).call(); + case 'ipv4': + return $(z.placeholder).attr(identifiers.ipv4).call(); + case 'ipv6': + return $(z.placeholder).attr(identifiers.ipv6).call(); + case 'time': + return $(z.placeholder) + .attr(identifiers.iso) + .attr(identifiers.time) + .call(); + case 'uri': + return $(z.placeholder).attr(identifiers.url).call(); + case 'uuid': + return $(z.placeholder).attr(identifiers.uuid).call(); + default: + return chain; + } +}; export const stringToAst = ({ plugin, @@ -11,6 +62,7 @@ export const stringToAst = ({ schema: SchemaWithType<'string'>; }): Omit => { const result: Partial> = {}; + let chain: CallTsDsl; const z = plugin.referenceSymbol({ category: 'external', @@ -18,157 +70,38 @@ export const stringToAst = ({ }); if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); + chain = $(z.placeholder) + .attr(identifiers.literal) + .call($.literal(schema.const)); + result.expression = chain.$render(); return result as Omit; } - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } + chain = $(z.placeholder).attr(identifiers.string).call(); if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: z.placeholder, - name: identifiers.uuid, - }), - }); - break; - } + const args: FormatResolverArgs = { $, chain, plugin, schema }; + const resolver = + plugin.config['~resolvers']?.string?.formats?.[schema.format]; + chain = resolver?.(args) ?? defaultFormatResolver(args); } if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); + chain = chain.attr(identifiers.length).call($.literal(schema.minLength)); } else { if (schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); + chain = chain.attr(identifiers.min).call($.literal(schema.minLength)); } if (schema.maxLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); + chain = chain.attr(identifiers.max).call($.literal(schema.maxLength)); } } if (schema.pattern) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); + chain = chain.attr(identifiers.regex).call($.regexp(schema.pattern)); } + result.expression = chain.$render(); return result as Omit; }; diff --git a/packages/openapi-ts/src/ts-dsl/array.ts b/packages/openapi-ts/src/ts-dsl/array.ts index 544d197ed9..c461c0e9bc 100644 --- a/packages/openapi-ts/src/ts-dsl/array.ts +++ b/packages/openapi-ts/src/ts-dsl/array.ts @@ -4,19 +4,19 @@ import { TsDsl } from './base'; import { LiteralTsDsl } from './literal'; export class ArrayTsDsl extends TsDsl { - private elements: Array> = + private _elements: Array> = []; /** Adds one or more elements to the array expression. */ - items( + elements( ...exprs: ReadonlyArray> ): this { - this.elements.push(...exprs); + this._elements.push(...exprs); return this; } $render(): ts.ArrayLiteralExpression { - const elements = this.elements.map((element) => { + const elements = this._elements.map((element) => { if ( typeof element === 'string' || typeof element === 'number' || diff --git a/packages/openapi-ts/src/ts-dsl/index.ts b/packages/openapi-ts/src/ts-dsl/index.ts index 7c2d02d7de..6a8cde5023 100644 --- a/packages/openapi-ts/src/ts-dsl/index.ts +++ b/packages/openapi-ts/src/ts-dsl/index.ts @@ -20,10 +20,12 @@ import { NotTsDsl } from './not'; import { ObjectTsDsl } from './object'; import { ParamTsDsl } from './param'; import { PatternTsDsl } from './pattern'; +import { RegExpTsDsl } from './regexp'; import { ReturnTsDsl } from './return'; import { SetterTsDsl } from './setter'; import { TemplateTsDsl } from './template'; import { ThrowTsDsl } from './throw'; +import { TypeAliasTsDsl } from './type/alias'; import { TypeAttrTsDsl } from './type/attr'; import { TypeExprTsDsl } from './type/expr'; import { TypeLiteralTsDsl } from './type/literal'; @@ -135,6 +137,10 @@ const base = { pattern: (...args: ConstructorParameters) => new PatternTsDsl(...args), + /** Creates a regular expression literal (e.g. `/foo/gi`). */ + regexp: (...args: ConstructorParameters) => + new RegExpTsDsl(...args), + /** Creates a return statement. */ return: (...args: ConstructorParameters) => new ReturnTsDsl(...args), @@ -156,6 +162,10 @@ const base = { (...args: ConstructorParameters) => new TypeExprTsDsl(...args), { + /** Creates a type alias declaration (e.g. `type Foo = Bar`). */ + alias: (...args: ConstructorParameters) => + new TypeAliasTsDsl(...args), + /** Creates a qualified type reference (e.g. Foo.Bar). */ attr: (...args: ConstructorParameters) => new TypeAttrTsDsl(...args), @@ -179,10 +189,61 @@ const base = { new VarTsDsl(...args), }; -/** Creates a general expression node. */ export const $ = Object.assign( (...args: ConstructorParameters) => new ExprTsDsl(...args), base, ); +export type DollarTsDsl = { + /** + * Entry point to the TypeScript DSL. + * + * `$` creates a general expression node by default, but also exposes + * builders for all other constructs such as `.type()`, `.call()`, + * `.object()`, `.func()`, etc. + * + * Example: + * ```ts + * const node = $('console').attr('log').call($.literal('Hello')); + * ``` + * + * Returns: + * - A new `ExprTsDsl` instance when called directly. + * - The `base` factory object for constructing more specific nodes. + */ + $: typeof $; +}; +export { ArrayTsDsl } from './array'; +export { AttrTsDsl } from './attr'; +export { AwaitTsDsl } from './await'; export { TsDsl } from './base'; +export { BinaryTsDsl } from './binary'; +export { CallTsDsl } from './call'; +export { ClassTsDsl } from './class'; +export { DecoratorTsDsl } from './decorator'; +export { DescribeTsDsl } from './describe'; +export { ExprTsDsl } from './expr'; +export { FieldTsDsl } from './field'; +export { FuncTsDsl } from './func'; +export { GetterTsDsl } from './getter'; +export { IfTsDsl } from './if'; +export { InitTsDsl } from './init'; +export { LiteralTsDsl } from './literal'; +export { MethodTsDsl } from './method'; +export { NewTsDsl } from './new'; +export { NewlineTsDsl } from './newline'; +export { NotTsDsl } from './not'; +export { ObjectTsDsl } from './object'; +export { ParamTsDsl } from './param'; +export { PatternTsDsl } from './pattern'; +export { RegExpTsDsl } from './regexp'; +export { ReturnTsDsl } from './return'; +export { SetterTsDsl } from './setter'; +export { TemplateTsDsl } from './template'; +export { ThrowTsDsl } from './throw'; +export { TypeAliasTsDsl } from './type/alias'; +export { TypeAttrTsDsl } from './type/attr'; +export { TypeExprTsDsl } from './type/expr'; +export { TypeLiteralTsDsl } from './type/literal'; +export { TypeObjectTsDsl } from './type/object'; +export { VarTsDsl } from './var'; diff --git a/packages/openapi-ts/src/ts-dsl/object.ts b/packages/openapi-ts/src/ts-dsl/object.ts index e1966bfb1e..d3858fc548 100644 --- a/packages/openapi-ts/src/ts-dsl/object.ts +++ b/packages/openapi-ts/src/ts-dsl/object.ts @@ -17,12 +17,22 @@ export class ObjectTsDsl extends TsDsl { return this; } + /** Returns true if object has at least one property or spread. */ + hasProps(): boolean { + return this.props.length > 0; + } + /** Sets single line output. */ inline(): this { this.layout = false; return this; } + /** Returns true if object has no properties or spreads. */ + get isEmpty(): boolean { + return !this.props.length; + } + /** Sets multi line output. */ pretty(): this { this.layout = true; diff --git a/packages/openapi-ts/src/ts-dsl/regexp.ts b/packages/openapi-ts/src/ts-dsl/regexp.ts new file mode 100644 index 0000000000..a9bc0d794e --- /dev/null +++ b/packages/openapi-ts/src/ts-dsl/regexp.ts @@ -0,0 +1,33 @@ +import ts from 'typescript'; + +import { TsDsl } from './base'; + +type RegexFlag = 'g' | 'i' | 'm' | 's' | 'u' | 'y'; + +type RegexFlags = + | '' + | { + [K in Avail]: `${K}${RegexFlags>}`; + }[Avail]; + +export class RegExpTsDsl extends TsDsl { + private pattern: string; + private flags?: RegexFlags; + + constructor(pattern: string, flags?: RegexFlags) { + super(); + this.pattern = pattern; + this.flags = flags; + } + + /** Emits a RegularExpressionLiteral node. */ + $render(): ts.RegularExpressionLiteral { + const patternContent = + this.pattern.startsWith('/') && this.pattern.endsWith('/') + ? this.pattern.slice(1, -1) + : this.pattern; + const escapedPattern = patternContent.replace(/(? { + private value?: MaybeTsDsl; + private modifiers = createModifierAccessor(this); + private name: string; + + constructor(name: string, fn?: (t: TypeAliasTsDsl) => void) { + super(); + this.name = name; + fn?.(this); + } + + /** Sets the type expression on the right-hand side of `= ...`. */ + type(node: MaybeTsDsl): this { + this.value = node; + return this; + } + + /** Renders a `TypeAliasDeclaration` node. */ + $render(): ts.TypeAliasDeclaration { + if (!this.value) + throw new Error(`Type alias '${this.name}' is missing a type definition`); + return ts.factory.createTypeAliasDeclaration( + this.modifiers.list(), + this.name, + this.$generics(), + this.$type(this.value), + ); + } +} + +export interface TypeAliasTsDsl extends ExportMixin, TypeParamsMixin {} +mixin(TypeAliasTsDsl, ExportMixin, TypeParamsMixin);