twitch-bot/swagger.go

182 lines
4.9 KiB
Go
Raw Permalink Normal View History

package main
import (
"bytes"
_ "embed"
"encoding/json"
"io"
"net/http"
"strings"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/wzshiming/openapi/spec"
"github.com/Luzifer/twitch-bot/v3/plugins"
)
var (
swaggerDoc = spec.OpenAPI{
OpenAPI: "3.0.3", // This generator uses v3 of OpenAPI standard
Info: &spec.Info{
Title: "Twitch-Bot public API",
Version: "v1",
},
Servers: []*spec.Server{
{URL: "./", Description: "Current bot instance"},
},
Paths: make(spec.Paths),
Components: &spec.Components{
Responses: map[string]*spec.Response{
"genericErrorResponse": spec.TextPlainResponse(nil).WithDescription("An error occurred: See error message"),
"inputErrorResponse": spec.TextPlainResponse(nil).WithDescription("Data sent to API is invalid: See error message"),
"notFoundResponse": spec.TextPlainResponse(nil).WithDescription("Document was not found or insufficient permissions"),
},
},
}
//go:embed swagger.html
swaggerHTML []byte
)
func init() {
secConfigEditor := spec.APIKeyAuth("Authorization", spec.InHeader)
secConfigEditor.Description = "Authorization token issued by Twitch"
secWriteAuth := spec.APIKeyAuth("Authorization", spec.InHeader)
secWriteAuth.Description = "Authorization token stored in the config"
swaggerDoc.Components.SecuritySchemes = map[string]*spec.SecurityScheme{
"configEditor": secConfigEditor,
"writeAuth": secWriteAuth,
}
}
func handleSwaggerHTML(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if _, err := io.Copy(w, bytes.NewReader(swaggerHTML)); err != nil {
http.Error(w, errors.Wrap(err, "writing frontend").Error(), http.StatusInternalServerError)
}
}
func handleSwaggerRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(swaggerDoc); err != nil {
http.Error(w, errors.Wrap(err, "rendering documentation").Error(), http.StatusInternalServerError)
}
}
//nolint:funlen,gocyclo // Makes no sense to split just to spare a little complexity
func registerSwaggerRoute(route plugins.HTTPRouteRegistrationArgs) error {
fullPath := strings.Join([]string{
"",
route.Module,
strings.TrimLeft(route.Path, "/"),
}, "/")
pi, ok := swaggerDoc.Paths[fullPath]
if !ok {
pi = &spec.PathItem{}
for _, param := range route.RouteParams {
pi.Parameters = append(
pi.Parameters,
spec.PathParam(param.Name, spec.StringProperty()).WithDescription(param.Description),
)
}
swaggerDoc.Paths[fullPath] = pi
}
op := &spec.Operation{
Summary: route.Name,
Description: route.Description,
Tags: []string{route.Module},
Responses: map[string]*spec.Response{
"204": spec.TextPlainResponse(nil).WithDescription("Successful execution without response object"),
"404": spec.RefResponse("notFoundResponse"),
"500": spec.RefResponse("genericErrorResponse"),
},
}
switch {
case route.RequiresEditorsAuth:
op.Security = []map[string]spec.SecurityRequirement{
{"configEditor": {}},
}
case route.RequiresWriteAuth:
op.Security = []map[string]spec.SecurityRequirement{
{"writeAuth": {}},
}
}
switch route.ResponseType {
case plugins.HTTPRouteResponseTypeJSON:
op.Responses["200"] = spec.JSONResponse(nil).WithDescription("Successful execution with JSON object response")
case plugins.HTTPRouteResponseTypeMultiple:
op.Responses["200"] = (&spec.Response{}).WithDescription("Successful execution with variable response based on Accept header")
for _, a := range route.Accept {
op.Responses["200"].AddContent(a, &spec.MediaType{Schema: nil})
}
case plugins.HTTPRouteResponseTypeNo200:
// We don't add a 200 then
case plugins.HTTPRouteResponseTypeTextPlain:
op.Responses["200"] = spec.TextPlainResponse(nil).WithDescription("Successful execution with plain text response")
}
for _, param := range route.QueryParams {
var ps *spec.Schema
switch param.Type {
case "bool", "boolean":
ps = spec.BooleanProperty()
case "int", "int64":
ps = spec.Int64Property()
case "string":
ps = spec.StringProperty()
default:
log.WithFields(log.Fields{"module": route.Module, "type": param.Type}).Warn("Module registered unhandled query-param type")
ps = spec.StringProperty()
}
specParam := spec.QueryParam(param.Name, ps).
WithDescription(param.Description)
specParam.Required = param.Required
op.Parameters = append(
op.Parameters,
specParam,
)
}
switch route.Method {
case http.MethodDelete:
pi.Delete = op
case http.MethodGet:
pi.Get = op
case http.MethodPatch:
op.Responses["400"] = spec.RefResponse("inputErrorResponse")
pi.Patch = op
case http.MethodPost:
op.Responses["400"] = spec.RefResponse("inputErrorResponse")
pi.Post = op
case http.MethodPut:
op.Responses["400"] = spec.RefResponse("inputErrorResponse")
pi.Put = op
default:
return errors.Errorf("assignment for %q is not implemented", route.Method)
}
return nil
}