Repository: deepmap/oapi-codegen
Pull request #533 (2022-02-27 17:26:49)

Fix Type Prefix
While trying to generate a client for the GitHub API, `oapi-codegen` generated a struct like this:

```go
type ReactionRollup struct {
	N1         int    `json:"+1"`
	N1         int    `json:"-1"`
}
```

To prevent a duplicate field like that, I'm making this PR to generate smarter prefixes for schema type names.



Patch:
diff --git a/README.md b/README.md
index 3e0fba8..b7b9387 100644
--- a/README.md
+++ b/README.md
@@ -474,6 +474,10 @@ which help you to use the various OpenAPI 3 Authentication mechanism.
   will override any default value. This extended property isn't supported in all parts of
   OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will
   flag incorrect usage of this property.
+- `x-go-name`: specifies Go field name. It allows you to specify the field name for a schema, and
+  will override any default value. This extended property isn't supported in all parts of
+  OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will
+  flag incorrect usage of this property.
 - `x-oapi-codegen-extra-tags`: adds extra Go field tags to the generated struct field. This is
   useful for interfacing with tag based ORM or validation libraries. The extra tags that
   are added are in addition to the regular json tags that are generated. If you specify your 
diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go
index 49c8747..cfe5ef4 100644
--- a/cmd/oapi-codegen/oapi-codegen.go
+++ b/cmd/oapi-codegen/oapi-codegen.go
@@ -163,7 +163,7 @@ func main() {
 			errExit("error writing generated code to file: %s", err)
 		}
 	} else {
-		fmt.Println(code)
+		fmt.Print(code)
 	}
 }
 
diff --git a/pkg/codegen/extension.go b/pkg/codegen/extension.go
index 89d85fa..28294b1 100644
--- a/pkg/codegen/extension.go
+++ b/pkg/codegen/extension.go
@@ -7,21 +7,29 @@ import (
 
 const (
 	extPropGoType    = "x-go-type"
+	extGoFieldName   = "x-go-name"
 	extPropOmitEmpty = "x-omitempty"
 	extPropExtraTags = "x-oapi-codegen-extra-tags"
 )
 
-func extTypeName(extPropValue interface{}) (string, error) {
+func extString(extPropValue interface{}) (string, error) {
 	raw, ok := extPropValue.(json.RawMessage)
 	if !ok {
 		return "", fmt.Errorf("failed to convert type: %T", extPropValue)
 	}
-	var name string
-	if err := json.Unmarshal(raw, &name); err != nil {
+	var str string
+	if err := json.Unmarshal(raw, &str); err != nil {
 		return "", fmt.Errorf("failed to unmarshal json: %w", err)
 	}
 
-	return name, nil
+	return str, nil
+}
+func extTypeName(extPropValue interface{}) (string, error) {
+	return extString(extPropValue)
+}
+
+func extParseGoFieldName(extPropValue interface{}) (string, error) {
+	return extString(extPropValue)
 }
 
 func extParseOmitEmpty(extPropValue interface{}) (bool, error) {
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index 1ee521c..b5aca0a 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -432,7 +432,15 @@ func GenFieldsFromProperties(props []Property) []string {
 			}
 			field += fmt.Sprintf("%s\n", StringToGoComment(p.Description))
 		}
-		field += fmt.Sprintf("    %s %s", p.GoFieldName(), p.GoTypeDef())
+
+		goFieldName := p.GoFieldName()
+		if _, ok := p.ExtensionProps.Extensions[extGoFieldName]; ok {
+			if extGoFieldName, err := extParseGoFieldName(p.ExtensionProps.Extensions[extGoFieldName]); err == nil {
+				goFieldName = extGoFieldName
+			}
+		}
+
+		field += fmt.Sprintf("    %s %s", goFieldName, p.GoTypeDef())
 
 		// Support x-omitempty
 		omitEmpty := true
diff --git a/pkg/codegen/templates/gin/gin-register.tmpl b/pkg/codegen/templates/gin/gin-register.tmpl
index 5362dd7..cdd11e8 100644
--- a/pkg/codegen/templates/gin/gin-register.tmpl
+++ b/pkg/codegen/templates/gin/gin-register.tmpl
@@ -1,21 +1,26 @@
 // GinServerOptions provides options for the Gin server.
 type GinServerOptions struct {
     BaseURL string
-    Middlewares []MiddlewareFunc
+    Middlewares []gin.HandlerFunc
 }
 
+// keeping for backward compatibility
+type MiddlewareFunc = gin.HandlerFunc
+
 // RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
-func RegisterHandlers(router *gin.Engine, si ServerInterface) *gin.Engine {
+func RegisterHandlers(router gin.IRouter, si ServerInterface) gin.IRouter {
   return RegisterHandlersWithOptions(router, si, GinServerOptions{})
 }
 
 // RegisterHandlersWithOptions creates http.Handler with additional options
-func RegisterHandlersWithOptions(router *gin.Engine, si ServerInterface, options GinServerOptions) *gin.Engine {
+func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) gin.IRouter {
 {{if .}}wrapper := ServerInterfaceWrapper{
 Handler: si,
-HandlerMiddlewares: options.Middlewares,
 }
 {{end}}
+
+router = router.Group("/", options.Middlewares...)
+
 {{range .}}
 router.{{.Method }}(options.BaseURL+"{{.Path | swaggerUriToGinUri }}", wrapper.{{.OperationId}})
 {{end}}
diff --git a/pkg/codegen/templates/gin/gin-wrappers.tmpl b/pkg/codegen/templates/gin/gin-wrappers.tmpl
index 6eb787c..0ddc686 100644
--- a/pkg/codegen/templates/gin/gin-wrappers.tmpl
+++ b/pkg/codegen/templates/gin/gin-wrappers.tmpl
@@ -1,11 +1,8 @@
 // ServerInterfaceWrapper converts contexts to parameters.
 type ServerInterfaceWrapper struct {
     Handler ServerInterface
-    HandlerMiddlewares []MiddlewareFunc
 }
 
-type MiddlewareFunc func(c *gin.Context)
-
 {{range .}}{{$opid := .OperationId}}
 
 // {{$opid}} operation middleware
@@ -165,10 +162,6 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) {
     {{end}}
   {{end}}
 
-  for _, middleware := range siw.HandlerMiddlewares {
-    middleware(c)
-  }
-
   siw.Handler.{{.OperationId}}(c{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}})
 }
 {{end}}
diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go
index f433f48..a7dc3cf 100644
--- a/pkg/codegen/utils.go
+++ b/pkg/codegen/utils.go
@@ -534,19 +534,45 @@ func SanitizeEnumNames(enumNames []string) map[string]string {
 	return sanitizedDeDup
 }
 
-// Converts a Schema name to a valid Go type name. It converts to camel case, and makes sure the name is
-// valid in Go
-func SchemaNameToTypeName(name string) string {
-	if name == "$" {
-		name = "DollarSign"
-	} else {
-		name = ToCamelCase(name)
-		// Prepend "N" to schemas starting with a number
-		if name != "" && unicode.IsDigit([]rune(name)[0]) {
-			name = "N" + name
+func typeNamePrefix(name string) (prefix string) {
+	for _, r := range name {
+		switch r {
+		case '$':
+			if len(name) == 1 {
+				return "DollarSign"
+			}
+		case '-':
+			prefix += "Minus"
+		case '+':
+			prefix += "Plus"
+		case '&':
+			prefix += "And"
+		case '~':
+			prefix += "Tilde"
+		case '=':
+			prefix += "Equal"
+		case '#':
+			prefix += "Hash"
+		case '.':
+			prefix += "Dot"
+		default:
+			// Prepend "N" to schemas starting with a number
+			if prefix == "" && unicode.IsDigit(r) {
+				return "N"
+			}
+
+			// break the loop, done parsing prefix
+			return
 		}
 	}
-	return name
+
+	return
+}
+
+// SchemaNameToTypeName converts a Schema name to a valid Go type name. It converts to camel case, and makes sure the name is
+// valid in Go
+func SchemaNameToTypeName(name string) string {
+	return typeNamePrefix(name) + ToCamelCase(name)
 }
 
 // According to the spec, additionalProperties may be true, false, or a
diff --git a/pkg/gin-middleware/oapi_validate.go b/pkg/gin-middleware/oapi_validate.go
index 82a885e..44075c3 100644
--- a/pkg/gin-middleware/oapi_validate.go
+++ b/pkg/gin-middleware/oapi_validate.go
@@ -72,6 +72,8 @@ func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) gin.
 		err := ValidateRequestFromContext(c, router, options)
 		if err != nil {
 			// note: i am not sure if this is the best way to handle this
+			// todo: the error message should be customizable by the user
+			c.Error(err)
 			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 		}
 		c.Next()
@@ -124,6 +126,9 @@ func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *
 			errorLines := strings.Split(e.Error(), "\n")
 			return fmt.Errorf("error in openapi3filter.RequestError: %s", errorLines[0])
 		case *openapi3filter.SecurityRequirementsError:
+			for _, e := range e.Errors {
+				c.Error(e)
+			}
 			return fmt.Errorf("error in openapi3filter.SecurityRequirementsError: %s", e.Error())
 		default:
 			// This should never happen today, but if our upstream code changes,


Test patch:
diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go
index fd560be..b391852 100644
--- a/pkg/codegen/utils_test.go
+++ b/pkg/codegen/utils_test.go
@@ -337,3 +337,25 @@ func TestEscapePathElements(t *testing.T) {
 	p = "/foo/bar:baz"
 	assert.Equal(t, "/foo/bar%3Abaz", EscapePathElements(p))
 }
+
+func TestSchemaNameToTypeName(t *testing.T) {
+	t.Parallel()
+
+	for in, want := range map[string]string{
+		"$":            "DollarSign",
+		"$ref":         "Ref",
+		"no_prefix~+-": "NoPrefix",
+		"123":          "N123",
+		"-1":           "Minus1",
+		"+1":           "Plus1",
+		"@timestamp,":  "Timestamp",
+		"&now":         "AndNow",
+		"~":            "Tilde",
+		"_foo":         "Foo",
+		"=3":           "Equal3",
+		"#Tag":         "HashTag",
+		".com":         "DotCom",
+	} {
+		assert.Equal(t, want, SchemaNameToTypeName(in))
+	}
+}