From 17bde72d01a4ce6c887d7bd6d3f56857f88b379f Mon Sep 17 00:00:00 2001 From: Yan Zhu Date: Wed, 1 Jul 2026 01:39:06 +0800 Subject: [PATCH] fix(go-server): use operationId consistently to fix uncompilable output for duplicate operationIds When two paths share the same auto-generated operationId (e.g. /foo and /foo/), go-server and go-gin-server produced uncompilable code: the route map and interfaces used {{operationId}} (deduplicated to FooGet_0 by addOperationToGroup) while the controller handler and service stub used {{nickname}} (deduplicated to FooGet_1 by a separate pass with a different counter convention), so routes referenced handlers that didn't exist. Switch the go-server/go-gin-server templates from {{nickname}} to {{operationId}} so routes, interfaces, handlers and service stubs all reference the same deduplicated name. This is a no-op for non-duplicate operationIds and leaves DefaultGenerator untouched, so no other generator is affected. Add regression tests for both generators covering the /foo + /foo/ case. --- .../go-gin-server/controller-api.mustache | 2 +- .../go-gin-server/interface-api.mustache | 4 +- .../go-server/controller-api.mustache | 6 +-- .../main/resources/go-server/service.mustache | 8 +-- .../goginserver/GoGinServerCodegenTest.java | 38 +++++++++++++ .../codegen/goserver/GoServerCodegenTest.java | 53 +++++++++++++++++++ .../duplicate-operation-ids.yaml | 21 ++++++++ .../go-server/duplicate-operation-ids.yaml | 21 ++++++++ 8 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/go-gin-server/duplicate-operation-ids.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/go-server/duplicate-operation-ids.yaml diff --git a/modules/openapi-generator/src/main/resources/go-gin-server/controller-api.mustache b/modules/openapi-generator/src/main/resources/go-gin-server/controller-api.mustache index e480e2394a88..a6ca44747e43 100644 --- a/modules/openapi-generator/src/main/resources/go-gin-server/controller-api.mustache +++ b/modules/openapi-generator/src/main/resources/go-gin-server/controller-api.mustache @@ -15,7 +15,7 @@ type {{classname}} struct { {{#isDeprecated}} // Deprecated {{/isDeprecated}} -func (api *{{classname}}) {{nickname}}(c *gin.Context) { +func (api *{{classname}}) {{operationId}}(c *gin.Context) { // Your handler implementation c.JSON(200, gin.H{"status": "OK"}) } diff --git a/modules/openapi-generator/src/main/resources/go-gin-server/interface-api.mustache b/modules/openapi-generator/src/main/resources/go-gin-server/interface-api.mustache index 0f52d3954d47..aee704ed373e 100644 --- a/modules/openapi-generator/src/main/resources/go-gin-server/interface-api.mustache +++ b/modules/openapi-generator/src/main/resources/go-gin-server/interface-api.mustache @@ -10,12 +10,12 @@ type {{classname}} interface { {{#operation}} - // {{nickname}} {{httpMethod}} {{{basePathWithoutHost}}}{{{path}}}{{#summary}} + // {{operationId}} {{httpMethod}} {{{basePathWithoutHost}}}{{{path}}}{{#summary}} // {{{.}}} {{/summary}} {{#isDeprecated}} // Deprecated {{/isDeprecated}} - {{nickname}}(c *gin.Context) + {{operationId}}(c *gin.Context) {{/operation}} {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache b/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache index b390633971a7..41153b5addde 100644 --- a/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/controller-api.mustache @@ -92,11 +92,11 @@ func (c *{{classname}}Controller) OrderedRoutes() []Route { {{#operations}}{{#operation}} -// {{nickname}} - {{{summary}}} +// {{operationId}} - {{{summary}}} {{#isDeprecated}} // Deprecated {{/isDeprecated}} -func (c *{{classname}}Controller) {{nickname}}(w http.ResponseWriter, r *http.Request) { +func (c *{{classname}}Controller) {{operationId}}(w http.ResponseWriter, r *http.Request) { {{#hasFormParams}} {{#isMultipart}} if err := r.ParseMultipartForm(32 << 20); err != nil { @@ -641,7 +641,7 @@ func (c *{{classname}}Controller) {{nickname}}(w http.ResponseWriter, r *http.Re {{/isArray}} {{/isBodyParam}} {{/allParams}} - result, err := c.service.{{nickname}}(r.Context(){{#allParams}}, {{#isNullable}}{{#isBodyParam}}&{{/isBodyParam}}{{/isNullable}}{{paramName}}Param{{/allParams}}) + result, err := c.service.{{operationId}}(r.Context(){{#allParams}}, {{#isNullable}}{{#isBodyParam}}&{{/isBodyParam}}{{/isNullable}}{{paramName}}Param{{/allParams}}) // If an error occurred, encode the error with the status code if err != nil { c.errorHandler(w, r, err, &result) diff --git a/modules/openapi-generator/src/main/resources/go-server/service.mustache b/modules/openapi-generator/src/main/resources/go-server/service.mustache index 52ddb6ac23b0..27ae7dd6d828 100644 --- a/modules/openapi-generator/src/main/resources/go-server/service.mustache +++ b/modules/openapi-generator/src/main/resources/go-server/service.mustache @@ -19,12 +19,12 @@ func New{{classname}}Service() *{{classname}}Service { return &{{classname}}Service{} }{{#operations}}{{#operation}} -// {{nickname}} - {{summary}} +// {{operationId}} - {{summary}} {{#isDeprecated}} // Deprecated {{/isDeprecated}} -func (s *{{classname}}Service) {{nickname}}(ctx context.Context{{#allParams}}, {{paramName}} {{#isNullable}}*{{/isNullable}}{{dataType}}{{/allParams}}) (ImplResponse, error) { - // TODO - update {{nickname}} with the required logic for this service method. +func (s *{{classname}}Service) {{operationId}}(ctx context.Context{{#allParams}}, {{paramName}} {{#isNullable}}*{{/isNullable}}{{dataType}}{{/allParams}}) (ImplResponse, error) { + // TODO - update {{operationId}} with the required logic for this service method. // Add {{classFilename}}_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation. {{#responses}} @@ -39,5 +39,5 @@ func (s *{{classname}}Service) {{nickname}}(ctx context.Context{{#allParams}}, { {{/dataType}} {{/responses}} - return Response(http.StatusNotImplemented, nil), errors.New("{{nickname}} method not implemented") + return Response(http.StatusNotImplemented, nil), errors.New("{{operationId}} method not implemented") }{{/operation}}{{/operations}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goginserver/GoGinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goginserver/GoGinServerCodegenTest.java index a9b570586a3b..aeb839657ef8 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goginserver/GoGinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goginserver/GoGinServerCodegenTest.java @@ -20,10 +20,12 @@ import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.config.CodegenConfigurator; +import org.testng.Assert; import org.testng.annotations.Test; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; @@ -85,6 +87,42 @@ public void verifyInterfaceOnly() throws IOException { "type PetAPI interface"); } + @Test + public void verifyDuplicateOperationIdsAreConsistent() throws IOException { + // Regression test for duplicate auto-generated operationIds (e.g. /foo and /foo/). + // go-gin-server's routers.mustache references handleFunctions..{{operationId}} + // while interface-api.mustache/controller-api.mustache used to declare the method + // with {{nickname}}. nickname and operationId were deduplicated by two independent + // passes with inconsistent counter conventions (_1 vs _0), so routers referenced + // FooGet_0 while the handler/interface declared FooGet_1, producing uncompilable + // code. The fix makes those templates use {{operationId}} consistently. + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-gin-server/duplicate-operation-ids.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + // routers.go uses {{operationId}} to reference the handler method on the generated struct. + TestUtils.assertFileContains(Paths.get(output + "/go/routers.go"), + "handleFunctions.DefaultAPI.FooGet_0"); + // controller-api.mustache now uses {{operationId}} for the handler method, matching + // the {{operationId}} reference in routers.go. + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "func (api *DefaultAPI) FooGet_0("); + + // Negative assertion: the divergent _1 name must not appear anywhere. + String routers = new String(Files.readAllBytes(Paths.get(output + "/go/routers.go")), StandardCharsets.UTF_8); + String apiDefault = new String(Files.readAllBytes(Paths.get(output + "/go/api_default.go")), StandardCharsets.UTF_8); + Assert.assertFalse(routers.contains("FooGet_1"), + "nickname diverged from operationId (FooGet_1 present in routers.go)"); + Assert.assertFalse(apiDefault.contains("FooGet_1"), + "nickname diverged from operationId (FooGet_1 present in api_default.go)"); + } + private static CodegenConfigurator createDefaultCodegenConfigurator(File output) { return new CodegenConfigurator() .setGeneratorName("go-gin-server") diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java index 9195a5859614..45a258883b8d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/goserver/GoServerCodegenTest.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; @@ -74,6 +75,58 @@ public void verifyOrder() throws IOException { } + @Test + public void verifyDuplicateOperationIdsAreConsistent() throws IOException { + // Regression test for duplicate auto-generated operationIds (e.g. /foo and /foo/). + // Previously nickname and operationId were deduplicated by two independent passes with + // inconsistent counter conventions (_1 vs _0), so the route map / interfaces used + // {{operationId}} (e.g. FooGet_0) while the controller handler / service stub used + // {{nickname}} (e.g. FooGet_1), producing uncompilable code. + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = createDefaultCodegenConfigurator(output) + .setInputSpec("src/test/resources/3_0/go-server/duplicate-operation-ids.yaml"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + // The deduplicated operationId pair is FooGet and FooGet_0; routes, handlers and + // service stubs must all reference the same name (templates use {{operationId}}). + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "\"FooGet\": Route{"); + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "\"FooGet_0\": Route{"); + + // Route handler reference (uses {{operationId}}) and handler method definition + // (uses {{nickname}}) must reference the same name. + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "c.FooGet_0,"); + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "func (c *DefaultAPIController) FooGet_0("); + // Service call inside the handler must also use the deduplicated operationId. + TestUtils.assertFileContains(Paths.get(output + "/go/api_default.go"), + "c.service.FooGet_0("); + + // The interface (api.go) and the service stub must declare FooGet_0 too. + TestUtils.assertFileContains(Paths.get(output + "/go/api.go"), + "FooGet_0("); + TestUtils.assertFileContains(Paths.get(output + "/go/api_default_service.go"), + "func (s *DefaultAPIService) FooGet_0("); + + // Negative assertions: the divergent _1 name must not appear anywhere. + String apiDefault = new String(Files.readAllBytes(Paths.get(output + "/go/api_default.go")), StandardCharsets.UTF_8); + String api = new String(Files.readAllBytes(Paths.get(output + "/go/api.go")), StandardCharsets.UTF_8); + String apiService = new String(Files.readAllBytes(Paths.get(output + "/go/api_default_service.go")), StandardCharsets.UTF_8); + Assert.assertFalse(apiDefault.contains("FooGet_1"), + "nickname diverged from operationId (FooGet_1 present in api_default.go)"); + Assert.assertFalse(api.contains("FooGet_1"), + "nickname diverged from operationId (FooGet_1 present in api.go)"); + Assert.assertFalse(apiService.contains("FooGet_1"), + "nickname diverged from operationId (FooGet_1 present in api_default_service.go)"); + } + private static CodegenConfigurator createDefaultCodegenConfigurator(File output) { return new CodegenConfigurator() .setGeneratorName("go-server") diff --git a/modules/openapi-generator/src/test/resources/3_0/go-gin-server/duplicate-operation-ids.yaml b/modules/openapi-generator/src/test/resources/3_0/go-gin-server/duplicate-operation-ids.yaml new file mode 100644 index 000000000000..c742a851bcc3 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-gin-server/duplicate-operation-ids.yaml @@ -0,0 +1,21 @@ +openapi: 3.0.1 + +info: + version: 1.0.0 + title: Duplicate auto-generated operationIds + +paths: + /foo: + get: + tags: + - default + responses: + '200': + description: ok + /foo/: + get: + tags: + - default + responses: + '200': + description: ok diff --git a/modules/openapi-generator/src/test/resources/3_0/go-server/duplicate-operation-ids.yaml b/modules/openapi-generator/src/test/resources/3_0/go-server/duplicate-operation-ids.yaml new file mode 100644 index 000000000000..c742a851bcc3 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/go-server/duplicate-operation-ids.yaml @@ -0,0 +1,21 @@ +openapi: 3.0.1 + +info: + version: 1.0.0 + title: Duplicate auto-generated operationIds + +paths: + /foo: + get: + tags: + - default + responses: + '200': + description: ok + /foo/: + get: + tags: + - default + responses: + '200': + description: ok