waveterm/pkg/tsgen/tsgen.go
Evan Simkowitz 936d4bfb30
Migrate websocket eventbus messages to wps (#367)
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.
2024-09-11 18:03:55 -07:00

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
}