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
4 changes: 4 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen#OutputOptions">OutputOptions</a>
Expand Down
56 changes: 56 additions & 0 deletions pkg/codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Comment thread
mromaszewicz marked this conversation as resolved.
}

func (co CompatibilityOptions) Validate() map[string]string {
Expand Down
46 changes: 37 additions & 9 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down