Skip to content

Instantly share code, notes, and snippets.

@jamesrr39
Last active January 5, 2024 10:47
Show Gist options
  • Save jamesrr39/86bfa013a93689c7f5bc2da951d1bed0 to your computer and use it in GitHub Desktop.
Save jamesrr39/86bfa013a93689c7f5bc2da951d1bed0 to your computer and use it in GitHub Desktop.
convience functions for setting up Openapi-backend endpoints in Go
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
}
}
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