private/apigen: isolate TypeScript class generation
The code responsible for generating TypeScript classes has been separated from the rest of the TypeScript generation code so that other packages may take advantage of this functionality. References #5494 Change-Id: I97eabd430bd6a5f748eafaf8b1d783977e75e660
This commit is contained in:
parent
4e94e6188c
commit
4d823e8166
@ -4,6 +4,10 @@
|
|||||||
package apigen
|
package apigen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"storj.io/storj/private/api"
|
"storj.io/storj/private/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,3 +31,42 @@ func (a *API) Group(name, prefix string) *EndpointGroup {
|
|||||||
|
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringBuilder is an extension of strings.Builder that allows for writing formatted lines.
|
||||||
|
type StringBuilder struct{ strings.Builder }
|
||||||
|
|
||||||
|
// Writelnf formats arguments according to a format specifier
|
||||||
|
// and appends the resulting string to the StringBuilder's buffer.
|
||||||
|
func (s *StringBuilder) Writelnf(format string, a ...interface{}) {
|
||||||
|
s.WriteString(fmt.Sprintf(format+"\n", a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getElementaryType simplifies a Go type.
|
||||||
|
func getElementaryType(t reflect.Type) reflect.Type {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Array, reflect.Chan, reflect.Ptr, reflect.Slice:
|
||||||
|
return getElementaryType(t.Elem())
|
||||||
|
default:
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter returns a new slice of reflect.Type values that satisfy the given keep function.
|
||||||
|
func filter(types []reflect.Type, keep func(reflect.Type) bool) []reflect.Type {
|
||||||
|
filtered := make([]reflect.Type, 0, len(types))
|
||||||
|
for _, t := range types {
|
||||||
|
if keep(t) {
|
||||||
|
filtered = append(filtered, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNillableType returns whether instances of the given type can be nil.
|
||||||
|
func isNillableType(t reflect.Type) bool {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Chan, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package apigen
|
package apigen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"go/format"
|
"go/format"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -37,11 +36,8 @@ func (a *API) MustWriteGo(path string) {
|
|||||||
|
|
||||||
// generateGo generates api code and returns an output.
|
// generateGo generates api code and returns an output.
|
||||||
func (a *API) generateGo() ([]byte, error) {
|
func (a *API) generateGo() ([]byte, error) {
|
||||||
var result string
|
result := &StringBuilder{}
|
||||||
|
pf := result.Writelnf
|
||||||
pf := func(format string, a ...interface{}) {
|
|
||||||
result += fmt.Sprintf(format+"\n", a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
getPackageName := func(path string) string {
|
getPackageName := func(path string) string {
|
||||||
pathPackages := strings.Split(path, "/")
|
pathPackages := strings.Split(path, "/")
|
||||||
@ -81,13 +77,23 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var getTypePackages func(t reflect.Type) []string
|
||||||
|
getTypePackages = func(t reflect.Type) []string {
|
||||||
|
t = getElementaryType(t)
|
||||||
|
if t.Kind() == reflect.Map {
|
||||||
|
pkgs := []string{getElementaryType(t.Key()).PkgPath()}
|
||||||
|
return append(pkgs, getTypePackages(t.Elem())...)
|
||||||
|
}
|
||||||
|
return []string{t.PkgPath()}
|
||||||
|
}
|
||||||
|
|
||||||
for _, group := range a.EndpointGroups {
|
for _, group := range a.EndpointGroups {
|
||||||
for _, method := range group.endpoints {
|
for _, method := range group.endpoints {
|
||||||
if method.Request != nil {
|
if method.Request != nil {
|
||||||
i(getElementaryType(reflect.TypeOf(method.Request)).PkgPath())
|
i(getTypePackages(reflect.TypeOf(method.Request))...)
|
||||||
}
|
}
|
||||||
if method.Response != nil {
|
if method.Response != nil {
|
||||||
i(getElementaryType(reflect.TypeOf(method.Response)).PkgPath())
|
i(getTypePackages(reflect.TypeOf(method.Response))...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,7 +128,7 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
if e.Response != nil {
|
if e.Response != nil {
|
||||||
responseType := reflect.TypeOf(e.Response)
|
responseType := reflect.TypeOf(e.Response)
|
||||||
returnParam := a.handleTypesPackage(responseType)
|
returnParam := a.handleTypesPackage(responseType)
|
||||||
if responseType == getElementaryType(responseType) {
|
if !isNillableType(responseType) {
|
||||||
returnParam = "*" + returnParam
|
returnParam = "*" + returnParam
|
||||||
}
|
}
|
||||||
pf("%s(ctx context.Context, "+paramStr+") (%s, api.HTTPError)", e.MethodName, returnParam)
|
pf("%s(ctx context.Context, "+paramStr+") (%s, api.HTTPError)", e.MethodName, returnParam)
|
||||||
@ -186,7 +192,7 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
pf("w.Header().Set(\"Content-Type\", \"application/json\")")
|
pf("w.Header().Set(\"Content-Type\", \"application/json\")")
|
||||||
pf("")
|
pf("")
|
||||||
|
|
||||||
if err := handleParams(pf, i, endpoint.PathParams, endpoint.QueryParams); err != nil {
|
if err := handleParams(result, i, endpoint.PathParams, endpoint.QueryParams); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,8 +248,9 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBody := result
|
fileBody := result.String()
|
||||||
result = ""
|
result = &StringBuilder{}
|
||||||
|
pf = result.Writelnf
|
||||||
|
|
||||||
pf("// AUTOGENERATED BY private/apigen")
|
pf("// AUTOGENERATED BY private/apigen")
|
||||||
pf("// DO NOT EDIT.")
|
pf("// DO NOT EDIT.")
|
||||||
@ -271,9 +278,9 @@ func (a *API) generateGo() ([]byte, error) {
|
|||||||
pf("")
|
pf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
result += fileBody
|
result.WriteString(fileBody)
|
||||||
|
|
||||||
output, err := format.Source([]byte(result))
|
output, err := format.Source([]byte(result.String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -293,7 +300,8 @@ func (a *API) handleTypesPackage(t reflect.Type) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleParams handles parsing of URL path parameters or query parameters.
|
// handleParams handles parsing of URL path parameters or query parameters.
|
||||||
func handleParams(pf func(format string, a ...interface{}), i func(paths ...string), pathParams, queryParams []Param) error {
|
func handleParams(builder *StringBuilder, i func(paths ...string), pathParams, queryParams []Param) error {
|
||||||
|
pf := builder.Writelnf
|
||||||
pErrCheck := func() {
|
pErrCheck := func() {
|
||||||
pf("if err != nil {")
|
pf("if err != nil {")
|
||||||
pf("api.ServeError(h.log, w, http.StatusBadRequest, err)")
|
pf("api.ServeError(h.log, w, http.StatusBadRequest, err)")
|
||||||
@ -373,13 +381,3 @@ func handleBody(pf func(format string, a ...interface{}), body interface{}) {
|
|||||||
pf("}")
|
pf("}")
|
||||||
pf("")
|
pf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getElementaryType simplifies a Go type.
|
|
||||||
func getElementaryType(t reflect.Type) reflect.Type {
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
||||||
return getElementaryType(t.Elem())
|
|
||||||
default:
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,57 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zeebo/errs"
|
"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.
|
// MustWriteTS writes generated TypeScript code into a file.
|
||||||
func (a *API) MustWriteTS(path string) {
|
func (a *API) MustWriteTS(path string) {
|
||||||
f := newTSGenFile(path, a)
|
f := newTSGenFile(path, a)
|
||||||
@ -73,31 +27,16 @@ func (a *API) MustWriteTS(path string) {
|
|||||||
type tsGenFile struct {
|
type tsGenFile struct {
|
||||||
result string
|
result string
|
||||||
path string
|
path string
|
||||||
// types is a map of struct types and their struct type dependencies.
|
api *API
|
||||||
// We use this to ensure all dependencies are written before their parent.
|
types Types
|
||||||
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 {
|
func newTSGenFile(filepath string, api *API) *tsGenFile {
|
||||||
f := &tsGenFile{
|
return &tsGenFile{
|
||||||
path: filepath,
|
path: filepath,
|
||||||
types: make(map[reflect.Type][]reflect.Type),
|
api: api,
|
||||||
typesWritten: make(map[reflect.Type]bool),
|
types: NewTypes(),
|
||||||
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{}) {
|
func (f *tsGenFile) pf(format string, a ...interface{}) {
|
||||||
@ -109,36 +48,14 @@ func (f *tsGenFile) write() error {
|
|||||||
return os.WriteFile(f.path, []byte(content), 0644)
|
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() {
|
func (f *tsGenFile) generateTS() {
|
||||||
f.createClasses()
|
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 {
|
for _, group := range f.api.EndpointGroups {
|
||||||
// Not sure if this is a good name
|
// Not sure if this is a good name
|
||||||
@ -146,87 +63,38 @@ func (f *tsGenFile) generateTS() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *tsGenFile) emitStruct(t reflect.Type) {
|
func (f *tsGenFile) registerTypes() {
|
||||||
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 _, group := range f.api.EndpointGroups {
|
||||||
for _, method := range group.endpoints {
|
for _, method := range group.endpoints {
|
||||||
if method.Request != nil {
|
if method.Request != nil {
|
||||||
reqType := reflect.TypeOf(method.Request)
|
f.types.Register(reflect.TypeOf(method.Request))
|
||||||
f.getStructsFromType(reqType)
|
|
||||||
}
|
}
|
||||||
if method.Response != nil {
|
if method.Response != nil {
|
||||||
resType := reflect.TypeOf(method.Response)
|
f.types.Register(reflect.TypeOf(method.Response))
|
||||||
f.getStructsFromType(resType)
|
|
||||||
}
|
}
|
||||||
if len(method.QueryParams) > 0 {
|
if len(method.QueryParams) > 0 {
|
||||||
for _, p := range method.QueryParams {
|
for _, p := range method.QueryParams {
|
||||||
t := getBasicReflectType(p.Type)
|
t := getElementaryType(p.Type)
|
||||||
f.getStructsFromType(t)
|
f.types.Register(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) {
|
func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
||||||
f.pf("export class %sHttpApi%s {", group.Prefix, strings.ToUpper(f.api.Version))
|
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 http: HttpClient = new HttpClient();")
|
||||||
f.pf("\tprivate readonly ROOT_PATH: string = '/api/%s/%s';", f.api.Version, group.Prefix)
|
f.pf("\tprivate readonly ROOT_PATH: string = '/api/%s/%s';", f.api.Version, group.Prefix)
|
||||||
f.pf("")
|
|
||||||
for _, method := range group.endpoints {
|
for _, method := range group.endpoints {
|
||||||
|
f.pf("")
|
||||||
|
|
||||||
funcArgs, path := f.getArgsAndPath(method)
|
funcArgs, path := f.getArgsAndPath(method)
|
||||||
|
|
||||||
returnStmt := "return"
|
returnStmt := "return"
|
||||||
returnType := "void"
|
returnType := "void"
|
||||||
if method.Response != nil {
|
if method.Response != nil {
|
||||||
returnType = tsType(getBasicReflectType(reflect.TypeOf(method.Response)))
|
returnType = TypescriptTypeName(getElementaryType(reflect.TypeOf(method.Response)))
|
||||||
if v := reflect.ValueOf(method.Response); v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
|
if v := reflect.ValueOf(method.Response); v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
|
||||||
returnType = fmt.Sprintf("Array<%s>", returnType)
|
returnType = fmt.Sprintf("Array<%s>", returnType)
|
||||||
}
|
}
|
||||||
@ -246,9 +114,9 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
|||||||
f.pf("\t\tif (response.ok) {")
|
f.pf("\t\tif (response.ok) {")
|
||||||
f.pf("\t\t\t%s", returnStmt)
|
f.pf("\t\t\t%s", returnStmt)
|
||||||
f.pf("\t\t}")
|
f.pf("\t\t}")
|
||||||
f.pf("\t\tconst err = await response.json()")
|
f.pf("\t\tconst err = await response.json();")
|
||||||
f.pf("\t\tthrow new Error(err.error)")
|
f.pf("\t\tthrow new Error(err.error);")
|
||||||
f.pf("\t}\n")
|
f.pf("\t}")
|
||||||
}
|
}
|
||||||
f.pf("}")
|
f.pf("}")
|
||||||
}
|
}
|
||||||
@ -263,12 +131,12 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string)
|
|||||||
path = "${this.ROOT_PATH}" + path
|
path = "${this.ROOT_PATH}" + path
|
||||||
|
|
||||||
if method.Request != nil {
|
if method.Request != nil {
|
||||||
t := getBasicReflectType(reflect.TypeOf(method.Request))
|
t := getElementaryType(reflect.TypeOf(method.Request))
|
||||||
funcArgs += fmt.Sprintf("request: %s, ", tsType(t))
|
funcArgs += fmt.Sprintf("request: %s, ", TypescriptTypeName(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range method.PathParams {
|
for _, p := range method.PathParams {
|
||||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, tsType(p.Type))
|
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
||||||
path += fmt.Sprintf("/${%s}", p.Name)
|
path += fmt.Sprintf("/${%s}", p.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +147,7 @@ func (f *tsGenFile) getArgsAndPath(method *fullEndpoint) (funcArgs, path string)
|
|||||||
path += "&"
|
path += "&"
|
||||||
}
|
}
|
||||||
|
|
||||||
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, tsType(p.Type))
|
funcArgs += fmt.Sprintf("%s: %s, ", p.Name, TypescriptTypeName(p.Type))
|
||||||
path += fmt.Sprintf("%s=${%s}", p.Name, p.Name)
|
path += fmt.Sprintf("%s=${%s}", p.Name, p.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
189
private/apigen/tstypes.go
Normal file
189
private/apigen/tstypes.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package apigen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"storj.io/common/memory"
|
||||||
|
"storj.io/common/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// commonPath is the path to the TypeScript module that common classes are imported from.
|
||||||
|
const commonPath = "@/types/common"
|
||||||
|
|
||||||
|
// commonClasses is a mapping of Go types to their corresponding TypeScript class names.
|
||||||
|
var commonClasses = map[reflect.Type]string{
|
||||||
|
reflect.TypeOf(memory.Size(0)): "MemorySize",
|
||||||
|
reflect.TypeOf(time.Time{}): "Time",
|
||||||
|
reflect.TypeOf(uuid.UUID{}): "UUID",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTypes creates a new type definition generator.
|
||||||
|
func NewTypes() Types {
|
||||||
|
return Types{top: make(map[reflect.Type]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types handles generating definitions from types.
|
||||||
|
type Types struct {
|
||||||
|
top map[reflect.Type]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers a type for generation.
|
||||||
|
func (types *Types) Register(t reflect.Type) {
|
||||||
|
types.top[t] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns a slice containing every top-level type and their dependencies.
|
||||||
|
func (types *Types) All() []reflect.Type {
|
||||||
|
seen := map[reflect.Type]struct{}{}
|
||||||
|
all := []reflect.Type{}
|
||||||
|
|
||||||
|
var walk func(t reflect.Type)
|
||||||
|
walk = func(t reflect.Type) {
|
||||||
|
if _, ok := seen[t]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[t] = struct{}{}
|
||||||
|
all = append(all, t)
|
||||||
|
|
||||||
|
if _, ok := commonClasses[t]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Array, reflect.Ptr, reflect.Slice:
|
||||||
|
walk(t.Elem())
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
walk(t.Field(i).Type)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
case reflect.String:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("type '%s' is not supported", t.Kind().String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for t := range types.top {
|
||||||
|
walk(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(all, func(i, j int) bool {
|
||||||
|
return strings.Compare(all[i].Name(), all[j].Name()) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateTypescriptDefinitions returns the TypeScript class definitions corresponding to the registered Go types.
|
||||||
|
func (types *Types) GenerateTypescriptDefinitions() string {
|
||||||
|
var out StringBuilder
|
||||||
|
pf := out.Writelnf
|
||||||
|
|
||||||
|
pf(types.getTypescriptImports())
|
||||||
|
|
||||||
|
all := filter(types.All(), func(t reflect.Type) bool {
|
||||||
|
if _, ok := commonClasses[t]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t.Kind() == reflect.Struct
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, t := range all {
|
||||||
|
func() {
|
||||||
|
pf("\nexport class %s {", t.Name())
|
||||||
|
defer pf("}")
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
attributes := strings.Fields(field.Tag.Get("json"))
|
||||||
|
if len(attributes) == 0 || attributes[0] == "" {
|
||||||
|
pathParts := strings.Split(t.PkgPath(), "/")
|
||||||
|
pkg := pathParts[len(pathParts)-1]
|
||||||
|
panic(fmt.Sprintf("(%s.%s).%s missing json declaration", pkg, t.Name(), field.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonField := attributes[0]
|
||||||
|
if jsonField == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isOptional := ""
|
||||||
|
if isNillableType(t) {
|
||||||
|
isOptional = "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTypescriptImports returns the TypeScript import directive for the registered Go types.
|
||||||
|
func (types *Types) getTypescriptImports() string {
|
||||||
|
classes := []string{}
|
||||||
|
|
||||||
|
all := types.All()
|
||||||
|
for _, t := range all {
|
||||||
|
if tsClass, ok := commonClasses[t]; ok {
|
||||||
|
classes = append(classes, tsClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(classes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(classes, func(i, j int) bool {
|
||||||
|
return strings.Compare(classes[i], classes[j]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return fmt.Sprintf("import { %s } from '%s';", strings.Join(classes, ", "), commonPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypescriptTypeName gets the corresponding TypeScript type for a provided reflect.Type.
|
||||||
|
func TypescriptTypeName(t reflect.Type) string {
|
||||||
|
if override, ok := commonClasses[t]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return TypescriptTypeName(t.Elem())
|
||||||
|
case reflect.Slice:
|
||||||
|
// []byte ([]uint8) is marshaled as a base64 string
|
||||||
|
elem := t.Elem()
|
||||||
|
if elem.Kind() == reflect.Uint8 {
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
return TypescriptTypeName(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())
|
||||||
|
}
|
||||||
|
}
|
@ -2,16 +2,17 @@
|
|||||||
// DO NOT EDIT.
|
// DO NOT EDIT.
|
||||||
|
|
||||||
import { HttpClient } from '@/utils/httpClient';
|
import { HttpClient } from '@/utils/httpClient';
|
||||||
|
import { MemorySize, Time, UUID } from '@/types/common';
|
||||||
|
|
||||||
class APIKeyInfo {
|
export class APIKeyInfo {
|
||||||
id: string;
|
id: UUID;
|
||||||
projectId: string;
|
projectId: UUID;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
name: string;
|
name: string;
|
||||||
createdAt: string;
|
createdAt: Time;
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIKeyPage {
|
export class APIKeyPage {
|
||||||
apiKeys: APIKeyInfo[];
|
apiKeys: APIKeyInfo[];
|
||||||
search: string;
|
search: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
@ -23,8 +24,8 @@ class APIKeyPage {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BucketUsageRollup {
|
export class BucketUsageRollup {
|
||||||
projectID: string;
|
projectID: UUID;
|
||||||
bucketName: string;
|
bucketName: string;
|
||||||
totalStoredData: number;
|
totalStoredData: number;
|
||||||
totalSegments: number;
|
totalSegments: number;
|
||||||
@ -33,49 +34,49 @@ class BucketUsageRollup {
|
|||||||
repairEgress: number;
|
repairEgress: number;
|
||||||
getEgress: number;
|
getEgress: number;
|
||||||
auditEgress: number;
|
auditEgress: number;
|
||||||
since: string;
|
since: Time;
|
||||||
before: string;
|
before: Time;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateAPIKeyRequest {
|
export class CreateAPIKeyRequest {
|
||||||
projectID: string;
|
projectID: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateAPIKeyResponse {
|
export class CreateAPIKeyResponse {
|
||||||
key: string;
|
key: string;
|
||||||
keyInfo: APIKeyInfo;
|
keyInfo: APIKeyInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Project {
|
export class Project {
|
||||||
id: string;
|
id: UUID;
|
||||||
publicId: string;
|
publicId: UUID;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
ownerId: string;
|
ownerId: UUID;
|
||||||
rateLimit: number;
|
rateLimit: number;
|
||||||
burstLimit: number;
|
burstLimit: number;
|
||||||
maxBuckets: number;
|
maxBuckets: number;
|
||||||
createdAt: string;
|
createdAt: Time;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
storageLimit: string;
|
storageLimit: MemorySize;
|
||||||
bandwidthLimit: string;
|
bandwidthLimit: MemorySize;
|
||||||
userSpecifiedStorageLimit: string;
|
userSpecifiedStorageLimit: MemorySize;
|
||||||
userSpecifiedBandwidthLimit: string;
|
userSpecifiedBandwidthLimit: MemorySize;
|
||||||
segmentLimit: number;
|
segmentLimit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProjectInfo {
|
export class ProjectInfo {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
storageLimit: string;
|
storageLimit: MemorySize;
|
||||||
bandwidthLimit: string;
|
bandwidthLimit: MemorySize;
|
||||||
createdAt: string;
|
createdAt: Time;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResponseUser {
|
export class ResponseUser {
|
||||||
id: string;
|
id: UUID;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
shortName: string;
|
shortName: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -105,7 +106,7 @@ export class projectsHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateProject(request: ProjectInfo, id: string): Promise<Project> {
|
public async updateProject(request: ProjectInfo, id: UUID): Promise<Project> {
|
||||||
const path = `${this.ROOT_PATH}/update/${id}`;
|
const path = `${this.ROOT_PATH}/update/${id}`;
|
||||||
const response = await this.http.patch(path, JSON.stringify(request));
|
const response = await this.http.patch(path, JSON.stringify(request));
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -115,7 +116,7 @@ export class projectsHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteProject(id: string): Promise<void> {
|
public async deleteProject(id: UUID): Promise<void> {
|
||||||
const path = `${this.ROOT_PATH}/delete/${id}`;
|
const path = `${this.ROOT_PATH}/delete/${id}`;
|
||||||
const response = await this.http.delete(path);
|
const response = await this.http.delete(path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -135,7 +136,7 @@ export class projectsHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBucketRollup(projectID: string, bucket: string, since: string, before: string): Promise<BucketUsageRollup> {
|
public async getBucketRollup(projectID: UUID, bucket: string, since: Time, before: Time): Promise<BucketUsageRollup> {
|
||||||
const path = `${this.ROOT_PATH}/bucket-rollup?projectID=${projectID}&bucket=${bucket}&since=${since}&before=${before}`;
|
const path = `${this.ROOT_PATH}/bucket-rollup?projectID=${projectID}&bucket=${bucket}&since=${since}&before=${before}`;
|
||||||
const response = await this.http.get(path);
|
const response = await this.http.get(path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -145,7 +146,7 @@ export class projectsHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBucketRollups(projectID: string, since: string, before: string): Promise<Array<BucketUsageRollup>> {
|
public async getBucketRollups(projectID: UUID, since: Time, before: Time): Promise<Array<BucketUsageRollup>> {
|
||||||
const path = `${this.ROOT_PATH}/bucket-rollups?projectID=${projectID}&since=${since}&before=${before}`;
|
const path = `${this.ROOT_PATH}/bucket-rollups?projectID=${projectID}&since=${since}&before=${before}`;
|
||||||
const response = await this.http.get(path);
|
const response = await this.http.get(path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -155,7 +156,7 @@ export class projectsHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAPIKeys(projectID: string, search: string, limit: number, page: number, order: number, orderDirection: number): Promise<APIKeyPage> {
|
public async getAPIKeys(projectID: UUID, search: string, limit: number, page: number, order: number, orderDirection: number): Promise<APIKeyPage> {
|
||||||
const path = `${this.ROOT_PATH}/apikeys/${projectID}?search=${search}&limit=${limit}&page=${page}&order=${order}&orderDirection=${orderDirection}`;
|
const path = `${this.ROOT_PATH}/apikeys/${projectID}?search=${search}&limit=${limit}&page=${page}&order=${order}&orderDirection=${orderDirection}`;
|
||||||
const response = await this.http.get(path);
|
const response = await this.http.get(path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -164,8 +165,8 @@ export class projectsHttpApiV0 {
|
|||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class apikeysHttpApiV0 {
|
export class apikeysHttpApiV0 {
|
||||||
private readonly http: HttpClient = new HttpClient();
|
private readonly http: HttpClient = new HttpClient();
|
||||||
private readonly ROOT_PATH: string = '/api/v0/apikeys';
|
private readonly ROOT_PATH: string = '/api/v0/apikeys';
|
||||||
@ -180,7 +181,7 @@ export class apikeysHttpApiV0 {
|
|||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAPIKey(id: string): Promise<void> {
|
public async deleteAPIKey(id: UUID): Promise<void> {
|
||||||
const path = `${this.ROOT_PATH}/delete/${id}`;
|
const path = `${this.ROOT_PATH}/delete/${id}`;
|
||||||
const response = await this.http.delete(path);
|
const response = await this.http.delete(path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@ -189,8 +190,8 @@ export class apikeysHttpApiV0 {
|
|||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class usersHttpApiV0 {
|
export class usersHttpApiV0 {
|
||||||
private readonly http: HttpClient = new HttpClient();
|
private readonly http: HttpClient = new HttpClient();
|
||||||
private readonly ROOT_PATH: string = '/api/v0/users';
|
private readonly ROOT_PATH: string = '/api/v0/users';
|
||||||
@ -204,5 +205,4 @@ export class usersHttpApiV0 {
|
|||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new Error(err.error);
|
throw new Error(err.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,3 +42,8 @@ export enum PricingPlanType {
|
|||||||
PARTNER = 'partner',
|
PARTNER = 'partner',
|
||||||
PRO = 'pro',
|
PRO = 'pro',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fully implement these types and their methods according to their Go counterparts
|
||||||
|
export type UUID = string
|
||||||
|
export type MemorySize = string
|
||||||
|
export type Time = string
|
||||||
|
Loading…
Reference in New Issue
Block a user