diff --git a/templates/default/api.ejs b/templates/default/api.ejs index a393c917..b631ba89 100644 --- a/templates/default/api.ejs +++ b/templates/default/api.ejs @@ -50,7 +50,7 @@ export class <%~ config.apiClassName %><% if ( <% if (routes.outOfModule) { %> <% for (const route of routes.outOfModule) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %> <% } %> <% } %> @@ -60,7 +60,7 @@ export class <%~ config.apiClassName %><% if ( <%~ moduleName %> = { <% for (const route of combinedRoutes) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: true }) %> <% } %> } diff --git a/templates/default/procedure-call.ejs b/templates/default/procedure-call.ejs index 90af47bd..d197037e 100644 --- a/templates/default/procedure-call.ejs +++ b/templates/default/procedure-call.ejs @@ -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; @@ -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) %>', diff --git a/templates/modular/api.ejs b/templates/modular/api.ejs index 25bfa182..070f38b3 100644 --- a/templates/modular/api.ejs +++ b/templates/modular/api.ejs @@ -23,6 +23,6 @@ export class <%= apiClassName %><% if (!config.singl <% } %> <% for (const route of routes) { %> - <%~ includeFile('./procedure-call.ejs', { ...it, route }) %> + <%~ includeFile('./procedure-call.ejs', { ...it, route, isObjectProperty: false }) %> <% } %> } diff --git a/templates/modular/procedure-call.ejs b/templates/modular/procedure-call.ejs index 90af47bd..d197037e 100644 --- a/templates/modular/procedure-call.ejs +++ b/templates/modular/procedure-call.ejs @@ -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; @@ -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) %>', diff --git a/tests/spec/typescript-syntax-fix/basic.test.ts b/tests/spec/typescript-syntax-fix/basic.test.ts new file mode 100644 index 00000000..cc2c0a5f --- /dev/null +++ b/tests/spec/typescript-syntax-fix/basic.test.ts @@ -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: '); + }); +}); \ No newline at end of file