882c9d64e4
Create a class that inherits the Javascript Error class to use it when throwing errors on the TypeScript client when the ok property of the Fetch API Response object is false, which means that the server responded with a status code less than 200 or greater than 299. The APIError class contains apart of the message, the response status code which may be useful for the caller to decide what to do, for example, it may decide to retry the call on certain response status errors. Change-Id: Ic48466b40cbf134a27d5c92a4af9f93232e84fca
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"
|
|
"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`);", 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
|
|
}
|