adcd810e37
The API generator doesn't have a way to customize each Go handler endpoint unless that the Go generator is modified. This commit adds a way to customize each endpoint injecting instances of types that implement an interface (Middleware) that return the code to inject. To show how it works, the commit get rid of the 2 fields that we used to customize the authentication request with the logic that the satellite/console/consoleweb/consoleapi needs and replace the hardcoded customization using this new way to customize handlers. This new way should allow to hook the satellite/admin/back-office authorization into the handlers using a Middleware implementation. Change-Id: I894aa0026b30fa2f4a5604a6c34c22e0ed582e2b
168 lines
4.2 KiB
Go
168 lines
4.2 KiB
Go
// Copyright (C) 2022 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package apigen
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/zeebo/errs"
|
|
)
|
|
|
|
// MustWriteTS writes generated TypeScript code into a file indicated by path.
|
|
// The generated code is an API client to run in the browser.
|
|
//
|
|
// If an error occurs, it panics.
|
|
func (a *API) MustWriteTS(path string) {
|
|
f := newTSGenFile(path, a)
|
|
|
|
f.generateTS()
|
|
|
|
err := f.write()
|
|
if err != nil {
|
|
panic(errs.Wrap(err))
|
|
}
|
|
}
|
|
|
|
type tsGenFile struct {
|
|
result string
|
|
path string
|
|
api *API
|
|
types Types
|
|
}
|
|
|
|
func newTSGenFile(filepath string, api *API) *tsGenFile {
|
|
return &tsGenFile{
|
|
path: filepath,
|
|
api: api,
|
|
types: NewTypes(),
|
|
}
|
|
}
|
|
|
|
func (f *tsGenFile) pf(format string, a ...interface{}) {
|
|
f.result += fmt.Sprintf(format+"\n", a...)
|
|
}
|
|
|
|
func (f *tsGenFile) write() error {
|
|
content := strings.ReplaceAll(f.result, "\t", " ")
|
|
return os.WriteFile(f.path, []byte(content), 0644)
|
|
}
|
|
|
|
func (f *tsGenFile) generateTS() {
|
|
f.pf("// AUTOGENERATED BY private/apigen")
|
|
f.pf("// DO NOT EDIT.")
|
|
f.pf("")
|
|
f.pf("import { HttpClient } from '@/utils/httpClient';")
|
|
|
|
f.registerTypes()
|
|
f.result += f.types.GenerateTypescriptDefinitions()
|
|
|
|
f.result += `
|
|
class APIError extends Error {
|
|
constructor(
|
|
public readonly msg: string,
|
|
public readonly responseStatusCode?: number,
|
|
) {
|
|
super(msg);
|
|
}
|
|
}
|
|
`
|
|
|
|
for _, group := range f.api.EndpointGroups {
|
|
f.createAPIClient(group)
|
|
}
|
|
}
|
|
|
|
func (f *tsGenFile) registerTypes() {
|
|
for _, group := range f.api.EndpointGroups {
|
|
for _, method := range group.endpoints {
|
|
if method.Request != nil {
|
|
f.types.Register(reflect.TypeOf(method.Request))
|
|
}
|
|
if method.Response != nil {
|
|
f.types.Register(reflect.TypeOf(method.Response))
|
|
}
|
|
if len(method.QueryParams) > 0 {
|
|
for _, p := range method.QueryParams {
|
|
t := getElementaryType(p.Type)
|
|
f.types.Register(t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
|
f.pf("\nexport class %sHttpApi%s {", capitalize(group.Name), strings.ToUpper(f.api.Version))
|
|
f.pf("\tprivate readonly http: HttpClient = new HttpClient();")
|
|
f.pf("\tprivate readonly ROOT_PATH: string = '%s/%s';", f.api.endpointBasePath(), strings.ToLower(group.Prefix))
|
|
for _, method := range group.endpoints {
|
|
f.pf("")
|
|
|
|
funcArgs, path := f.getArgsAndPath(method, group)
|
|
|
|
returnStmt := "return"
|
|
returnType := "void"
|
|
if method.Response != nil {
|
|
returnType = TypescriptTypeName(reflect.TypeOf(method.Response))
|
|
returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType)
|
|
}
|
|
returnStmt += ";"
|
|
|
|
f.pf("\tpublic async %s(%s): Promise<%s> {", method.TypeScriptName, funcArgs, returnType)
|
|
if len(method.QueryParams) > 0 {
|
|
f.pf("\t\tconst u = new URL(`%s`, window.location.href);", path)
|
|
for _, p := range method.QueryParams {
|
|
f.pf("\t\tu.searchParams.set('%s', %s);", p.Name, p.Name)
|
|
}
|
|
f.pf("\t\tconst fullPath = u.toString();")
|
|
} else {
|
|
f.pf("\t\tconst fullPath = `%s`;", path)
|
|
}
|
|
|
|
if method.Request != nil {
|
|
f.pf("\t\tconst response = await this.http.%s(fullPath, JSON.stringify(request));", strings.ToLower(method.Method))
|
|
} else {
|
|
f.pf("\t\tconst response = await this.http.%s(fullPath);", strings.ToLower(method.Method))
|
|
}
|
|
|
|
f.pf("\t\tif (response.ok) {")
|
|
f.pf("\t\t\t%s", returnStmt)
|
|
f.pf("\t\t}")
|
|
f.pf("\t\tconst err = await response.json();")
|
|
f.pf("\t\tthrow new APIError(err.error, response.status);")
|
|
f.pf("\t}")
|
|
}
|
|
f.pf("}")
|
|
}
|
|
|
|
func (f *tsGenFile) getArgsAndPath(method *FullEndpoint, group *EndpointGroup) (funcArgs, path string) {
|
|
// remove path parameter placeholders
|
|
path = method.Path
|
|
i := strings.Index(path, "{")
|
|
if i > -1 {
|
|
path = method.Path[:i]
|
|
}
|
|
path = "${this.ROOT_PATH}" + path
|
|
|
|
if method.Request != nil {
|
|
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(reflect.TypeOf(method.Request)))
|
|
}
|
|
|
|
for _, p := range method.PathParams {
|
|
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
|
path += fmt.Sprintf("/${%s}", p.Name)
|
|
}
|
|
|
|
for _, p := range method.QueryParams {
|
|
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
|
}
|
|
|
|
path = strings.ReplaceAll(path, "//", "/")
|
|
|
|
return strings.Trim(funcArgs, ", "), path
|
|
}
|