Skip to content
Draft
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
4 changes: 2 additions & 2 deletions templates/default/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (
<% if (routes.outOfModule) { %>
<% for (const route of routes.outOfModule) { %>

<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %>

<% } %>
<% } %>
Expand All @@ -60,7 +60,7 @@ export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (
<%~ moduleName %> = {
<% for (const route of combinedRoutes) { %>

<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: true }) %>

<% } %>
}
Expand Down
4 changes: 2 additions & 2 deletions templates/default/procedure-call.ejs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%
const { utils, route, config } = it;
const { utils, route, config, isObjectProperty = route.namespace } = it;
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
Expand Down Expand Up @@ -90,7 +90,7 @@ const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
<%~ routeDocs.lines %>

*/
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ isObjectProperty ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ isObjectProperty ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
path: `<%~ path %>`,
method: '<%~ _.upperCase(method) %>',
Expand Down
2 changes: 1 addition & 1 deletion templates/modular/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export class <%= apiClassName %><SecurityDataType = unknown><% if (!config.singl
<% } %>

<% for (const route of routes) { %>
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %>
<% } %>
}
4 changes: 2 additions & 2 deletions templates/modular/procedure-call.ejs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%
const { utils, route, config } = it;
const { utils, route, config, isObjectProperty = route.namespace } = it;
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
Expand Down Expand Up @@ -90,7 +90,7 @@ const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
<%~ routeDocs.lines %>

*/
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ isObjectProperty ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ isObjectProperty ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
path: `<%~ path %>`,
method: '<%~ _.upperCase(method) %>',
Expand Down
186 changes: 186 additions & 0 deletions tests/spec/typescript-syntax-fix/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { generateApi } from "../../../src/index.js";

describe("TypeScript syntax fix for operationIds starting with numbers", async () => {
let tmpdir: string;

beforeAll(async () => {
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
});

afterAll(async () => {
await fs.rm(tmpdir, { recursive: true });
});

test("should use assignment syntax for direct class methods", async () => {
const schema = {
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/": {
"get": {
"operationId": "123getUser",
"summary": "Get user with operationId starting with number",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
}
}
}
}
}
}
}
};

const schemaPath = path.join(tmpdir, "direct-method-schema.json");
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2));

await generateApi({
fileName: "direct-method",
input: schemaPath,
output: tmpdir,
silent: true,
generateClient: true,
});

const content = await fs.readFile(path.join(tmpdir, "direct-method.ts"), {
encoding: "utf8",
});

// Should use assignment syntax for direct class methods
expect(content).toContain('"123GetUser" = ');
// Should not use colon syntax for direct class methods
expect(content).not.toContain('"123GetUser": ');
});

test("should use colon syntax for object properties", async () => {
const schema = {
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/api/user/{id}": {
"get": {
"operationId": "456getUser",
"summary": "Get user with operationId starting with number",
"tags": ["Users"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
}
}
}
}
}
}
}
};

const schemaPath = path.join(tmpdir, "object-property-schema.json");
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2));

await generateApi({
fileName: "object-property",
input: schemaPath,
output: tmpdir,
silent: true,
generateClient: true,
});

const content = await fs.readFile(path.join(tmpdir, "object-property.ts"), {
encoding: "utf8",
});

// Should use colon syntax for object properties
expect(content).toContain('"456GetUser": ');
// Should not use assignment syntax for object properties
expect(content).not.toContain('"456GetUser" = ');
});

test("should handle normal identifiers correctly", async () => {
const schema = {
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/": {
"get": {
"operationId": "normalMethod",
"summary": "Normal method",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
}
}
}
};

const schemaPath = path.join(tmpdir, "normal-method-schema.json");
await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2));

await generateApi({
fileName: "normal-method",
input: schemaPath,
output: tmpdir,
silent: true,
generateClient: true,
});

const content = await fs.readFile(path.join(tmpdir, "normal-method.ts"), {
encoding: "utf8",
});

// Should use assignment syntax for direct class methods (no quotes needed)
expect(content).toContain('normalMethod = ');
// Should not use colon syntax for direct class methods
expect(content).not.toContain('normalMethod: ');
});
});
Loading