diff --git a/configuration-schema.json b/configuration-schema.json index 4124bec14..bd030f736 100644 --- a/configuration-schema.json +++ b/configuration-schema.json @@ -135,6 +135,10 @@ "enable-auth-scopes-on-context": { "type": "boolean", "description": "DEPRECATED: perform authentication and authorization at runtime using the request validation middleware instead, which evaluates the spec's security requirements directly. Please see https://github.com/oapi-codegen/oapi-codegen/issues/1524\nRe-enables the legacy emission of security scheme scopes by generated server code: the per-scheme context key types (e.g. `bearerAuthContextKey`), the scope constants (e.g. `BearerAuthScopes`), and the per-operation calls that store the operation's scopes into the request context.\nThis mechanism is off by default: it flattens the OpenAPI `security` requirements into a per-scheme list of scopes, and cannot represent alternative schemes (OR), combined schemes (AND), or anonymous (`{}`) alternatives.\nA security scheme that is a $ref into a spec covered by import-mapping does not declare its own context key type; its scopes constant is an alias of the one in the mapped package, so `context.Value` lookups use the same key across the generated packages. This requires the mapped spec's config to also set this flag. Please see https://github.com/oapi-codegen/oapi-codegen/issues/2383" + }, + "sort-handler-registrations": { + "type": "boolean", + "description": "Restores the historical behavior of registering generated route handlers in sorted (lexicographic, by path then method) order. By default handlers are registered in the order their paths are declared in the spec, so that on routers which match in registration order (e.g. Fiber, Gorilla/mux) overlapping paths can be disambiguated by ordering them in the spec. Set this to true to opt out and go back to the old sorted registration order.\nPlease see https://github.com/oapi-codegen/oapi-codegen/issues/1887" } } }, diff --git a/docs/configuration.md b/docs/configuration.md index 2bd0d3075..a088b20c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,6 +62,7 @@ compatibility: disable-enum-value-conflict-resolution: false headers-implicitly-required: false enable-auth-scopes-on-context: false + sort-handler-registrations: false # Output modification options # See OutputOptions diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go index 8b5abd5a5..009ed1066 100644 --- a/pkg/codegen/codegen_test.go +++ b/pkg/codegen/codegen_test.go @@ -780,5 +780,61 @@ func TestSecuritySchemeScopesWithoutOperations(t *testing.T) { assert.Contains(t, code, `BearerAuthScopes bearerAuthContextKey = "BearerAuth.Scopes"`) } +// TestSortHandlerRegistrations verifies the sort-handler-registrations +// compatibility flag: by default handlers are registered in spec-declaration +// order (issue #1887), and setting the flag restores the historical +// lexicographic (by path) registration order. +func TestSortHandlerRegistrations(t *testing.T) { + // Paths are declared in non-lexicographic order: zebra before apple. + const spec = ` +openapi: 3.0.0 +info: { title: t, version: "1.0" } +paths: + /zebra: + get: + operationId: getZebra + responses: { '200': { description: ok } } + /apple: + get: + operationId: getApple + responses: { '200': { description: ok } } +` + load := func() *openapi3.T { + loader := openapi3.NewLoader() + loader.IncludeOrigin = true // recover spec declaration order for SpecOrder + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + return swagger + } + + base := Configuration{ + PackageName: "api", + Generate: GenerateOptions{FiberServer: true, Models: true}, + } + + regOrder := func(code string) (zebra, apple int) { + return strings.Index(code, `options.BaseURL+"/zebra"`), + strings.Index(code, `options.BaseURL+"/apple"`) + } + + // Default: registration follows spec order — zebra before apple. + code, err := Generate(load(), base) + require.NoError(t, err) + zebra, apple := regOrder(code) + require.NotEqual(t, -1, zebra) + require.NotEqual(t, -1, apple) + assert.Less(t, zebra, apple, "default registration should follow spec order (zebra before apple)") + + // Flag set: registration restored to lexicographic order — apple before zebra. + sorted := base + sorted.Compatibility.SortHandlerRegistrations = true + code, err = Generate(load(), sorted) + require.NoError(t, err) + zebra, apple = regOrder(code) + require.NotEqual(t, -1, zebra) + require.NotEqual(t, -1, apple) + assert.Less(t, apple, zebra, "sort-handler-registrations should restore lexicographic order (apple before zebra)") +} + //go:embed test_spec.yaml var testOpenAPIDefinition string diff --git a/pkg/codegen/configuration.go b/pkg/codegen/configuration.go index a6068e36a..0eb375613 100644 --- a/pkg/codegen/configuration.go +++ b/pkg/codegen/configuration.go @@ -386,6 +386,16 @@ type CompatibilityOptions struct { // middleware, which evaluates the spec's security requirements directly. // Please see https://github.com/oapi-codegen/oapi-codegen/issues/1524 EnableAuthScopesOnContext bool `yaml:"enable-auth-scopes-on-context,omitempty"` + + // SortHandlerRegistrations restores the historical behavior of registering + // generated route handlers in sorted (lexicographic, by path then method) + // order. By default handlers are registered in the order their paths are + // declared in the spec, so that on routers which match in registration + // order (e.g. Fiber, Gorilla/mux) overlapping paths can be disambiguated by + // ordering them in the spec. Set this to true to opt out and go back to the + // old sorted registration order. + // Please see https://github.com/oapi-codegen/oapi-codegen/issues/1887 + SortHandlerRegistrations bool `yaml:"sort-handler-registrations,omitempty"` } func (co CompatibilityOptions) Validate() map[string]string { diff --git a/pkg/codegen/operations.go b/pkg/codegen/operations.go index ccb9c39d7..6e0447a5d 100644 --- a/pkg/codegen/operations.go +++ b/pkg/codegen/operations.go @@ -2217,6 +2217,34 @@ func sortOperationsBySpecOrder(ops []OperationDefinition) []OperationDefinition return out } +// sortOperationsLexicographically returns a copy of ops sorted by path then +// method, reproducing the historical (pre-#1887) route-registration order in +// which OperationDefinitions gathers paths. It is a stable no-op on an +// already-gathered slice, but re-sorts explicitly so the ordering does not +// depend on the caller's input. +func sortOperationsLexicographically(ops []OperationDefinition) []OperationDefinition { + out := make([]OperationDefinition, len(ops)) + copy(out, ops) + slices.SortStableFunc(out, func(a, b OperationDefinition) int { + if c := cmp.Compare(a.Path, b.Path); c != 0 { + return c + } + return cmp.Compare(a.Method, b.Method) + }) + return out +} + +// operationsInRegistrationOrder returns ops in the order route handlers should +// be registered. By default this follows spec-declaration order (issue #1887); +// when the sort-handler-registrations compatibility flag is set it restores the +// historical lexicographic order. +func operationsInRegistrationOrder(ops []OperationDefinition) []OperationDefinition { + if globalState.options.Compatibility.SortHandlerRegistrations { + return sortOperationsLexicographically(ops) + } + return sortOperationsBySpecOrder(ops) +} + // GenerateIrisServer generates all the go code for the ServerInterface as well as // all the wrapper functions around our handlers. func GenerateIrisServer(t *template.Template, operations []OperationDefinition) (string, error) { @@ -2225,7 +2253,7 @@ func GenerateIrisServer(t *template.Template, operations []OperationDefinition) return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"iris/iris-handler.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"iris/iris-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2239,7 +2267,7 @@ func GenerateChiServer(t *template.Template, operations []OperationDefinition) ( return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"chi/chi-handler.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"chi/chi-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2253,7 +2281,7 @@ func GenerateFiberServer(t *template.Template, operations []OperationDefinition) return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber/fiber-handler.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber/fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2267,7 +2295,7 @@ func GenerateFiberV3Server(t *template.Template, operations []OperationDefinitio return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-v3/fiber-handler.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"fiber-v3/fiber-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2281,7 +2309,7 @@ func GenerateEchoServer(t *template.Template, operations []OperationDefinition) return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-register.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/echo-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2295,7 +2323,7 @@ func GenerateEcho5Server(t *template.Template, operations []OperationDefinition) return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/v5/echo-register.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"echo/v5/echo-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2309,7 +2337,7 @@ func GenerateGinServer(t *template.Template, operations []OperationDefinition) ( return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"gin/gin-register.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"gin/gin-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2323,7 +2351,7 @@ func GenerateGorillaServer(t *template.Template, operations []OperationDefinitio return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"gorilla/gorilla-register.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"gorilla/gorilla-register.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil @@ -2337,7 +2365,7 @@ func GenerateStdHTTPServer(t *template.Template, operations []OperationDefinitio return "", err } // Route registration follows spec-declaration order (issue #1887). - if err := GenerateTemplatesIntoBuffer(&buf, []string{"stdhttp/std-http-handler.tmpl"}, t, sortOperationsBySpecOrder(operations)); err != nil { + if err := GenerateTemplatesIntoBuffer(&buf, []string{"stdhttp/std-http-handler.tmpl"}, t, operationsInRegistrationOrder(operations)); err != nil { return "", err } return buf.String(), nil