Last active
January 5, 2024 10:47
-
-
Save jamesrr39/86bfa013a93689c7f5bc2da951d1bed0 to your computer and use it in GitHub Desktop.
convience functions for setting up Openapi-backend endpoints in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"fmt" | |
"net/http" | |
"time" | |
"github.com/go-chi/chi/middleware" | |
"github.com/go-chi/chi/v5" | |
"github.com/swaggest/rest" | |
"github.com/swaggest/rest/chirouter" | |
"github.com/swaggest/rest/jsonschema" | |
"github.com/swaggest/rest/nethttp" | |
"github.com/swaggest/rest/openapi" | |
"github.com/swaggest/rest/request" | |
"github.com/swaggest/rest/response" | |
"github.com/swaggest/rest/response/gzip" | |
) | |
func main() { | |
openapiBuilder := NewOpenapiBuilder() | |
// Setup openapi collector | |
apiSchema := &openapi.Collector{} | |
apiSchema.Reflector().SpecEns().Info.Title = "My Application" | |
apiSchema.Reflector().SpecEns().Info.Version = "dev" | |
// Setup request decoder and validator. | |
validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema) | |
decoderFactory := request.NewDecoderFactory() | |
decoderFactory.ApplyDefaults = true | |
decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues) | |
r := chirouter.NewWrapper(chi.NewRouter()) | |
r.Use( | |
middleware.Recoverer, // Panic recovery. | |
nethttp.OpenAPIMiddleware(apiSchema), // Documentation collector. | |
request.DecoderMiddleware(decoderFactory), // Request decoder setup. | |
request.ValidatorMiddleware(validatorFactory), // Request validator setup. | |
response.EncoderMiddleware, // Response encoder setup. | |
gzip.Middleware, // Response compression with support for direct gzip pass through. | |
) | |
Get(r, "/hello/{name}", MustCreateOpenapiEndpoint(openapiBuilder, "Get Picture", createPrintNameHandler())) | |
server := http.Server{ | |
ReadHeaderTimeout: time.Minute, | |
ReadTimeout: time.Minute * 20, | |
WriteTimeout: time.Minute * 20, | |
IdleTimeout: time.Minute * 5, | |
Addr: "localhost:9000", | |
Handler: r, | |
} | |
err := server.ListenAndServe() | |
if err != nil { | |
panic(err) | |
} | |
} | |
type PrintNameReq struct { | |
Name string `path:"name"` | |
} | |
type PrintNameResp struct { | |
Message string `json:"message"` | |
} | |
func createPrintNameHandler() OpenapiHandlerFunc[PrintNameReq, PrintNameResp] { | |
return func(ctx context.Context, input *PrintNameReq, output *PrintNameResp) error { | |
output.Message = fmt.Sprintf("hello %s", input.Name) | |
return nil | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"fmt" | |
"net/http" | |
"strings" | |
"github.com/go-chi/chi/v5" | |
"github.com/swaggest/rest/nethttp" | |
"github.com/swaggest/usecase" | |
) | |
// OpenapiBuilder is an object ensures no duplication of name/title between endpoints | |
type OpenapiBuilder struct { | |
ByNameMap, ByTitleMap map[string]struct{} | |
} | |
func NewOpenapiBuilder() *OpenapiBuilder { | |
return &OpenapiBuilder{ | |
ByNameMap: make(map[string]struct{}), | |
ByTitleMap: make(map[string]struct{}), | |
} | |
} | |
type OpenapiHandlerFunc[Req any, Resp any] func(ctx context.Context, input *Req, output *Resp) error | |
// MustCreateOpenapiEndpoint creates an openapi endpoint. It panics on error. | |
func MustCreateOpenapiEndpoint[Req any, Resp any](builder *OpenapiBuilder, title string, handler OpenapiHandlerFunc[Req, Resp]) *nethttp.Handler { | |
handlerDoc := usecase.IOInteractor{} | |
if strings.TrimSpace(title) == "" { | |
panic("title must be non-blank") | |
} | |
if strings.TrimSpace(title) == title { | |
panic("title must not start or end with whitespace") | |
} | |
name := createEndpointName(title) | |
// check no entry already exists for this name/title | |
// 1. name | |
_, ok := builder.ByNameMap[name] | |
if ok { | |
panic(fmt.Sprintf("name entry already exists for %q", name)) | |
} | |
builder.ByNameMap[name] = struct{}{} | |
// 2. title | |
_, ok = builder.ByTitleMap[name] | |
if ok { | |
panic(fmt.Sprintf("title entry already exists for %q", name)) | |
} | |
builder.ByTitleMap[name] = struct{}{} | |
handlerDoc.SetTitle(title) | |
handlerDoc.SetName(name) | |
handlerDoc.Input = new(Req) | |
handlerDoc.Output = new(Resp) | |
handlerDoc.Interactor = usecase.NewInteractor[*Req, Resp]( | |
func(ctx context.Context, input *Req, output *Resp) error { | |
return handler(ctx, input, output) | |
}) | |
return nethttp.NewHandler(handlerDoc) | |
} | |
// createEndpointName generates an openapi "name" from a given "title" | |
// e.g. "Get All Users" -> "getAllUsers" | |
func createEndpointName(title string) string { | |
var nameFragments []string | |
for idx, titleFragment := range strings.Split(title, " ") { | |
if strings.TrimSpace(titleFragment) == "" { | |
continue | |
} | |
var nameFragment string | |
if idx == 0 { | |
nameFragment = strings.ToLower(string(titleFragment[0])) + titleFragment[1:] | |
} else { | |
nameFragment = strings.ToUpper(string(titleFragment[0])) + titleFragment[1:] | |
} | |
nameFragments = append(nameFragments, nameFragment) | |
} | |
return strings.Join(nameFragments, "") | |
} | |
// convience methods for setting up endpoints | |
func Get(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodGet, path, handler) | |
} | |
func Head(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodHead, path, handler) | |
} | |
func Post(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodPost, path, handler) | |
} | |
func Put(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodPut, path, handler) | |
} | |
func Patch(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodPatch, path, handler) | |
} | |
func Delete(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodDelete, path, handler) | |
} | |
func Connect(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodConnect, path, handler) | |
} | |
func Options(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodOptions, path, handler) | |
} | |
func Trace(r chi.Router, path string, handler *nethttp.Handler) { | |
r.Method(http.MethodTrace, path, handler) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment