satellite/console{gen}: GetUsersProjects endpoint

Initial implementation of auto-generated GetUsersProjects endpoint

Change-Id: If41bff2ea3ff9cfc87afeda9e5e5b3f586cbab33
This commit is contained in:
Vitalii Shpital 2022-01-11 15:20:02 +02:00
parent cf03209c16
commit 07c71e34c2
6 changed files with 320 additions and 15 deletions

View File

@ -0,0 +1,11 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package api
import "net/http"
// Auth exposes methods to control authentication process for each endpoint.
type Auth interface {
IsAuthenticated(r *http.Request) error
}

View File

@ -0,0 +1,49 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package api
import (
"encoding/json"
"net/http"
"go.uber.org/zap"
)
// HTTPError holds http error entity with http status and error itself.
type HTTPError struct {
Status int
Err error
}
// Error returns http error's string representation.
func (e HTTPError) Error() string {
return e.Err.Error()
}
// ServeError writes JSON error to response output stream.
func ServeError(log *zap.Logger, w http.ResponseWriter, status int, err error) {
msg := err.Error()
fields := []zap.Field{
zap.Int("code", status),
zap.String("message", msg),
zap.Error(err),
}
if status == http.StatusNoContent {
return
} else if status/100 == 5 { // Check for 5XX status.
log.Error("returning error to client", fields...)
} else {
log.Debug("returning error to client", fields...)
}
w.WriteHeader(status)
err = json.NewEncoder(w).Encode(map[string]string{
"error": msg,
})
if err != nil {
log.Debug("failed to write json error response", zap.Error(err))
}
}

View File

@ -3,21 +3,27 @@
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
}
// New creates new API with specific configuration.
func New(version, description string) *API {
return &API{
Version: version,
Description: description,
}
}
// Group adds new endpoints group to API.
func (a *API) Group(name, prefix string) *EndpointGroup {
group := &EndpointGroup{
@ -30,3 +36,169 @@ func (a *API) Group(name, prefix string) *EndpointGroup {
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+"api.gen.go", generated, 0755)
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)
if strings.Contains(responseType.String(), a.PackageName) {
p("%s(context.Context) (%s, api.HTTPError)", method.MethodName, responseType.Elem().Name())
} else {
p("%s(context.Context) (%s, api.HTTPError)", method.MethodName, responseType)
}
}
p("}")
p("")
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) *Handler {",
group.Name,
group.Name,
)
p("handler := &Handler{")
p("log: log,")
p("service: service,")
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
}

View File

@ -0,0 +1,66 @@
// AUTOGENERATED BY private/apigen
// DO NOT EDIT.
package consoleapi
import (
"context"
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/private/api"
"storj.io/storj/satellite/console"
)
var ErrProjectsAPI = errs.Class("consoleapi projects api")
type ProjectManagementService interface {
GetUserProjects(context.Context) ([]console.Project, api.HTTPError)
}
type Handler struct {
log *zap.Logger
service ProjectManagementService
auth api.Auth
}
func NewProjectManagement(log *zap.Logger, service ProjectManagementService, router *mux.Router) *Handler {
handler := &Handler{
log: log,
service: service,
}
projectsRouter := router.PathPrefix("/api/v0/projects").Subrouter()
projectsRouter.HandleFunc("/", handler.handleGetUserProjects).Methods("GET")
return handler
}
func (h *Handler) handleGetUserProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
err = h.auth.IsAuthenticated(r)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
retVal, httpErr := h.service.GetUserProjects(ctx)
if err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GetUserProjects response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}

View File

@ -11,16 +11,23 @@ import (
)
func main() {
api := apigen.New("v1", "")
a := &apigen.API{
Version: "v1",
Description: "",
PackageName: "consoleapi",
}
{
g := api.Group("Projects", "projects")
g := a.Group("ProjectManagement", "projects")
g.Get("/", &apigen.Endpoint{
Name: "List Projects",
Description: "Lists all projects user has",
MethodName: "ListUserProjects",
Name: "Get Projects",
Description: "Gets all projects user has",
MethodName: "GetUserProjects",
Response: []console.Project{},
})
}
a.MustWrite("satellite/console/consoleweb/consoleapi/")
}

View File

@ -81,8 +81,8 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
* Update project name and description.
*
* @param projectId - project ID
* @param name - project name
* @param description - project description
* @param projectFields - project fields
* @param projectLimits - project limits
* @returns Project[]
* @throws Error
*/