storj/private/apigen/api.go
Vitalii Shpital ba6956db0f console/server, apigen: feature flag for new generated console api
Added a feture flag which will be used to indicate if new generated console api is used.
Fixed some comments from previous PR.

Change-Id: Ice31c998b0b347028a491c971a648fd1269bfd49
2022-02-28 23:00:12 +00:00

214 lines
5.0 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package apigen
import (
"fmt"
"go/format"
"os"
"reflect"
"strings"
"github.com/zeebo/errs"
"storj.io/storj/private/api"
)
// API represents specific API's configuration.
type API struct {
Version string
Description string
PackageName string
Auth api.Auth
EndpointGroups []*EndpointGroup
}
// Group adds new endpoints group to API.
func (a *API) Group(name, prefix string) *EndpointGroup {
group := &EndpointGroup{
Name: name,
Prefix: prefix,
Endpoints: make(map[PathMethod]*Endpoint),
}
a.EndpointGroups = append(a.EndpointGroups, group)
return group
}
// MustWrite writes generated code into a file.
func (a *API) MustWrite(path string) {
generated, err := a.generateGo()
if err != nil {
panic(errs.Wrap(err))
}
err = os.WriteFile(path, generated, 0644)
if err != nil {
panic(errs.Wrap(err))
}
}
// generateGo generates api code and returns an output.
func (a *API) generateGo() ([]byte, error) {
var result string
p := func(format string, a ...interface{}) {
result += fmt.Sprintf(format+"\n", a...)
}
getPackageName := func(path string) string {
pathPackages := strings.Split(path, "/")
return pathPackages[len(pathPackages)-1]
}
p("// AUTOGENERATED BY private/apigen")
p("// DO NOT EDIT.")
p("")
p("package %s", a.PackageName)
p("")
p("import (")
p(`"context"`)
p(`"encoding/json"`)
p(`"net/http"`)
p("")
p(`"github.com/gorilla/mux"`)
p(`"github.com/zeebo/errs"`)
p(`"go.uber.org/zap"`)
p("")
p(`"storj.io/storj/private/api"`)
for _, group := range a.EndpointGroups {
for _, method := range group.Endpoints {
if method.Request != nil {
path := reflect.TypeOf(method.Request).Elem().PkgPath()
pn := getPackageName(path)
if pn == a.PackageName {
continue
}
p(`"%s"`, path)
}
if method.Response != nil {
path := reflect.TypeOf(method.Response).Elem().PkgPath()
pn := getPackageName(path)
if pn == a.PackageName {
continue
}
p(`"%s"`, path)
}
}
p(")")
p("")
p("var Err%sAPI = errs.Class(\"%s %s api\")", strings.Title(group.Prefix), a.PackageName, group.Prefix)
p("")
p("type %sService interface {", group.Name)
for _, method := range group.Endpoints {
responseType := reflect.TypeOf(method.Response)
p("%s(context.Context) (%s, api.HTTPError)", method.MethodName, a.handleTypesPackage(responseType))
}
p("}")
p("")
p("// Handler is an api handler that exposes all %s related functionality.", group.Prefix)
p("type Handler struct {")
p("log *zap.Logger")
p("service %sService", group.Name)
p("auth api.Auth")
p("}")
p("")
p(
"func New%s(log *zap.Logger, service %sService, router *mux.Router, auth api.Auth) *Handler {",
group.Name,
group.Name,
)
p("handler := &Handler{")
p("log: log,")
p("service: service,")
p("auth: auth,")
p("}")
p("")
p("%sRouter := router.PathPrefix(\"/api/v0/%s\").Subrouter()", group.Prefix, group.Prefix)
for pathMethod, endpoint := range group.Endpoints {
handlerName := "handle" + endpoint.MethodName
p("%sRouter.HandleFunc(\"%s\", handler.%s).Methods(\"%s\")", group.Prefix, pathMethod.Path, handlerName, pathMethod.Method)
}
p("")
p("return handler")
p("}")
for _, endpoint := range group.Endpoints {
p("")
handlerName := "handle" + endpoint.MethodName
p("func (h *Handler) %s(w http.ResponseWriter, r *http.Request) {", handlerName)
p("ctx := r.Context()")
p("var err error")
p("defer mon.Task()(&ctx)(&err)")
p("")
p("w.Header().Set(\"Content-Type\", \"application/json\")")
p("")
if !endpoint.NoCookieAuth {
p("err = h.auth.IsAuthenticated(r)")
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusUnauthorized, err)")
p("return")
p("}")
p("")
}
methodFormat := "retVal, httpErr := h.service.%s(ctx"
args := []string{endpoint.MethodName}
// TODO to be implemented
// if !endpoint.NoAPIAuth {}
methodFormat += ")"
interfaceArgs := make([]interface{}, len(args))
for i, v := range args {
interfaceArgs[i] = v
}
p(methodFormat, interfaceArgs...)
p("if err != nil {")
p("api.ServeError(h.log, w, httpErr.Status, httpErr.Err)")
p("return")
p("}")
p("")
p("err = json.NewEncoder(w).Encode(retVal)")
p("if err != nil {")
p("h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", endpoint.MethodName, strings.Title(group.Prefix))
p("}")
p("}")
p("")
}
}
output, err := format.Source([]byte(result))
if err != nil {
return nil, err
}
return output, nil
}
// handleTypesPackage handles the way some type is used in generated code.
// If type is from the same package then we use only type's name.
// If type is from external package then we use type along with it's appropriate package name.
func (a *API) handleTypesPackage(t reflect.Type) interface{} {
if strings.HasPrefix(t.String(), a.PackageName) {
return t.Elem().Name()
}
return t
}