mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-31 18:18:02 +01:00
936d4bfb30
This migrates all remaining eventbus events sent over the websocket to use the wps interface. WPS is more flexible for registering events and callbacks and provides support for more reliable unsubscribes and resubscribes.
507 lines
15 KiB
Go
507 lines
15 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package tsgen
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/eventbus"
|
|
"github.com/wavetermdev/waveterm/pkg/filestore"
|
|
"github.com/wavetermdev/waveterm/pkg/service"
|
|
"github.com/wavetermdev/waveterm/pkg/tsgen/tsgenmeta"
|
|
"github.com/wavetermdev/waveterm/pkg/userinput"
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
"github.com/wavetermdev/waveterm/pkg/vdom"
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
|
"github.com/wavetermdev/waveterm/pkg/web/webcmd"
|
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
|
)
|
|
|
|
// add extra types to generate here
|
|
var ExtraTypes = []any{
|
|
waveobj.ORef{},
|
|
(*waveobj.WaveObj)(nil),
|
|
map[string]any{},
|
|
service.WebCallType{},
|
|
service.WebReturnType{},
|
|
waveobj.UIContext{},
|
|
eventbus.WSEventType{},
|
|
wps.WSFileEventData{},
|
|
waveobj.LayoutActionData{},
|
|
filestore.WaveFile{},
|
|
wconfig.FullConfigType{},
|
|
wconfig.WatcherUpdate{},
|
|
wshutil.RpcMessage{},
|
|
wshrpc.WshServerCommandMeta{},
|
|
userinput.UserInputRequest{},
|
|
vdom.Elem{},
|
|
vdom.VDomFuncType{},
|
|
vdom.VDomRefType{},
|
|
waveobj.MetaTSType{},
|
|
}
|
|
|
|
// add extra type unions to generate here
|
|
var TypeUnions = []tsgenmeta.TypeUnionMeta{
|
|
webcmd.WSCommandTypeUnionMeta(),
|
|
}
|
|
|
|
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
|
|
var errorRType = reflect.TypeOf((*error)(nil)).Elem()
|
|
var anyRType = reflect.TypeOf((*interface{})(nil)).Elem()
|
|
var metaRType = reflect.TypeOf((*waveobj.MetaMapType)(nil)).Elem()
|
|
var metaSettingsType = reflect.TypeOf((*wconfig.MetaSettingsType)(nil)).Elem()
|
|
var uiContextRType = reflect.TypeOf((*waveobj.UIContext)(nil)).Elem()
|
|
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
|
|
var updatesRtnRType = reflect.TypeOf(waveobj.UpdatesRtnType{})
|
|
var orefRType = reflect.TypeOf((*waveobj.ORef)(nil)).Elem()
|
|
var wshRpcInterfaceRType = reflect.TypeOf((*wshrpc.WshRpcInterface)(nil)).Elem()
|
|
|
|
func generateTSMethodTypes(method reflect.Method, tsTypesMap map[reflect.Type]string, skipFirstArg bool) error {
|
|
for idx := 0; idx < method.Type.NumIn(); idx++ {
|
|
if skipFirstArg && idx == 0 {
|
|
continue
|
|
}
|
|
inType := method.Type.In(idx)
|
|
GenerateTSType(inType, tsTypesMap)
|
|
}
|
|
for idx := 0; idx < method.Type.NumOut(); idx++ {
|
|
outType := method.Type.Out(idx)
|
|
GenerateTSType(outType, tsTypesMap)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getTSFieldName(field reflect.StructField) string {
|
|
tsFieldTag := field.Tag.Get("tsfield")
|
|
if tsFieldTag != "" {
|
|
if tsFieldTag == "-" {
|
|
return ""
|
|
}
|
|
return tsFieldTag
|
|
}
|
|
jsonTag := utilfn.GetJsonTag(field)
|
|
if jsonTag == "-" {
|
|
return ""
|
|
}
|
|
if strings.Contains(jsonTag, ":") {
|
|
return "\"" + jsonTag + "\""
|
|
}
|
|
if jsonTag != "" {
|
|
return jsonTag
|
|
}
|
|
return field.Name
|
|
}
|
|
|
|
func isFieldOmitEmpty(field reflect.StructField) bool {
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag != "" {
|
|
parts := strings.Split(jsonTag, ",")
|
|
if len(parts) > 1 {
|
|
for _, part := range parts[1:] {
|
|
if part == "omitempty" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) {
|
|
switch t.Kind() {
|
|
case reflect.String:
|
|
return "string", nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
|
reflect.Float32, reflect.Float64:
|
|
return "number", nil
|
|
case reflect.Bool:
|
|
return "boolean", nil
|
|
case reflect.Slice, reflect.Array:
|
|
elemType, subTypes := TypeToTSType(t.Elem(), tsTypesMap)
|
|
if elemType == "" {
|
|
return "", nil
|
|
}
|
|
return fmt.Sprintf("%s[]", elemType), subTypes
|
|
case reflect.Map:
|
|
if t.Key().Kind() != reflect.String {
|
|
return "", nil
|
|
}
|
|
if t == metaRType {
|
|
return "MetaType", nil
|
|
}
|
|
elemType, subTypes := TypeToTSType(t.Elem(), tsTypesMap)
|
|
if elemType == "" {
|
|
return "", nil
|
|
}
|
|
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
|
|
case reflect.Struct:
|
|
name := t.Name()
|
|
if tsRename := tsRenameMap[name]; tsRename != "" {
|
|
name = tsRename
|
|
}
|
|
return name, []reflect.Type{t}
|
|
case reflect.Ptr:
|
|
return TypeToTSType(t.Elem(), tsTypesMap)
|
|
case reflect.Interface:
|
|
if _, ok := tsTypesMap[t]; ok {
|
|
return t.Name(), nil
|
|
}
|
|
return "any", nil
|
|
default:
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
var tsRenameMap = map[string]string{
|
|
"Window": "WaveWindow",
|
|
"Elem": "VDomElem",
|
|
"MetaTSType": "MetaType",
|
|
"MetaSettingsType": "SettingsType",
|
|
}
|
|
|
|
func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) {
|
|
var buf bytes.Buffer
|
|
tsTypeName := rtype.Name()
|
|
if tsRename, ok := tsRenameMap[tsTypeName]; ok {
|
|
tsTypeName = tsRename
|
|
}
|
|
var isWaveObj bool
|
|
buf.WriteString(fmt.Sprintf("// %s\n", rtype.String()))
|
|
if rtype.Implements(waveObjRType) || reflect.PointerTo(rtype).Implements(waveObjRType) {
|
|
isWaveObj = true
|
|
buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName))
|
|
} else {
|
|
buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName))
|
|
}
|
|
var subTypes []reflect.Type
|
|
for i := 0; i < rtype.NumField(); i++ {
|
|
field := rtype.Field(i)
|
|
if field.PkgPath != "" {
|
|
continue
|
|
}
|
|
fieldName := getTSFieldName(field)
|
|
if fieldName == "" {
|
|
continue
|
|
}
|
|
if isWaveObj && (fieldName == waveobj.OTypeKeyName || fieldName == waveobj.OIDKeyName || fieldName == waveobj.VersionKeyName || fieldName == waveobj.MetaKeyName) {
|
|
continue
|
|
}
|
|
optMarker := ""
|
|
if isFieldOmitEmpty(field) {
|
|
optMarker = "?"
|
|
}
|
|
tsTypeTag := field.Tag.Get("tstype")
|
|
if tsTypeTag != "" {
|
|
if tsTypeTag == "-" {
|
|
continue
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag))
|
|
continue
|
|
}
|
|
tsType, fieldSubTypes := TypeToTSType(field.Type, tsTypesMap)
|
|
if tsType == "" {
|
|
continue
|
|
}
|
|
subTypes = append(subTypes, fieldSubTypes...)
|
|
if tsType == "UIContext" {
|
|
optMarker = "?"
|
|
}
|
|
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType))
|
|
}
|
|
buf.WriteString("};\n")
|
|
return buf.String(), subTypes
|
|
}
|
|
|
|
func GenerateWaveObjTSType() string {
|
|
var buf bytes.Buffer
|
|
buf.WriteString("// waveobj.WaveObj\n")
|
|
buf.WriteString("type WaveObj = {\n")
|
|
buf.WriteString(" otype: string;\n")
|
|
buf.WriteString(" oid: string;\n")
|
|
buf.WriteString(" version: number;\n")
|
|
buf.WriteString(" meta: MetaType;\n")
|
|
buf.WriteString("};\n")
|
|
return buf.String()
|
|
}
|
|
|
|
func GenerateTSTypeUnion(unionMeta tsgenmeta.TypeUnionMeta, tsTypeMap map[reflect.Type]string) {
|
|
rtn := generateTSTypeUnionInternal(unionMeta)
|
|
tsTypeMap[unionMeta.BaseType] = rtn
|
|
for _, rtype := range unionMeta.Types {
|
|
GenerateTSType(rtype, tsTypeMap)
|
|
}
|
|
}
|
|
|
|
func generateTSTypeUnionInternal(unionMeta tsgenmeta.TypeUnionMeta) string {
|
|
var buf bytes.Buffer
|
|
if unionMeta.Desc != "" {
|
|
buf.WriteString(fmt.Sprintf("// %s\n", unionMeta.Desc))
|
|
}
|
|
buf.WriteString(fmt.Sprintf("type %s = {\n", unionMeta.BaseType.Name()))
|
|
buf.WriteString(fmt.Sprintf(" %s: string;\n", unionMeta.TypeFieldName))
|
|
buf.WriteString("} & ( ")
|
|
for idx, rtype := range unionMeta.Types {
|
|
if idx > 0 {
|
|
buf.WriteString(" | ")
|
|
}
|
|
buf.WriteString(rtype.Name())
|
|
}
|
|
buf.WriteString(" );\n")
|
|
return buf.String()
|
|
}
|
|
|
|
func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) {
|
|
if rtype == nil {
|
|
return
|
|
}
|
|
if rtype.Kind() == reflect.Chan {
|
|
rtype = rtype.Elem()
|
|
}
|
|
if rtype == contextRType || rtype == errorRType || rtype == anyRType {
|
|
return
|
|
}
|
|
if rtype.Kind() == reflect.Slice {
|
|
rtype = rtype.Elem()
|
|
}
|
|
if rtype.Kind() == reflect.Map {
|
|
rtype = rtype.Elem()
|
|
}
|
|
if rtype.Kind() == reflect.Ptr {
|
|
rtype = rtype.Elem()
|
|
}
|
|
if _, ok := tsTypesMap[rtype]; ok {
|
|
return
|
|
}
|
|
if rtype == orefRType {
|
|
tsTypesMap[orefRType] = "// waveobj.ORef\ntype ORef = string;\n"
|
|
return
|
|
}
|
|
if rtype == waveObjRType {
|
|
tsTypesMap[rtype] = GenerateWaveObjTSType()
|
|
return
|
|
}
|
|
if rtype == metaSettingsType {
|
|
return
|
|
}
|
|
if rtype.Kind() != reflect.Struct {
|
|
return
|
|
}
|
|
tsType, subTypes := generateTSTypeInternal(rtype, tsTypesMap)
|
|
tsTypesMap[rtype] = tsType
|
|
for _, subType := range subTypes {
|
|
GenerateTSType(subType, tsTypesMap)
|
|
}
|
|
}
|
|
|
|
func hasUpdatesReturn(method reflect.Method) bool {
|
|
for idx := 0; idx < method.Type.NumOut(); idx++ {
|
|
outType := method.Type.Out(idx)
|
|
if outType == updatesRtnRType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func GenerateMethodSignature(serviceName string, method reflect.Method, meta tsgenmeta.MethodMeta, isFirst bool, tsTypesMap map[reflect.Type]string) string {
|
|
var sb strings.Builder
|
|
mayReturnUpdates := hasUpdatesReturn(method)
|
|
if (meta.Desc != "" || meta.ReturnDesc != "" || mayReturnUpdates) && !isFirst {
|
|
sb.WriteString("\n")
|
|
}
|
|
if meta.Desc != "" {
|
|
sb.WriteString(fmt.Sprintf(" // %s\n", meta.Desc))
|
|
}
|
|
if mayReturnUpdates || meta.ReturnDesc != "" {
|
|
if mayReturnUpdates && meta.ReturnDesc != "" {
|
|
sb.WriteString(fmt.Sprintf(" // @returns %s (and object updates)\n", meta.ReturnDesc))
|
|
} else if mayReturnUpdates {
|
|
sb.WriteString(" // @returns object updates\n")
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(" // @returns %s\n", meta.ReturnDesc))
|
|
}
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(method.Name)
|
|
sb.WriteString("(")
|
|
wroteArg := false
|
|
// skip first arg, which is the receiver
|
|
for idx := 1; idx < method.Type.NumIn(); idx++ {
|
|
if wroteArg {
|
|
sb.WriteString(", ")
|
|
}
|
|
inType := method.Type.In(idx)
|
|
if inType == contextRType || inType == uiContextRType {
|
|
continue
|
|
}
|
|
tsTypeName, _ := TypeToTSType(inType, tsTypesMap)
|
|
var argName string
|
|
if idx-1 < len(meta.ArgNames) {
|
|
argName = meta.ArgNames[idx-1] // subtract 1 for receiver
|
|
} else {
|
|
argName = fmt.Sprintf("arg%d", idx)
|
|
}
|
|
sb.WriteString(fmt.Sprintf("%s: %s", argName, tsTypeName))
|
|
wroteArg = true
|
|
}
|
|
sb.WriteString("): ")
|
|
wroteRtn := false
|
|
for idx := 0; idx < method.Type.NumOut(); idx++ {
|
|
outType := method.Type.Out(idx)
|
|
if outType == errorRType {
|
|
continue
|
|
}
|
|
if outType == updatesRtnRType {
|
|
continue
|
|
}
|
|
tsTypeName, _ := TypeToTSType(outType, tsTypesMap)
|
|
sb.WriteString(fmt.Sprintf("Promise<%s>", tsTypeName))
|
|
wroteRtn = true
|
|
}
|
|
if !wroteRtn {
|
|
sb.WriteString("Promise<void>")
|
|
}
|
|
sb.WriteString(" {\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func GenerateMethodBody(serviceName string, method reflect.Method, meta tsgenmeta.MethodMeta) string {
|
|
return fmt.Sprintf(" return WOS.callBackendService(%q, %q, Array.from(arguments))\n", serviceName, method.Name)
|
|
}
|
|
|
|
func GenerateServiceClass(serviceName string, serviceObj any, tsTypesMap map[reflect.Type]string) string {
|
|
serviceType := reflect.TypeOf(serviceObj)
|
|
var sb strings.Builder
|
|
tsServiceName := serviceType.Elem().Name()
|
|
sb.WriteString(fmt.Sprintf("// %s (%s)\n", serviceType.Elem().String(), serviceName))
|
|
sb.WriteString("class ")
|
|
sb.WriteString(tsServiceName + "Type")
|
|
sb.WriteString(" {\n")
|
|
isFirst := true
|
|
for midx := 0; midx < serviceType.NumMethod(); midx++ {
|
|
method := serviceType.Method(midx)
|
|
if strings.HasSuffix(method.Name, "_Meta") {
|
|
continue
|
|
}
|
|
var meta tsgenmeta.MethodMeta
|
|
metaMethod, found := serviceType.MethodByName(method.Name + "_Meta")
|
|
if found {
|
|
serviceObjVal := reflect.ValueOf(serviceObj)
|
|
metaVal := metaMethod.Func.Call([]reflect.Value{serviceObjVal})
|
|
meta = metaVal[0].Interface().(tsgenmeta.MethodMeta)
|
|
}
|
|
sb.WriteString(GenerateMethodSignature(serviceName, method, meta, isFirst, tsTypesMap))
|
|
sb.WriteString(GenerateMethodBody(serviceName, method, meta))
|
|
sb.WriteString(" }\n")
|
|
isFirst = false
|
|
}
|
|
sb.WriteString("}\n\n")
|
|
sb.WriteString(fmt.Sprintf("export const %s = new %sType();\n", tsServiceName, tsServiceName))
|
|
return sb.String()
|
|
}
|
|
|
|
func GenerateWshServerMethod(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
|
|
if methodDecl.CommandType == wshrpc.RpcType_ResponseStream {
|
|
return GenerateWshServerMethod_ResponseStream(methodDecl, tsTypesMap)
|
|
} else if methodDecl.CommandType == wshrpc.RpcType_Call {
|
|
return GenerateWshServerMethod_Call(methodDecl, tsTypesMap)
|
|
} else {
|
|
panic(fmt.Sprintf("cannot generate wshserver commandtype %q", methodDecl.CommandType))
|
|
}
|
|
}
|
|
|
|
func GenerateWshServerMethod_ResponseStream(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType))
|
|
respType := "any"
|
|
if methodDecl.DefaultResponseDataType != nil {
|
|
respType, _ = TypeToTSType(methodDecl.DefaultResponseDataType, tsTypesMap)
|
|
}
|
|
dataName := "null"
|
|
if methodDecl.CommandDataType != nil {
|
|
dataName = "data"
|
|
}
|
|
genRespType := fmt.Sprintf("AsyncGenerator<%s, void, boolean>", respType)
|
|
if methodDecl.CommandDataType != nil {
|
|
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
|
|
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, genRespType))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(" %s(opts?: RpcOpts): %s {\n", methodDecl.MethodName, genRespType))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" return wshServerRpcHelper_responsestream(%q, %s, opts);\n", methodDecl.Command, dataName))
|
|
sb.WriteString(" }\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func GenerateWshServerMethod_Call(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf(" // command %q [%s]\n", methodDecl.Command, methodDecl.CommandType))
|
|
rtnType := "Promise<void>"
|
|
if methodDecl.DefaultResponseDataType != nil {
|
|
rtnTypeName, _ := TypeToTSType(methodDecl.DefaultResponseDataType, tsTypesMap)
|
|
rtnType = fmt.Sprintf("Promise<%s>", rtnTypeName)
|
|
}
|
|
dataName := "null"
|
|
if methodDecl.CommandDataType != nil {
|
|
dataName = "data"
|
|
}
|
|
if methodDecl.CommandDataType != nil {
|
|
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
|
|
sb.WriteString(fmt.Sprintf(" %s(data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, rtnType))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf(" %s(opts?: RpcOpts): %s {\n", methodDecl.MethodName, rtnType))
|
|
}
|
|
methodBody := fmt.Sprintf(" return wshServerRpcHelper_call(%q, %s, opts);\n", methodDecl.Command, dataName)
|
|
sb.WriteString(methodBody)
|
|
sb.WriteString(" }\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) {
|
|
for _, typeUnion := range TypeUnions {
|
|
GenerateTSTypeUnion(typeUnion, tsTypesMap)
|
|
}
|
|
for _, extraType := range ExtraTypes {
|
|
GenerateTSType(reflect.TypeOf(extraType), tsTypesMap)
|
|
}
|
|
for _, rtype := range waveobj.AllWaveObjTypes() {
|
|
GenerateTSType(rtype, tsTypesMap)
|
|
}
|
|
}
|
|
|
|
func GenerateServiceTypes(tsTypesMap map[reflect.Type]string) error {
|
|
for _, serviceObj := range service.ServiceMap {
|
|
serviceType := reflect.TypeOf(serviceObj)
|
|
for midx := 0; midx < serviceType.NumMethod(); midx++ {
|
|
method := serviceType.Method(midx)
|
|
err := generateTSMethodTypes(method, tsTypesMap, true)
|
|
if err != nil {
|
|
return fmt.Errorf("error generating TS method types for %s.%s: %v", serviceType, method.Name, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GenerateWshServerTypes(tsTypesMap map[reflect.Type]string) error {
|
|
GenerateTSType(reflect.TypeOf(wshrpc.RpcOpts{}), tsTypesMap)
|
|
rtype := wshRpcInterfaceRType
|
|
for midx := 0; midx < rtype.NumMethod(); midx++ {
|
|
method := rtype.Method(midx)
|
|
err := generateTSMethodTypes(method, tsTypesMap, false)
|
|
if err != nil {
|
|
return fmt.Errorf("error generating TS method types for %s.%s: %v", rtype, method.Name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|