From 8b1c67eac7286eafe7f234d4ec8a0829b30bcc6e Mon Sep 17 00:00:00 2001 From: Anton Isoaho Date: Fri, 27 Mar 2026 09:07:43 +0100 Subject: [PATCH] feat(tool): Update the way we handle zod and enums --- .../TypeScript/ApiClientWriterTests.cs | 26 ++++++++++++++++++ .../TypeScript/TypeScriptWriterTests.cs | 8 +++--- .../Templates/EndpointTemplateDto.cs | 1 + TypeContractor/Templates/aurelia.hbs | 2 +- TypeContractor/TypeScript/ApiClientWriter.cs | 15 ++++++----- TypeContractor/TypeScript/ZodSchemaWriter.cs | 27 +++++++++++-------- 6 files changed, 56 insertions(+), 23 deletions(-) diff --git a/TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs b/TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs index 0bb8510..3b2d1d9 100644 --- a/TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs +++ b/TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs @@ -238,6 +238,32 @@ [new EndpointParameter("year", typeof(int), null, false, false, true, false, fal .And.Contain("url.searchParams.append('reportType', reportType.toString());"); } + [Fact] + public void Handles_Enum_Return_With_Zod_Schema() + { + // Arrange + var outputTypes = new List + { + _converter.Convert(typeof(ReportType)) + }; + + var apiClient = new ApiClient("TestClient", "TestController", "test", null); + apiClient.AddEndpoint(new ApiClientEndpoint("getReportType", "report-type", EndpointMethod.GET, typeof(ReportType), typeof(ReportType), false, [], null)); + + // Act + var result = Sut.Write(apiClient, outputTypes, _converter, true, _templateFn, Casing.Pascal); + + // Assert + var file = File.ReadAllText(result).Trim(); + file.Should() + .NotBeEmpty() + .And.Contain("import { z } from 'zod';") + .And.Contain("import { ReportType, ReportTypeEnum } from '~/type-contractor/tests/type-script/report-type';") + .And.Contain("public async getReportType(cancellationToken: AbortSignal = null): Promise {") + .And.Contain("return await response.parseJson(ReportTypeEnum);") + .And.NotContain("ReportTypeSchema"); + } + [Fact] public void Unpacks_Complex_Object_To_Query() { diff --git a/TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs b/TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs index 3109ba4..b736c48 100644 --- a/TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs +++ b/TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs @@ -296,9 +296,9 @@ public void Writes_Zod_Schema_With_Enum_Reference() file.Should() .NotBeEmpty() .And.Contain("import { z } from 'zod';") - .And.Contain("import { ObsoleteEnum } from './ObsoleteEnum';") + .And.Contain("import { ObsoleteEnum, ObsoleteEnumEnum } from './ObsoleteEnum';") .And.Contain("export const TypeWithEnumSchema = z.object({") - .And.Contain(" status: z.enum(ObsoleteEnum),") + .And.Contain(" status: ObsoleteEnumEnum,") .And.Contain("});"); } @@ -316,9 +316,9 @@ public void Writes_Zod_Schema_With_Nullable_Enum() file.Should() .NotBeEmpty() .And.Contain("import { z } from 'zod';") - .And.Contain("import { ObsoleteEnum } from './ObsoleteEnum';") + .And.Contain("import { ObsoleteEnum, ObsoleteEnumEnum } from './ObsoleteEnum';") .And.Contain("export const TypeWithNullableEnumSchema = z.object({") - .And.Contain(" status: z.enum(ObsoleteEnum).nullable(),") + .And.Contain(" status: ObsoleteEnumEnum.nullable(),") .And.Contain("});"); } diff --git a/TypeContractor/Templates/EndpointTemplateDto.cs b/TypeContractor/Templates/EndpointTemplateDto.cs index de73e83..b9dc57c 100644 --- a/TypeContractor/Templates/EndpointTemplateDto.cs +++ b/TypeContractor/Templates/EndpointTemplateDto.cs @@ -8,6 +8,7 @@ public record EndpointTemplateDto( string HttpMethodUppercase, string ReturnType, string? UnwrappedReturnType, + string? UnwrappedReturnSchema, bool EnumerableReturnType, string? MappedReturnType, bool? EnumerableMappedReturnType, diff --git a/TypeContractor/Templates/aurelia.hbs b/TypeContractor/Templates/aurelia.hbs index 41e59c1..d08599a 100644 --- a/TypeContractor/Templates/aurelia.hbs +++ b/TypeContractor/Templates/aurelia.hbs @@ -66,7 +66,7 @@ export class {{Name}} { const response = await this.http.{{HttpMethod}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}json({{BodyParameter}}){{else}}null{{/if}}, {{/if}}{ signal: cancellationToken }); {{#if ../BuildZodSchema}} {{#if UnwrappedReturnType}} - return await response.parseJson<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>({{UnwrappedReturnType}}Schema{{#if EnumerableReturnType}}.array(){{/if}}); + return await response.parseJson<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>({{UnwrappedReturnSchema}}{{#if EnumerableReturnType}}.array(){{/if}}); {{else}} {{#if MappedReturnType}} return await response.parseJson(z.{{MappedReturnType}}(){{#if EnumerableMappedReturnType}}.array(){{/if}}); diff --git a/TypeContractor/TypeScript/ApiClientWriter.cs b/TypeContractor/TypeScript/ApiClientWriter.cs index 6ecfb6a..2ba9b8b 100644 --- a/TypeContractor/TypeScript/ApiClientWriter.cs +++ b/TypeContractor/TypeScript/ApiClientWriter.cs @@ -112,6 +112,11 @@ public string Write(ApiClient apiClient, IEnumerable allTypes, TypeS var targetType = buildZodSchema && endpoint.ReturnType is not null ? converter.GetDestinationType(endpoint.ReturnType, endpoint.ReturnType.CustomAttributes, false, TypeChecks.IsNullable(endpoint.ReturnType)) : null; + var unwrappedReturnSchema = endpoint.UnwrappedReturnType is null + ? null + : endpoint.UnwrappedReturnType.IsEnum + ? $"{endpoint.UnwrappedReturnType.Name}Enum" + : $"{endpoint.UnwrappedReturnType.Name}Schema"; endpoints.Add(new EndpointTemplateDto( endpoint.Name, @@ -121,6 +126,7 @@ public string Write(ApiClient apiClient, IEnumerable allTypes, TypeS method, returnType, endpoint.UnwrappedReturnType?.Name, + unwrappedReturnSchema, endpoint.EnumerableReturnType, targetType?.TypeName, targetType?.IsArray, @@ -171,7 +177,6 @@ private static (string ParameterName, DestinationType? Type, bool IsOptional) Ma private List BuildImports(IEnumerable endpoints, IEnumerable allTypes, TypeScriptConverter converter, bool buildZodSchema) { var imports = new List(); - var needZodLibrary = false; foreach (var endpoint in endpoints) { @@ -179,11 +184,7 @@ private List BuildImports(IEnumerable endpoints, IEnu ? null : converter.GetDestinationType(endpoint.ReturnType, endpoint.ReturnType.CustomAttributes, false, TypeChecks.IsNullable(endpoint.ReturnType)); - if (returnType is not null && returnType.IsBuiltin) - { - needZodLibrary = true; - } - else if (returnType is not null && !returnType.IsBuiltin) + if (returnType is not null && !returnType.IsBuiltin) { var importTypes = new List { returnType.ImportType }; if (buildZodSchema) @@ -227,7 +228,7 @@ private List BuildImports(IEnumerable endpoints, IEnu } } - if (buildZodSchema && needZodLibrary) + if (buildZodSchema) imports.Insert(0, ZodSchemaWriter.LibraryImport); return imports.Distinct().ToList(); diff --git a/TypeContractor/TypeScript/ZodSchemaWriter.cs b/TypeContractor/TypeScript/ZodSchemaWriter.cs index ad03fc1..f0f8a2a 100644 --- a/TypeContractor/TypeScript/ZodSchemaWriter.cs +++ b/TypeContractor/TypeScript/ZodSchemaWriter.cs @@ -39,10 +39,6 @@ public static void Write(OutputType type, IEnumerable allTypes, Stri if (TypeChecks.IsNullable(sourceType)) sourceType = TypeChecks.GetGenericType(sourceType); - // We don't currently import any schema for enums - if (sourceType.IsEnum) - return null; - var suffix = sourceType.IsEnum ? "Enum" : "Schema"; var name = GetImportType(import.InnerSourceType, sourceType); return $"{name}{suffix}"; @@ -53,6 +49,10 @@ public static void Write(OutputType type, IEnumerable allTypes, Stri if (returnType.IsBuiltin) return null; + var sourceType = returnType.InnerType ?? returnType.SourceType; + if (sourceType?.IsEnum == true) + return $"{returnType.ImportType}Enum"; + return $"{returnType.ImportType}Schema"; } @@ -69,12 +69,12 @@ private static string GetImportType(Type? innerSourceType, Type sourceType) private static string? GetZodOutputType(OutputProperty property, IEnumerable allTypes) { if (!property.IsBuiltin && property.SourceType.IsEnum) - return $"z.enum({property.SourceType.Name})"; + return $"{property.SourceType.Name.Split('`').First()}Enum"; else if (!property.IsBuiltin && property.IsNullable && property.SourceType.IsGenericType) { var sourceType = TypeChecks.GetGenericType(property.SourceType); if (sourceType.IsEnum) - return $"z.enum({sourceType.Name}).nullable()"; + return $"{sourceType.Name.Split('`').First()}Enum.nullable()"; } string? output; @@ -87,9 +87,11 @@ private static string GetImportType(Type? innerSourceType, Type sourceType) } else if (!property.IsBuiltin && !property.IsNullable) { - var name = property.InnerSourceType?.Name ?? property.SourceType.Name; + var sourceType = property.InnerSourceType ?? property.SourceType; + var name = sourceType.Name; name = name.Split('`').First(); - output = $"{name}Schema"; + var suffix = sourceType.IsEnum ? "Enum" : "Schema"; + output = $"{name}{suffix}"; } else if (property.IsBuiltin) { @@ -97,9 +99,11 @@ private static string GetImportType(Type? innerSourceType, Type sourceType) } else if (!property.IsBuiltin && property.IsArray && property.InnerSourceType is not null) { - var name = property.InnerSourceType.Name; + var sourceType = property.InnerSourceType; + var name = sourceType.Name; name = name.Split('`').First(); - output = $"{name}Schema"; + var suffix = sourceType.IsEnum ? "Enum" : "Schema"; + output = $"{name}{suffix}"; } else { @@ -142,7 +146,8 @@ private static string GetImportType(Type? innerSourceType, Type sourceType) else if (allTypes.Any(x => IsOfType(sourceType, x.ContractedType.Type))) { var targetType = allTypes.First(x => IsOfType(sourceType, x.ContractedType.Type)); - output = $"{targetType.Name}Schema"; + var suffix = targetType.IsEnum ? "Enum" : "Schema"; + output = $"{targetType.Name}{suffix}"; } else output = "z.any()";