storj/private/apigen/tsgen.go
Jeremy Wharton 032faefa4b private/apigen: fix URL construction in generated TypeScript code
This change fixes an incorrect invocation of the URL object constructor
in generated TypeScript HTTP clients.

Change-Id: I9011bc535f2096374d20b74b401d4cc38a0451fb
2023-11-16 01:12:58 +00:00

168 lines
4.3 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package apigen
import (
"fmt"
"os"
"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(method.requestType(group))
}
if method.Response != nil {
f.types.Register(method.responseType(group))
}
if len(method.QueryParams) > 0 {
for _, p := range method.QueryParams {
t := getElementaryType(p.namedType(method.Endpoint, "query"))
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 {
respType := method.responseType(group)
returnType = TypescriptTypeName(respType)
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(method.requestType(group)))
}
for _, p := range method.PathParams {
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "path")))
path += fmt.Sprintf("/${%s}", p.Name)
}
for _, p := range method.QueryParams {
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.namedType(method.Endpoint, "query")))
}
path = strings.ReplaceAll(path, "//", "/")
return strings.Trim(funcArgs, ", "), path
}