Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 24 additions & 0 deletions internal/test/server-specific/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Server-specific tests
paths:
# The dashed path parameter name "addressing-identifier" is not a valid Go
# identifier. This exercises GitHub issue #2278: stdhttp's ServeMux requires
# wildcard names to be valid Go identifiers.
/resources/{addressing-identifier}:
get:
operationId: getResource
parameters:
- name: addressing-identifier
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
text/plain:
schema:
type: string
6 changes: 6 additions & 0 deletions internal/test/server-specific/stdhttp/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=../../../../configuration-schema.json
package: stdhttp
generate:
std-http-server: true
models: true
output: server.gen.go
3 changes: 3 additions & 0 deletions internal/test/server-specific/stdhttp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package stdhttp

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../spec.yaml
179 changes: 179 additions & 0 deletions internal/test/server-specific/stdhttp/server.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions internal/test/server-specific/stdhttp/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build go1.22

package stdhttp

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

type testServer struct {
receivedParam string
}

func (s *testServer) GetResource(w http.ResponseWriter, r *http.Request, addressingIdentifier string) {
s.receivedParam = addressingIdentifier
_, _ = fmt.Fprint(w, addressingIdentifier)
}

func TestDashedPathParam(t *testing.T) {
server := &testServer{}
handler := Handler(server)

req := httptest.NewRequest(http.MethodGet, "/resources/my-value", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code, "expected 200 OK, got %d; body: %s", rec.Code, rec.Body.String())
assert.Equal(t, "my-value", server.receivedParam, "path parameter was not correctly extracted")
}
8 changes: 8 additions & 0 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ func (pd *ParameterDefinition) SchemaFormat() string {
return ""
}

// SanitizedParamName returns the parameter name sanitized to be a valid Go
// identifier. This is needed for routers like net/http's ServeMux where path
// wildcards (e.g. {name}) must be valid Go identifiers. For the original
// OpenAPI parameter name (e.g. for error messages or JSON tags), use ParamName.
func (pd ParameterDefinition) SanitizedParamName() string {
return SanitizeGoIdentifier(pd.ParamName)
}

func (pd ParameterDefinition) GoVariableName() string {
name := LowercaseFirstCharacters(pd.GoName())
if IsGoKeyword(name) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/codegen/templates/stdhttp/std-http-middleware.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ
var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}}

{{if .IsPassThrough}}
{{$varName}} = r.PathValue("{{.ParamName}}")
{{$varName}} = r.PathValue("{{.SanitizedParamName}}")
{{end}}
{{if .IsJson}}
err = json.Unmarshal([]byte(r.PathValue("{{.ParamName}}")), &{{$varName}})
err = json.Unmarshal([]byte(r.PathValue("{{.SanitizedParamName}}")), &{{$varName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}
{{if .IsStyled}}
err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", r.PathValue("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"})
err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", r.PathValue("{{.SanitizedParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
Expand Down
28 changes: 21 additions & 7 deletions pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,8 +638,9 @@ func SwaggerUriToGorillaUri(uri string) string {
}

// SwaggerUriToStdHttpUri converts a swagger style path URI with parameters to a
// Chi compatible path URI. We need to replace all Swagger parameters with
// "{param}". Valid input parameters are:
// net/http ServeMux compatible path URI. Parameter names are sanitized to be
// valid Go identifiers, as required by ServeMux wildcard segments. Valid input
// parameters are:
//
// {param}
// {param*}
Expand All @@ -656,7 +657,10 @@ func SwaggerUriToStdHttpUri(uri string) string {
return "/{$}"
}

return pathParamRE.ReplaceAllString(uri, "{$1}")
return pathParamRE.ReplaceAllStringFunc(uri, func(match string) string {
sub := pathParamRE.FindStringSubmatch(match)
return "{" + SanitizeGoIdentifier(sub[1]) + "}"
})
Comment thread
mromaszewicz marked this conversation as resolved.
}

// OrderedParamsFromUri returns the argument names, in order, in a given URI string, so for
Expand Down Expand Up @@ -755,9 +759,12 @@ func IsValidGoIdentity(str string) bool {
return !IsPredeclaredGoIdentifier(str)
}

// SanitizeGoIdentity deletes and replaces the illegal runes in the given
// string to use the string as a valid identity.
func SanitizeGoIdentity(str string) string {
// SanitizeGoIdentifier replaces illegal runes in the given string so that
// it is a valid Go identifier. Unlike SanitizeGoIdentity, it does not
// prefix reserved keywords or predeclared identifiers. This is useful for
// contexts where the name must be a valid identifier but is not used as a
// Go symbol (e.g. net/http ServeMux wildcard names).
func SanitizeGoIdentifier(str string) string {
sanitized := []rune(str)

for i, c := range sanitized {
Expand All @@ -768,7 +775,14 @@ func SanitizeGoIdentity(str string) string {
}
}

str = string(sanitized)
return string(sanitized)
}

// SanitizeGoIdentity deletes and replaces the illegal runes in the given
// string to use the string as a valid identity. It also prefixes reserved
// keywords and predeclared identifiers with an underscore.
func SanitizeGoIdentity(str string) string {
str = SanitizeGoIdentifier(str)

if IsGoKeyword(str) || IsPredeclaredGoIdentifier(str) {
str = "_" + str
Expand Down
7 changes: 7 additions & 0 deletions pkg/codegen/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,13 @@ func TestSwaggerUriToStdHttpUriUri(t *testing.T) {
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{;arg*}/foo"))
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{?arg}/foo"))
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{?arg*}/foo"))

// Parameter names that are not valid Go identifiers must be sanitized (issue #2278)
assert.Equal(t, "/path/{addressing_identifier}", SwaggerUriToStdHttpUri("/path/{addressing-identifier}"))
assert.Equal(t, "/path/{my_param}/{other_param}", SwaggerUriToStdHttpUri("/path/{my-param}/{other-param}"))

// Go keywords are valid ServeMux wildcard names and should not be prefixed
assert.Equal(t, "/path/{type}", SwaggerUriToStdHttpUri("/path/{type}"))
}

func TestOrderedParamsFromUri(t *testing.T) {
Expand Down