feat(go-swagger): Refactoring to use go-swagger templating system for most of the work
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Matthieu 'JP' DERASSE
2023-01-01 16:59:41 +00:00
parent d997c0035d
commit 65add2a61d
29 changed files with 538 additions and 21 deletions

View File

@ -0,0 +1,60 @@
layout:
application:
- name: configure
source: asset:serverConfigureapi
target: "{{ joinFilePath .Target .ServerPackage }}"
file_name: "configure_{{ .Name }}.go"
- name: main
source: asset:serverMain
target: "{{ joinFilePath .Target \"cmd\" (dasherize (pascalize .Name)) }}-server"
file_name: "main.go"
skip_exists: true
- name: embedded_spec
source: asset:swaggerJsonEmbed
target: "{{ joinFilePath .Target .ServerPackage }}"
file_name: "embedded_spec.go"
- name: server
source: asset:serverServer
target: "{{ joinFilePath .Target .ServerPackage }}"
file_name: "server.go"
- name: builder
source: asset:serverBuilder
target: "{{ joinFilePath .Target .ServerPackage .Package }}"
file_name: "{{ snakize (pascalize .Name) }}_api.go"
- name: doc
source: asset:serverDoc
target: "{{ joinFilePath .Target .ServerPackage }}"
file_name: "doc.go"
models:
- name: definition
source: asset:model
target: "{{ joinFilePath .Target .ModelPackage }}"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
operations:
- name: parameters
source: asset:serverParameter
target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}"
file_name: "{{ (snakize (pascalize .Name)) }}_parameters.go"
- name: responses
source: asset:serverResponses
target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}"
file_name: "{{ (snakize (pascalize .Name)) }}_responses.go"
- name: handler
source: asset:serverOperation
target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
- name: apihandler
source: asset:apiHandler
target: "{{ joinFilePath .Target \"internal\" \"api\" .Package }}"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
operation_groups:
- name: cmdtag
source: asset:serverTag
target: "{{ joinFilePath .Target \"cmd\" .MainPackage }}"
file_name: "tag_{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
- name: apitag
source: asset:apiTag
target: "{{ joinFilePath .Target \"internal\" \"api\" (snakize (pascalize .Name)) }}"
file_name: "{{ (snakize (pascalize .Name)) }}.go"

View File

@ -0,0 +1,33 @@
package {{ .Package }}
import (
"context"
"github.com/go-openapi/runtime/middleware"
"{{ index .DefaultImports "models"}}"
api "{{ index .DefaultImports .Package }}"
)
// {{ pascalize .Name }} will {{ .Summary }}.
//
// {{ .Method }} {{ .Path }}
func (m {{ pascalize .Package }}) {{ pascalize .Name }}(ctx context.Context, params api.{{ pascalize .Name }}Params) middleware.Responder {
{{ if eq .Name "getMonPing" }}
return api.NewGetMonPingOK().WithPayload(
&models.MonPing{
Details: &models.MonPingDetails{
AppVersion: "",
GitHash: "",
},
Status: &models.MonPingStatus{
Application: true,
Database: true,
},
},
)
{{ else }}
return middleware.NotImplemented("Not Implemented")
{{ end }}
}

View File

@ -0,0 +1,6 @@
// Code generated by go-swagger; DO NOT EDIT.
package {{ (snakize (pascalize .Name)) }}
// {{ pascalize .Name }} implement the {{ pascalize .Name }}API interface from restapi.
type {{ pascalize .Name }} struct{}

View File

@ -0,0 +1,222 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .APIPackage }}
import (
"context"
"crypto/tls"
"net/http"
"log"
"fmt"
"github.com/go-openapi/errors"
"github.com/go-openapi/loads"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/runtime/security"
{{ imports .DefaultImports }}
{{ imports .Imports }}
)
{{ $package := .Package }}
type contextKey string
const AuthKey contextKey = "Auth"
{{ range .OperationGroups -}}
//go:generate mockery -name {{ pascalize .Name}}API -inpkg
/* {{ pascalize .Name }}API {{ .Description }} */
type {{ pascalize .Name }}API interface {
{{ range .Operations -}}
{{ if .Summary -}}
/* {{ pascalize .Name }} {{ .Summary }} */
{{ else if .Description -}}
/* {{ pascalize .Name }} {{ .Description }} */
{{ end -}}
{{ pascalize .Name }}(ctx context.Context, params {{.Package}}.{{ pascalize .Name }}Params) middleware.Responder
{{ end -}}
}
{{ end }}
// Config is configuration for Handler
type Config struct {
{{ range .OperationGroups -}}
{{ pascalize .Name }}API
{{ end -}}
Logger func(string, ...interface{})
// InnerMiddleware is for the handler executors. These do not apply to the swagger.json document.
// The middleware executes after routing but before authentication, binding and validation
InnerMiddleware func(http.Handler) http.Handler
// Authorizer is used to authorize a request after the Auth function was called using the "Auth*" functions
// and the principal was stored in the context in the "AuthKey" context value.
Authorizer func(*http.Request) error
{{ range .SecurityDefinitions -}}
{{ if .IsBasicAuth -}}
// Auth{{ pascalize .ID }} for basic authentication
Auth{{ pascalize .ID }} func(user string, pass string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end -}}
{{ if .IsAPIKeyAuth -}}
// Auth{{ pascalize .ID }} Applies when the "{{ .Name }}" {{ .Source }} is set
Auth{{ pascalize .ID }} func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end }}
{{ if .IsOAuth2 -}}
// Auth{{ pascalize .ID }} For OAuth2 authentication
Auth{{ pascalize .ID }} func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error)
{{ end -}}
{{ end -}}
// Authenticator to use for all APIKey authentication
APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator
// Authenticator to use for all Bearer authentication
BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator
// Authenticator to use for all Basic authentication
BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator
{{ range .Consumes -}}
{{ if .Implementation -}}
// {{ pascalize .Name }}Consumer is a {{ .Name }} consumer that will replace the default if not nil.
{{ pascalize .Name }}Consumer runtime.Consumer
{{ end -}}
{{ end -}}
}
// Handler returns an http.Handler given the handler configuration
// It mounts all the business logic implementers in the right routing.
func Handler(c Config) (http.Handler, error) {
h, _, err := HandlerAPI(c)
return h, err
}
// HandlerAPI returns an http.Handler given the handler configuration
// and the corresponding *{{ pascalize .Name }} instance.
// It mounts all the business logic implementers in the right routing.
func HandlerAPI(c Config) (http.Handler, *{{.Package}}.{{ pascalize .Name }}API, error) {
spec, err := loads.Analyzed(swaggerCopy(SwaggerJSON), "")
if err != nil {
return nil, nil, fmt.Errorf("analyze swagger: %v", err)
}
api := {{.Package}}.New{{ pascalize .Name }}API(spec)
api.ServeError = errors.ServeError
api.Logger = c.Logger
if c.APIKeyAuthenticator != nil {
api.APIKeyAuthenticator = c.APIKeyAuthenticator
}
if c.BasicAuthenticator != nil {
api.BasicAuthenticator = c.BasicAuthenticator
}
if c.BearerAuthenticator != nil {
api.BearerAuthenticator = c.BearerAuthenticator
}
{{ range .Consumes -}}
if c.{{ pascalize .Name }}Consumer != nil {
api.{{ pascalize .Name }}Consumer = c.{{ pascalize .Name }}Consumer
} else {
{{ if .Implementation -}}
api.{{ pascalize .Name }}Consumer = {{ .Implementation }}
{{ else }}
api.{{ pascalize .Name }}Consumer = runtime.ConsumerFunc(func(r io.Reader, target interface{}) error {
return errors.NotImplemented("{{.Name}} consumer has not yet been implemented")
})
{{ end -}}
}
{{ end -}}
{{ range .Produces -}}
{{ if .Implementation -}}
api.{{ pascalize .Name }}Producer = {{ .Implementation }}
{{ else -}}
api.{{ pascalize .Name }}Producer = runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
return errors.NotImplemented("{{.Name}} producer has not yet been implemented")
})
{{ end -}}
{{ end -}}
{{ range .SecurityDefinitions -}}
{{ if .IsBasicAuth -}}
api.{{ pascalize .ID }}Auth = func(user string, pass string) ({{if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return "", nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(user, pass)
}
{{ end -}}
{{ if .IsAPIKeyAuth -}}
api.{{ pascalize .ID }}Auth = func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return token, nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(token)
}
{{ end }}
{{ if .IsOAuth2 -}}
api.{{ pascalize .ID }}Auth = func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) {
if c.Auth{{ pascalize .ID }} == nil {
{{- if eq .Principal "interface{}" }}
return token, nil
{{- else }}
panic("you specified a custom principal type, but did not provide the authenticator to provide this")
{{- end }}
}
return c.Auth{{ pascalize .ID }}(token, scopes)
}
{{ end -}}
{{ end -}}
{{ if .SecurityDefinitions -}}
api.APIAuthorizer = authorizer(c.Authorizer)
{{ end -}}
{{ range .Operations -}}
api.{{if ne .Package $package}}{{pascalize .Package}}{{end}}{{ pascalize .Name }}Handler =
{{- .PackageAlias }}.{{ pascalize .Name }}HandlerFunc(func(params {{.PackageAlias}}.{{ pascalize .Name }}Params{{if .Authorized}}, principal {{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}{{end}}) middleware.Responder {
ctx := params.HTTPRequest.Context()
{{ if .Authorized -}}
ctx = storeAuth(ctx, principal)
{{ end -}}
return c.{{pascalize .Package}}API.{{pascalize .Name}}(ctx, params)
})
{{ end -}}
api.ServerShutdown = func() { }
return api.Serve(c.InnerMiddleware), api, nil
}
// swaggerCopy copies the swagger json to prevent data races in runtime
func swaggerCopy(orig json.RawMessage) json.RawMessage {
c := make(json.RawMessage, len(orig))
copy(c, orig)
return c
}
// authorizer is a helper function to implement the runtime.Authorizer interface.
type authorizer func(*http.Request) error
func (a authorizer) Authorize(req *http.Request, principal interface{}) error {
if a == nil {
return nil
}
ctx := storeAuth(req.Context(), principal)
return a(req.WithContext(ctx))
}
func storeAuth(ctx context.Context, principal interface{}) context.Context {
return context.WithValue(ctx, AuthKey, principal)
}

View File

@ -0,0 +1,92 @@
package main
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/sirupsen/logrus"
"git.dev.m-and-m.ovh/mderasse/gocommon/log"
"git.dev.m-and-m.ovh/mderasse/gocommon/tracing"
"{{ index .DefaultImports "restapi"}}"
)
// initTag is the signature of the function that tag initialization should respect.
type initTag func(*restapi.Config) error
// tagswill be fulfill by init function in tag_XXX.go.
var tags = map[string]initTag{}
func main() {
ctx := context.Background()
// Initialize logs
logger, err := log.Init()
if err != nil {
logrus.WithError(err).Fatal("fail to initialize gocommon.log")
}
// Initialize tracing
logger.Info("Initializing tracing")
tp, err := tracing.Init()
if err != nil {
logger.WithError(err).Fatal("fail to initialize gocommon.tracing")
}
// Configure go-swagger api
logger.Info("Configure go-swagger API")
conf := restapi.Config{
Logger: logger.Printf,
}
// Initialize Tags by calling initTag function that have been stored in tags variable (with init() in tag_XXX.go)
logger.Info("Initializing Tags")
for tag, initTagFunction := range tags {
if err := initTagFunction(&conf); err != nil {
logger.WithError(err).Fatalf("Failed to initialize tag %q", tag)
}
}
// Initialize API Handlers
logger.Info("Initializing API handlers")
h, _, err := restapi.HandlerAPI(conf)
if err != nil {
logger.WithError(err).Fatal("Failed to initialize API Handlers")
}
srv := http.Server{
Addr: ":8081",
Handler: h,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
logger.WithError(err).Fatalf("HTTP server error: %v", err)
}
logger.Info("Stopped serving new connections.")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.WithError(err).Fatalf("HTTP shutdown error: %v", err)
}
tp.Shutdown(ctx)
logger.Info("Graceful shutdown complete.")
}

View File

@ -0,0 +1,9 @@
// Code generated by go-swagger; DO NOT EDIT.
{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }}
package {{ .APIPackage }}
// this file is intentionally empty. Otherwise go-swagger will generate a server which we don't want

View File

@ -0,0 +1,13 @@
package main
import (
"{{ .GenCommon.TargetImportPath }}/internal/api/{{ (dasherize (pascalize .Name)) }}"
"{{ .GenCommon.TargetImportPath }}/restapi"
)
func init() {
tags["{{ pascalize .Name }}"] = func(c *restapi.Config) error {
c.{{ pascalize .Name }}API = {{ (snakize (pascalize .Name)) }}.{{ pascalize .Name }}{}
return nil
}
}

View File