// 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. 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() for _, group := range f.api.EndpointGroups { // Not sure if this is a good name 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 {", group.Prefix, strings.ToUpper(f.api.Version)) f.pf("\tprivate readonly http: HttpClient = new HttpClient();") f.pf("\tprivate readonly ROOT_PATH: string = '/api/%s/%s';", f.api.Version, group.Prefix) for _, method := range group.endpoints { f.pf("") funcArgs, path := f.getArgsAndPath(method) returnStmt := "return" returnType := "void" if method.Response != nil { returnType = TypescriptTypeName(getElementaryType(reflect.TypeOf(method.Response))) if v := reflect.ValueOf(method.Response); v.Kind() == reflect.Array || v.Kind() == reflect.Slice { returnType = fmt.Sprintf("Array<%s>", returnType) } returnStmt += fmt.Sprintf(" response.json().then((body) => body as %s)", returnType) } returnStmt += ";" f.pf("\tpublic async %s(%s): Promise<%s> {", method.RequestName, funcArgs, returnType) f.pf("\t\tconst path = `%s`;", path) if method.Request != nil { f.pf("\t\tconst response = await this.http.%s(path, JSON.stringify(request));", strings.ToLower(method.Method)) } else { f.pf("\t\tconst response = await this.http.%s(path);", 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 Error(err.error);") f.pf("\t}") } f.pf("}") } func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (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 { t := getElementaryType(reflect.TypeOf(method.Request)) funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(t)) } for _, p := range method.PathParams { funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type)) path += fmt.Sprintf("/${%s}", p.Name) } for i, p := range method.QueryParams { if i == 0 { path += "?" } else { path += "&" } funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type)) path += fmt.Sprintf("%s=${%s}", p.Name, p.Name) } path = strings.ReplaceAll(path, "//", "/") return strings.Trim(funcArgs, ", "), path }