Skip to content
55 changes: 54 additions & 1 deletion openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/json"
"net/http"
"reflect"
"regexp"
"strings"
"time"

"github.com/danielgtaylor/huma/v2/yaml"
Expand Down Expand Up @@ -1541,7 +1543,7 @@ func (o *OpenAPI) MarshalJSON() ([]byte, error) {
{"info", o.Info, omitNever},
{"jsonSchemaDialect", o.JSONSchemaDialect, omitEmpty},
{"servers", o.Servers, omitEmpty},
{"paths", o.Paths, omitEmpty},
{"paths", FixWildcardPaths(o.Paths), omitEmpty},
{"webhooks", o.Webhooks, omitEmpty},
{"components", o.Components, omitEmpty},
{"security", o.Security, omitNil},
Expand Down Expand Up @@ -1677,3 +1679,54 @@ func (o *OpenAPI) DowngradeYAML() ([]byte, error) {
}
return buf.Bytes(), err
}

// Patterns for router-specific wildcards
var (
// Matches {name...} (ServeMux)
serveMuxWildcard = regexp.MustCompile(`\{([^}]+)\.\.\.}`)
// Matches {name:.*} (Gorilla Mux)
gorillaMuxWildcard = regexp.MustCompile(`\{([^:}]+):\.\*}`)
// Matches *name at end of path (Gin, HttpRouter, BunRouter)
starNameWildcard = regexp.MustCompile(`/\*([a-zA-Z_][a-zA-Z0-9_]*)$`)
)
Comment thread
phrozen marked this conversation as resolved.

// fixWildcardPath converts router-specific wildcard patterns to OpenAPI-compatible path parameters
func fixWildcardPath(path string) string {
// ServeMux: {name...} -> {name}
if serveMuxWildcard.MatchString(path) {
return serveMuxWildcard.ReplaceAllString(path, "{$1}")
}

// Gorilla Mux: {name:.*} -> {name}
if gorillaMuxWildcard.MatchString(path) {
return gorillaMuxWildcard.ReplaceAllString(path, "{$1}")
}

// Gin, HttpRouter, BunRouter: /*name -> /{name}
if starNameWildcard.MatchString(path) {
return starNameWildcard.ReplaceAllString(path, "/{$1}")
Comment thread
phrozen marked this conversation as resolved.
Outdated
}

// Chi, Echo, Fiber: trailing /* or /+ -> /{path}
if strings.HasSuffix(path, "/*") {
return strings.TrimSuffix(path, "/*") + "/{path}"
}
if strings.HasSuffix(path, "/+") {
return strings.TrimSuffix(path, "/+") + "/{path}"
}
Comment thread
wolveix marked this conversation as resolved.

// No match, return original
return path
Comment thread
phrozen marked this conversation as resolved.
}

// FixWildcardPaths returns a copy of the paths map with wildcard patterns normalized for OpenAPI
Comment thread
phrozen marked this conversation as resolved.
Outdated
func FixWildcardPaths(paths map[string]*PathItem) map[string]*PathItem {
if paths == nil {
return nil
}
fixed := make(map[string]*PathItem, len(paths))
for path, item := range paths {
fixed[fixWildcardPath(path)] = item
Comment thread
phrozen marked this conversation as resolved.
Outdated
}
return fixed
}
55 changes: 55 additions & 0 deletions openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,58 @@ func TestDowngrade(t *testing.T) {
// Check that the downgrade worked as expected.
assert.JSONEq(t, expected, string(v30))
}

func TestFixWildcardPaths(t *testing.T) {
input := map[string]*huma.PathItem{
// ServeMux
"/api/{path...}": {},
"/files/{filepath...}": {},
// Gorilla Mux
"/mux/{path:.*}": {},
"/mux/v1/{rest:.*}": {},
// Gin, HttpRouter, BunRouter
"/gin/*filepath": {},
"/router/v1/*rest": {},
// Chi, Echo
"/chi/*": {},
"/echo/static/*": {},
// Fiber
"/fiber/+": {},
"/fiber/assets/+": {},
// No wildcard (unchanged)
"/users/{id}": {},
"/api/v1/items": {},
}

expected := map[string]bool{
// ServeMux
"/api/{path}": true,
"/files/{filepath}": true,
// Gorilla Mux
"/mux/{path}": true,
"/mux/v1/{rest}": true,
// Gin, HttpRouter, BunRouter
"/gin/{filepath}": true,
"/router/v1/{rest}": true,
// Chi, Echo
"/chi/{path}": true,
"/echo/static/{path}": true,
// Fiber
"/fiber/{path}": true,
"/fiber/assets/{path}": true,
// No wildcard (unchanged)
"/users/{id}": true,
"/api/v1/items": true,
}
Comment thread
phrozen marked this conversation as resolved.

result := huma.FixWildcardPaths(input)

require.Len(t, result, len(expected), "result should have same number of paths")

for path := range result {
assert.True(t, expected[path], "unexpected path in result: %q", path)
}
Comment thread
phrozen marked this conversation as resolved.

// Test nil input
assert.Nil(t, huma.FixWildcardPaths(nil))
Comment thread
phrozen marked this conversation as resolved.
}
Comment thread
phrozen marked this conversation as resolved.
Comment thread
phrozen marked this conversation as resolved.