Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Allow `TypeContractorIgnore` to be used on controller methods
- Include `IsGet`/`IsPost`/`IsPut`/`IsPatch`/`IsDelete`
in API endpoint template DTOs

### Fixed

- Don't set `RequireBody` for API endpoints using `DELETE`
- `await` all requests in the React template

## [0.20.0] - 2026-05-30

Expand Down
57 changes: 53 additions & 4 deletions TypeContractor.Tests/TypeScript/ApiClientWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ public ApiClientWriterTests()
_converter = new TypeScriptConverter(_configuration, BuildMetadataLoadContext());
Sut = new ApiClientWriter(_configuration.OutputPath, "~");

var embed = typeof(ApiClientWriter).Assembly.GetManifestResourceStream("TypeContractor.Templates.aurelia.hbs");
using var sr = new StreamReader(embed!);
var template = sr.ReadToEnd();
_templateFn = Handlebars.Compile(template);
_templateFn = CompileTemplate("TypeContractor.Templates.aurelia.hbs");
}

[Fact]
Expand Down Expand Up @@ -143,6 +140,50 @@ public void Writes_Post_With_Body()
.And.Contain("return await response.parseJson(z.string());");
}

[Fact]
public void Writes_Delete_Without_Body()
{
// Arrange
var apiClient = new ApiClient("TestClient", "TestController", "test", null);
apiClient.AddEndpoint(new ApiClientEndpoint("byId", "{id}", EndpointMethod.DELETE, null, null, false, [new EndpointParameter("id", typeof(Guid), null, false, false, true, false, false, false, false, false)], null));

// Act
var result = Sut.Write(apiClient, [], _converter, true, _templateFn, Casing.Pascal);

// Assert
var file = File.ReadAllText(result).Trim();
file.Should()
.NotBeEmpty()
.And.NotContain("import { z } from 'zod';") // No schema to care about
.And.Contain("export class TestClient {")
.And.Contain("public async byId(id: string, cancellationToken: AbortSignal = null): Promise<globalThis.Response> {")
.And.Contain("const url = new URL(`test/${id}`, window.location.origin);")
.And.Contain("const response = await this.http.delete(`${url.pathname}${url.search}`.slice(1), null, { signal: cancellationToken });")
.And.Contain("return response;");
}

[Fact]
public void Writes_Delete_Without_Body_Using_React_Template()
{
// Arrange
var apiClient = new ApiClient("TestClient", "TestController", "test", null);
apiClient.AddEndpoint(new ApiClientEndpoint("byId", "{id}", EndpointMethod.DELETE, null, null, false, [new EndpointParameter("id", typeof(Guid), null, false, false, true, false, false, false, false, false)], null));

// Act
var result = Sut.Write(apiClient, [], _converter, true, CompileTemplate("TypeContractor.Templates.react-axios.hbs"), Casing.Pascal);

// Assert
var file = File.ReadAllText(result).Trim();
file.Should()
.NotBeEmpty()
.And.Contain("import axios from 'axios';")
.And.NotContain("import { z } from 'zod';") // No schema to care about
.And.Contain("export const TestClient = {")
.And.Contain("byId: async (id: string, signal: AbortSignal | undefined = undefined) => {")
.And.Contain("const url = new URL(`test/${id}`, window.location.origin);")
.And.Contain("return await axios.delete(`${url.pathname}${url.search}`.slice(1), { signal });");
}

[Fact]
public void Writes_Get_With_Query()
{
Expand Down Expand Up @@ -424,4 +465,12 @@ public void Dispose()
if (_outputDirectory.Exists)
_outputDirectory.Delete(true);
}

private static HandlebarsTemplate<object, object> CompileTemplate(string resourceName)
{
var embed = typeof(ApiClientWriter).Assembly.GetManifestResourceStream(resourceName);
using var sr = new StreamReader(embed!);
var template = sr.ReadToEnd();
return Handlebars.Compile(template);
}
}
5 changes: 5 additions & 0 deletions TypeContractor/Templates/EndpointTemplateDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ public record EndpointTemplateDto(
string ObsoleteReason,
string HttpMethod,
string HttpMethodUppercase,
bool IsGet,
bool IsPost,
bool IsPut,
bool IsPatch,
bool IsDelete,
string ReturnType,
string? UnwrappedReturnType,
string? UnwrappedReturnSchema,
Expand Down
2 changes: 1 addition & 1 deletion TypeContractor/Templates/aurelia.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class {{Name}} {
{{/if}}
{{/if}}
{{/each}}
const response = await this.http.{{HttpMethod}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}json({{BodyParameter}}){{else}}null{{/if}}, {{/if}}{ signal: cancellationToken });
const response = await this.http.{{HttpMethod}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}json({{BodyParameter}}){{else}}null{{/if}}, {{else}}{{#if IsDelete}}null, {{/if}}{{/if}}{ signal: cancellationToken });
{{#if ../BuildZodSchema}}
{{#if UnwrappedReturnType}}
return await response.parseJson<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>({{UnwrappedReturnSchema}}{{#if EnumerableReturnType}}.array(){{/if}});
Expand Down
2 changes: 1 addition & 1 deletion TypeContractor/Templates/react-axios.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ else
{{/if}}
{{/each}}
{{#if ReturnUnparsedResponse}}
return axios.{{HttpMethod}}{{#if UnwrappedReturnType}}<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>{{/if}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}{{BodyParameter}}{{else}}null{{/if}}, {{/if}}{ signal });
return await axios.{{HttpMethod}}{{#if UnwrappedReturnType}}<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>{{/if}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}{{BodyParameter}}{{else}}null{{/if}}, {{/if}}{ signal });
{{else}}
const response = await axios.{{HttpMethod}}{{#if UnwrappedReturnType}}<{{UnwrappedReturnType}}{{#if EnumerableReturnType}}[]{{/if}}>{{/if}}(`${url.pathname}${url.search}`.slice(1), {{#if RequiresBody}}{{#if BodyParameter}}{{BodyParameter}}{{else}}null{{/if}}, {{/if}}{ signal });
return response.data;
Expand Down
7 changes: 6 additions & 1 deletion TypeContractor/TypeScript/ApiClientWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
[GeneratedRegex(@"([^\$])\{([A-Za-z0-9]+)\}")]
private static partial Regex RouteParameterRegex();

public string Write(ApiClient apiClient, IEnumerable<OutputType> allTypes, TypeScriptConverter converter, bool buildZodSchema, HandlebarsTemplate<object, ApiClientTemplateDto> template, Casing casing)

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant

Check warning on line 27 in TypeContractor/TypeScript/ApiClientWriter.cs

View workflow job for this annotation

GitHub Actions / build

Argument type 'HandlebarsTemplate<object, ApiClientTemplateDto>' is not CLS-compliant
{
var _builder = new StringBuilder();
ArgumentNullException.ThrowIfNull(apiClient);
Expand Down Expand Up @@ -105,7 +105,7 @@
}
}

var requiresBody = endpoint.Method is EndpointMethod.POST or EndpointMethod.PUT or EndpointMethod.PATCH or EndpointMethod.DELETE;
var requiresBody = endpoint.Method is EndpointMethod.POST or EndpointMethod.PUT or EndpointMethod.PATCH;
var body = endpoint.Parameters.FirstOrDefault(p => p.FromBody || (!p.FromRoute && !p.FromQuery));

var returnUnparsedResponse = endpoint.UnwrappedReturnType is null && endpoint.ReturnType is null;
Expand All @@ -124,6 +124,11 @@
endpoint.Obsolete?.Reason ?? "",
method.ToLower(CultureInfo.InvariantCulture),
method,
endpoint.Method is EndpointMethod.GET,
endpoint.Method is EndpointMethod.POST,
endpoint.Method is EndpointMethod.PUT,
endpoint.Method is EndpointMethod.PATCH,
endpoint.Method is EndpointMethod.DELETE,
returnType,
endpoint.UnwrappedReturnType?.Name,
unwrappedReturnSchema,
Expand Down
Loading