1343528a43
Currently the paths were set relative to the root of the module, however the code did not ensure that we are running relative to the module directory. Also, ensure typescript output corresponds to our styling. Change-Id: I2b3cbd4ea8f2615e35c7b58c6fb8851669c47885
290 lines
7.3 KiB
Go
290 lines
7.3 KiB
Go
// Copyright (C) 2022 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package apigen
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zeebo/errs"
|
|
)
|
|
|
|
// TODO maybe look at the Go structs MarshalJSON return type instead.
|
|
var tsTypeOverrides = map[string]string{
|
|
"uuid.UUID": "string",
|
|
"time.Time": "string",
|
|
"memory.Size": "string",
|
|
"[]uint8": "string", // e.g. []byte
|
|
}
|
|
|
|
// getBasicReflectType dereferences a pointer and gets the basic types from slices.
|
|
func getBasicReflectType(t reflect.Type) reflect.Type {
|
|
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
|
t = t.Elem()
|
|
}
|
|
return t
|
|
}
|
|
|
|
// tsType gets the corresponding typescript type for a provided reflect.Type.
|
|
// Input is expected to be a (non pointer) struct or primitive.
|
|
func tsType(t reflect.Type) string {
|
|
override := tsTypeOverrides[t.String()]
|
|
if len(override) > 0 {
|
|
return override
|
|
}
|
|
switch t.Kind() {
|
|
case reflect.Ptr:
|
|
return tsType(t.Elem())
|
|
case reflect.Slice, reflect.Array:
|
|
return tsType(t.Elem()) + "[]"
|
|
case reflect.String:
|
|
return "string"
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return "number"
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return "number"
|
|
case reflect.Float32, reflect.Float64:
|
|
return "number"
|
|
case reflect.Bool:
|
|
return "boolean"
|
|
case reflect.Struct:
|
|
return t.Name()
|
|
default:
|
|
panic("unhandled type: " + t.Name())
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// types is a map of struct types and their struct type dependencies.
|
|
// We use this to ensure all dependencies are written before their parent.
|
|
types map[reflect.Type][]reflect.Type
|
|
// typeList is a list of all struct types. We use this for sorting the types alphabetically
|
|
// to ensure that the diff is minimized when the file is regenerated.
|
|
typeList []reflect.Type
|
|
typesWritten map[reflect.Type]bool
|
|
api *API
|
|
}
|
|
|
|
func newTSGenFile(filepath string, api *API) *tsGenFile {
|
|
f := &tsGenFile{
|
|
path: filepath,
|
|
types: make(map[reflect.Type][]reflect.Type),
|
|
typesWritten: make(map[reflect.Type]bool),
|
|
api: api,
|
|
}
|
|
|
|
f.pf("// AUTOGENERATED BY private/apigen")
|
|
f.pf("// DO NOT EDIT.")
|
|
f.pf("")
|
|
f.pf("import { HttpClient } from '@/utils/httpClient'")
|
|
f.pf("")
|
|
|
|
return f
|
|
}
|
|
|
|
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) getStructsFromType(t reflect.Type) {
|
|
t = getBasicReflectType(t)
|
|
override := tsTypeOverrides[t.String()]
|
|
if len(override) > 0 {
|
|
return
|
|
}
|
|
|
|
// if it is a struct, get any types needed from the fields
|
|
if t.Kind() == reflect.Struct {
|
|
if _, ok := f.types[t]; !ok {
|
|
f.types[t] = []reflect.Type{}
|
|
f.typeList = append(f.typeList, t)
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
f.getStructsFromType(field.Type)
|
|
|
|
if field.Type.Kind() == reflect.Struct {
|
|
deps := f.types[t]
|
|
deps = append(deps, getBasicReflectType(field.Type))
|
|
f.types[t] = deps
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (f *tsGenFile) generateTS() {
|
|
f.createClasses()
|
|
|
|
for _, group := range f.api.EndpointGroups {
|
|
// Not sure if this is a good name
|
|
f.createAPIClient(group)
|
|
}
|
|
}
|
|
|
|
func (f *tsGenFile) emitStruct(t reflect.Type) {
|
|
override := tsTypeOverrides[t.String()]
|
|
if len(override) > 0 {
|
|
return
|
|
}
|
|
if f.typesWritten[t] {
|
|
return
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
// TODO: handle slices
|
|
// I'm not sure this is necessary. If it's not a struct then we don't need to create a TS class for it.
|
|
// We just use a JS array of the class.
|
|
return
|
|
}
|
|
|
|
for _, d := range f.types[t] {
|
|
if f.typesWritten[d] {
|
|
continue
|
|
}
|
|
f.emitStruct(d)
|
|
}
|
|
|
|
f.pf("class %s {", t.Name())
|
|
defer f.pf("}\n")
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
attributes := strings.Fields(field.Tag.Get("json"))
|
|
if len(attributes) == 0 || attributes[0] == "" {
|
|
panic(t.Name() + " missing json declaration")
|
|
}
|
|
if attributes[0] == "-" {
|
|
continue
|
|
}
|
|
f.pf("\t%s: %s;", attributes[0], tsType(field.Type))
|
|
}
|
|
|
|
f.typesWritten[t] = true
|
|
}
|
|
|
|
func (f *tsGenFile) createClasses() {
|
|
for _, group := range f.api.EndpointGroups {
|
|
for _, method := range group.endpoints {
|
|
if method.Request != nil {
|
|
reqType := reflect.TypeOf(method.Request)
|
|
f.getStructsFromType(reqType)
|
|
}
|
|
if method.Response != nil {
|
|
resType := reflect.TypeOf(method.Response)
|
|
f.getStructsFromType(resType)
|
|
}
|
|
if len(method.QueryParams) > 0 {
|
|
for _, p := range method.QueryParams {
|
|
t := getBasicReflectType(p.Type)
|
|
f.getStructsFromType(t)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(f.typeList, func(i, j int) bool {
|
|
return strings.Compare(f.typeList[i].Name(), f.typeList[j].Name()) < 0
|
|
})
|
|
|
|
for _, t := range f.typeList {
|
|
f.emitStruct(t)
|
|
}
|
|
}
|
|
|
|
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
|
f.pf("export 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)
|
|
f.pf("")
|
|
for _, method := range group.endpoints {
|
|
funcArgs, path := f.getArgsAndPath(method)
|
|
|
|
returnStmt := "return"
|
|
returnType := "void"
|
|
if method.Response != nil {
|
|
returnType = tsType(getBasicReflectType(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}\n")
|
|
}
|
|
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 := getBasicReflectType(reflect.TypeOf(method.Request))
|
|
funcArgs += fmt.Sprintf("request: %s, ", tsType(t))
|
|
}
|
|
|
|
for _, p := range method.PathParams {
|
|
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, tsType(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, tsType(p.Type))
|
|
path += fmt.Sprintf("%s=${%s}", p.Name, p.Name)
|
|
}
|
|
|
|
path = strings.ReplaceAll(path, "//", "/")
|
|
|
|
return strings.Trim(funcArgs, ", "), path
|
|
}
|