Skip to content

Commit

Permalink
Add support for Gorilla generation
Browse files Browse the repository at this point in the history
As part of #465, it'd be handy to have gorilla/mux, a commonly used HTTP
server as a generated server.

This is very similar to Chi, as it is also `net/http` compliant, and
allows us to mostly copy-paste the code, with very minor tweaks for
Gorilla-specific routing needs.
  • Loading branch information
Jamie Tanna authored and jamietanna committed May 21, 2022
1 parent b11a594 commit 3010f54
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ func SetupHandler() {
http.Handle("/", Handler(&myApi))
}
```

Alternatively, [Gorilla](https://github.com/gorilla/mux) is also 100% compatible with `net/http` and can be generated with `-generate gorilla`.

</summary></details>

#### Additional Properties in type definitions
Expand Down
4 changes: 3 additions & 1 deletion cmd/oapi-codegen/oapi-codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func main() {
// All flags below are deprecated, and will be removed in a future release. Please do not
// update their behavior.
flag.StringVar(&flagGenerate, "generate", "types,client,server,spec",
`Comma-separated list of code to generate; valid options: "types", "client", "chi-server", "server", "gin", "spec", "skip-fmt", "skip-prune"`)
`Comma-separated list of code to generate; valid options: "types", "client", "chi-server", "server", "gin", "gorilla", "spec", "skip-fmt", "skip-prune"`)
flag.StringVar(&flagIncludeTags, "include-tags", "", "Only include operations with the given tags. Comma-separated list of tags.")
flag.StringVar(&flagExcludeTags, "exclude-tags", "", "Exclude operations that are tagged with the given tags. Comma-separated list of tags.")
flag.StringVar(&flagTemplatesDir, "templates", "", "Path to directory containing user templates")
Expand Down Expand Up @@ -321,6 +321,8 @@ func newConfigFromOldConfig(c oldConfiguration) configuration {
opts.Generate.EchoServer = true
case "gin":
opts.Generate.GinServer = true
case "gorilla":
opts.Generate.GorillaServer = true
case "types":
opts.Generate.Models = true
case "spec":
Expand Down
15 changes: 15 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
}
}

var gorillaServerOut string
if opts.Generate.GorillaServer {
gorillaServerOut, err = GenerateGorillaServer(t, ops)
if err != nil {
return "", fmt.Errorf("error generating Go handlers for Paths: %w", err)
}
}

var clientOut string
if opts.Generate.Client {
clientOut, err = GenerateClient(t, ops)
Expand Down Expand Up @@ -256,6 +264,13 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
}
}

if opts.Generate.GorillaServer {
_, err = w.WriteString(gorillaServerOut)
if err != nil {
return "", fmt.Errorf("error writing server path handlers: %w", err)
}
}

if opts.Generate.EmbeddedSpec {
_, err = w.WriteString(inlinedSpec)
if err != nil {
Expand Down
13 changes: 7 additions & 6 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ type Configuration struct {

// GenerateOptions specifies which supported output formats to generate.
type GenerateOptions struct {
ChiServer bool `yaml:"chi-server,omitempty"` // ChiServer specifies whether to generate chi server boilerplate
EchoServer bool `yaml:"echo-server,omitempty"` // EchoServer specifies whether to generate echo server boilerplate
GinServer bool `yaml:"gin-server,omitempty"` // GinServer specifies whether to generate echo server boilerplate
Client bool `yaml:"client,omitempty"` // Client specifies whether to generate client boilerplate
Models bool `yaml:"models,omitempty"` // Models specifies whether to generate type definitions
EmbeddedSpec bool `yaml:"embedded-spec,omitempty"` // Whether to embed the swagger spec in the generated code
ChiServer bool `yaml:"chi-server,omitempty"` // ChiServer specifies whether to generate chi server boilerplate
EchoServer bool `yaml:"echo-server,omitempty"` // EchoServer specifies whether to generate echo server boilerplate
GinServer bool `yaml:"gin-server,omitempty"` // GinServer specifies whether to generate echo server boilerplate
GorillaServer bool `yaml:"gorilla-server,omitempty"` // GorillaServer specifies whether to generate Gorilla server boilerplate
Client bool `yaml:"client,omitempty"` // Client specifies whether to generate client boilerplate
Models bool `yaml:"models,omitempty"` // Models specifies whether to generate type definitions
EmbeddedSpec bool `yaml:"embedded-spec,omitempty"` // Whether to embed the swagger spec in the generated code
}

// CompatibilityOptions specifies backward compatibility settings for the
Expand Down
6 changes: 6 additions & 0 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,12 @@ func GenerateGinServer(t *template.Template, operations []OperationDefinition) (
return GenerateTemplates([]string{"gin/gin-interface.tmpl", "gin/gin-wrappers.tmpl", "gin/gin-register.tmpl"}, t, operations)
}

// GenerateGinServer This function generates all the go code for the ServerInterface as well as
// all the wrapper functions around our handlers.
func GenerateGorillaServer(t *template.Template, operations []OperationDefinition) (string, error) {
return GenerateTemplates([]string{"gorilla/gorilla-interface.tmpl", "gorilla/gorilla-middleware.tmpl", "gorilla/gorilla-register.tmpl"}, t, operations)
}

// Uses the template engine to generate the function which registers our wrappers
// as Echo path handlers.
func GenerateClient(t *template.Template, ops []OperationDefinition) (string, error) {
Expand Down
1 change: 1 addition & 0 deletions pkg/codegen/template_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ var TemplateFunctions = template.FuncMap{
"swaggerUriToEchoUri": SwaggerUriToEchoUri,
"swaggerUriToChiUri": SwaggerUriToChiUri,
"swaggerUriToGinUri": SwaggerUriToGinUri,
"swaggerUriToGorillaUri": SwaggerUriToGorillaUri,
"lcFirst": LowercaseFirstCharacter,
"ucFirst": UppercaseFirstCharacter,
"camelCase": ToCamelCase,
Expand Down
7 changes: 7 additions & 0 deletions pkg/codegen/templates/gorilla/gorilla-interface.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// ServerInterface represents all server handlers.
type ServerInterface interface {
{{range .}}{{.SummaryAsComment }}
// ({{.Method}} {{.Path}})
{{.OperationId}}(w http.ResponseWriter, r *http.Request{{genParamArgs .PathParams}}{{if .RequiresParamObject}}, params {{.OperationId}}Params{{end}})
{{end}}
}
251 changes: 251 additions & 0 deletions pkg/codegen/templates/gorilla/gorilla-middleware.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}

type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc

{{range .}}{{$opid := .OperationId}}

// {{$opid}} operation middleware
func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
{{if or .RequiresParamObject (gt (len .PathParams) 0) }}
var err error
{{end}}

{{range .PathParams}}// ------------- Path parameter "{{.ParamName}}" -------------
var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}}

{{if .IsPassThrough}}
{{$varName}} = mux.Vars(r)["{{.ParamName}}"]
{{end}}
{{if .IsJson}}
err = json.Unmarshal([]byte(mux.Vars(r)["{{.ParamName}}"]), &{{$varName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}
{{if .IsStyled}}
err = runtime.BindStyledParameter("{{.Style}}",{{.Explode}}, "{{.ParamName}}", mux.Vars(r)["{{.ParamName}}"], &{{$varName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}

{{end}}

{{range .SecurityDefinitions}}
ctx = context.WithValue(ctx, {{.ProviderName | ucFirst}}Scopes, {{toStringArray .Scopes}})
{{end}}

{{if .RequiresParamObject}}
// Parameter object where we will unmarshal all parameters from the context
var params {{.OperationId}}Params

{{range $paramIdx, $param := .QueryParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} query parameter "{{.ParamName}}" -------------
if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" {

{{if .IsPassThrough}}
params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue
{{end}}

{{if .IsJson}}
var value {{.TypeDef}}
err = json.Unmarshal([]byte(paramValue), &value)
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}

params.{{.GoName}} = {{if not .Required}}&{{end}}value
{{end}}
}{{if .Required}} else {
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"})
return
}{{end}}
{{if .IsStyled}}
err = runtime.BindQueryParameter("{{.Style}}", {{.Explode}}, {{.Required}}, "{{.ParamName}}", r.URL.Query(), &params.{{.GoName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}
{{end}}

{{if .HeaderParams}}
headers := r.Header

{{range .HeaderParams}}// ------------- {{if .Required}}Required{{else}}Optional{{end}} header parameter "{{.ParamName}}" -------------
if valueList, found := headers[http.CanonicalHeaderKey("{{.ParamName}}")]; found {
var {{.GoName}} {{.TypeDef}}
n := len(valueList)
if n != 1 {
siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "{{.ParamName}}", Count: n})
return
}

{{if .IsPassThrough}}
params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0]
{{end}}

{{if .IsJson}}
err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}

{{if .IsStyled}}
err = runtime.BindStyledParameterWithLocation("{{.Style}}",{{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, valueList[0], &{{.GoName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}

params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}}

} {{if .Required}}else {
err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found")
siw.ErrorHandlerFunc(w, r, &RequiredHeaderError{ParamName: "{{.ParamName}}", Err: err})
return
}{{end}}

{{end}}
{{end}}

{{range .CookieParams}}
var cookie *http.Cookie

if cookie, err = r.Cookie("{{.ParamName}}"); err == nil {

{{- if .IsPassThrough}}
params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value
{{end}}

{{- if .IsJson}}
var value {{.TypeDef}}
var decoded string
decoded, err := url.QueryUnescape(cookie.Value)
if err != nil {
err = fmt.Errorf("Error unescaping cookie parameter '{{.ParamName}}'")
siw.ErrorHandlerFunc(w, r, &UnescapedCookieParamError{ParamName: "{{.ParamName}}", Err: err})
return
}

err = json.Unmarshal([]byte(decoded), &value)
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}

params.{{.GoName}} = {{if not .Required}}&{{end}}value
{{end}}

{{- if .IsStyled}}
var value {{.TypeDef}}
err = runtime.BindStyledParameter("simple",{{.Explode}}, "{{.ParamName}}", cookie.Value, &value)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
}
params.{{.GoName}} = {{if not .Required}}&{{end}}value
{{end}}

}

{{- if .Required}} else {
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"})
return
}
{{- end}}
{{end}}
{{end}}

var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.{{.OperationId}}(w, r{{genParamNames .PathParams}}{{if .RequiresParamObject}}, params{{end}})
}

for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}

handler(w, r.WithContext(ctx))
}
{{end}}

type UnescapedCookieParamError struct {
ParamName string
Err error
}

func (e *UnescapedCookieParamError) Error() string {
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
}

func (e *UnescapedCookieParamError) Unwrap() error {
return e.Err
}

type UnmarshalingParamError struct {
ParamName string
Err error
}

func (e *UnmarshalingParamError) Error() string {
return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
}

func (e *UnmarshalingParamError) Unwrap() error {
return e.Err
}

type RequiredParamError struct {
ParamName string
}

func (e *RequiredParamError) Error() string {
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
}

type RequiredHeaderError struct {
ParamName string
Err error
}

func (e *RequiredHeaderError) Error() string {
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
}

func (e *RequiredHeaderError) Unwrap() error {
return e.Err
}

type InvalidParamFormatError struct {
ParamName string
Err error
}

func (e *InvalidParamFormatError) Error() string {
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
}

func (e *InvalidParamFormatError) Unwrap() error {
return e.Err
}

type TooManyValuesForParamError struct {
ParamName string
Count int
}

func (e *TooManyValuesForParamError) Error() string {
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
}

0 comments on commit 3010f54

Please sign in to comment.