waveterm/pkg/service/service.go
Mike Sawka e46906d423
wsh rpc working (#55)
lots of iterations on an RPC protocol. getting wsh working with a
getmeta/setmeta command in addition to html mode.
2024-06-17 09:58:28 -07:00

463 lines
15 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package service
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/wavetermdev/thenextwave/pkg/service/blockservice"
"github.com/wavetermdev/thenextwave/pkg/service/clientservice"
"github.com/wavetermdev/thenextwave/pkg/service/fileservice"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/service/windowservice"
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/web/webcmd"
"github.com/wavetermdev/thenextwave/pkg/wshutil"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
var ServiceMap = map[string]any{
"block": blockservice.BlockServiceInstance,
"object": &objectservice.ObjectService{},
"file": &fileservice.FileService{},
"client": &clientservice.ClientService{},
"window": &windowservice.WindowService{},
}
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var updatesRType = reflect.TypeOf(([]wstore.WaveObjUpdate{}))
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var waveObjSliceRType = reflect.TypeOf([]waveobj.WaveObj{})
var waveObjMapRType = reflect.TypeOf(map[string]waveobj.WaveObj{})
var methodMetaRType = reflect.TypeOf(tsgenmeta.MethodMeta{})
var waveObjUpdateRType = reflect.TypeOf(wstore.WaveObjUpdate{})
var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem()
var blockCommandRType = reflect.TypeOf((*wshutil.BlockCommand)(nil)).Elem()
var wsCommandRType = reflect.TypeOf((*webcmd.WSCommandType)(nil)).Elem()
type WebCallType struct {
Service string `json:"service"`
Method string `json:"method"`
UIContext *wstore.UIContext `json:"uicontext,omitempty"`
Args []any `json:"args"`
}
type WebReturnType struct {
Success bool `json:"success,omitempty"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
Updates []wstore.WaveObjUpdate `json:"updates,omitempty"`
}
func convertNumber(argType reflect.Type, jsonArg float64) (any, error) {
switch argType.Kind() {
case reflect.Int:
return int(jsonArg), nil
case reflect.Int8:
return int8(jsonArg), nil
case reflect.Int16:
return int16(jsonArg), nil
case reflect.Int32:
return int32(jsonArg), nil
case reflect.Int64:
return int64(jsonArg), nil
case reflect.Uint:
return uint(jsonArg), nil
case reflect.Uint8:
return uint8(jsonArg), nil
case reflect.Uint16:
return uint16(jsonArg), nil
case reflect.Uint32:
return uint32(jsonArg), nil
case reflect.Uint64:
return uint64(jsonArg), nil
case reflect.Float32:
return float32(jsonArg), nil
case reflect.Float64:
return jsonArg, nil
}
return nil, fmt.Errorf("invalid number type %s", argType)
}
func convertComplex(argType reflect.Type, jsonArg any) (any, error) {
nativeArgVal := reflect.New(argType)
err := utilfn.DoMapStucture(nativeArgVal.Interface(), jsonArg)
if err != nil {
return nil, err
}
return nativeArgVal.Elem().Interface(), nil
}
func isSpecialWaveArgType(argType reflect.Type) bool {
return argType == waveObjRType || argType == waveObjSliceRType || argType == waveObjMapRType || argType == blockCommandRType || argType == wsCommandRType
}
func convertBlockCommand(argType reflect.Type, jsonArg any) (any, error) {
if _, ok := jsonArg.(map[string]any); !ok {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
cmd, err := wshutil.ParseCmdMap(jsonArg.(map[string]any))
if err != nil {
return nil, fmt.Errorf("error parsing command map: %w", err)
}
return cmd, nil
}
func convertWSCommand(argType reflect.Type, jsonArg any) (any, error) {
if _, ok := jsonArg.(map[string]any); !ok {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
cmd, err := webcmd.ParseWSCommandMap(jsonArg.(map[string]any))
if err != nil {
return nil, fmt.Errorf("error parsing command map: %w", err)
}
return cmd, nil
}
func convertSpecial(argType reflect.Type, jsonArg any) (any, error) {
jsonType := reflect.TypeOf(jsonArg)
if argType == blockCommandRType {
return convertBlockCommand(argType, jsonArg)
} else if argType == wsCommandRType {
return convertWSCommand(argType, jsonArg)
} else if argType == waveObjRType {
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return waveobj.FromJsonMap(jsonArg.(map[string]any))
} else if argType == waveObjSliceRType {
if jsonType.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
sliceArg := jsonArg.([]any)
nativeSlice := make([]waveobj.WaveObj, len(sliceArg))
for idx, elem := range sliceArg {
elemMap, ok := elem.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot convert %T to %s (idx %d is not a map, is %T)", jsonArg, waveObjSliceRType, idx, elem)
}
nativeObj, err := waveobj.FromJsonMap(elemMap)
if err != nil {
return nil, fmt.Errorf("cannot convert %T to %s (idx %d) error: %v", jsonArg, waveObjSliceRType, idx, err)
}
nativeSlice[idx] = nativeObj
}
return nativeSlice, nil
} else if argType == waveObjMapRType {
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
mapArg := jsonArg.(map[string]any)
nativeMap := make(map[string]waveobj.WaveObj)
for key, elem := range mapArg {
elemMap, ok := elem.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot convert %T to %s (key %s is not a map, is %T)", jsonArg, waveObjMapRType, key, elem)
}
nativeObj, err := waveobj.FromJsonMap(elemMap)
if err != nil {
return nil, fmt.Errorf("cannot convert %T to %s (key %s) error: %v", jsonArg, waveObjMapRType, key, err)
}
nativeMap[key] = nativeObj
}
return nativeMap, nil
} else {
return nil, fmt.Errorf("invalid special wave argument type %s", argType)
}
}
func convertSpecialForReturn(argType reflect.Type, nativeArg any) (any, error) {
if argType == waveObjRType {
return waveobj.ToJsonMap(nativeArg.(waveobj.WaveObj))
} else if argType == waveObjSliceRType {
nativeSlice := nativeArg.([]waveobj.WaveObj)
jsonSlice := make([]map[string]any, len(nativeSlice))
for idx, elem := range nativeSlice {
elemMap, err := waveobj.ToJsonMap(elem)
if err != nil {
return nil, err
}
jsonSlice[idx] = elemMap
}
return jsonSlice, nil
} else if argType == waveObjMapRType {
nativeMap := nativeArg.(map[string]waveobj.WaveObj)
jsonMap := make(map[string]map[string]any)
for key, elem := range nativeMap {
elemMap, err := waveobj.ToJsonMap(elem)
if err != nil {
return nil, err
}
jsonMap[key] = elemMap
}
return jsonMap, nil
} else {
return nil, fmt.Errorf("invalid special wave argument type %s", argType)
}
}
func convertArgument(argType reflect.Type, jsonArg any) (any, error) {
if jsonArg == nil {
return nil, nil
}
if isSpecialWaveArgType(argType) {
return convertSpecial(argType, jsonArg)
}
jsonType := reflect.TypeOf(jsonArg)
switch argType.Kind() {
case reflect.String:
if jsonType.Kind() == reflect.String {
return jsonArg, nil
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
case reflect.Bool:
if jsonType.Kind() == reflect.Bool {
return jsonArg, nil
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
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:
if jsonType.Kind() == reflect.Float64 {
return convertNumber(argType, jsonArg.(float64))
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
case reflect.Map:
if argType.Key().Kind() != reflect.String {
return nil, fmt.Errorf("invalid map key type %s", argType.Key())
}
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Slice:
if jsonType.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Struct:
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Ptr:
if argType.Elem().Kind() != reflect.Struct {
return nil, fmt.Errorf("invalid pointer type %s", argType)
}
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
default:
return nil, fmt.Errorf("invalid argument type %s", argType)
}
}
func isNilable(val reflect.Value) bool {
switch val.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Chan, reflect.Func:
return true
}
return false
}
func convertReturnValues(rtnVals []reflect.Value) *WebReturnType {
rtn := &WebReturnType{}
if len(rtnVals) == 0 {
return rtn
}
for _, val := range rtnVals {
if isNilable(val) && val.IsNil() {
continue
}
valType := val.Type()
if valType == errorRType {
rtn.Error = val.Interface().(error).Error()
continue
}
if valType == updatesRType {
// has a special MarshalJSON method
rtn.Updates = val.Interface().([]wstore.WaveObjUpdate)
continue
}
if isSpecialWaveArgType(valType) {
jsonVal, err := convertSpecialForReturn(valType, val.Interface())
if err != nil {
rtn.Error = fmt.Errorf("cannot convert special return value: %v", err).Error()
continue
}
rtn.Data = jsonVal
continue
}
rtn.Data = val.Interface()
}
if rtn.Error == "" {
rtn.Success = true
}
return rtn
}
func webErrorRtn(err error) *WebReturnType {
return &WebReturnType{
Error: err.Error(),
}
}
func CallService(ctx context.Context, webCall WebCallType) *WebReturnType {
svcObj := ServiceMap[webCall.Service]
if svcObj == nil {
return webErrorRtn(fmt.Errorf("invalid service: %q", webCall.Service))
}
method := reflect.ValueOf(svcObj).MethodByName(webCall.Method)
if !method.IsValid() {
return webErrorRtn(fmt.Errorf("invalid method: %s.%s", webCall.Service, webCall.Method))
}
var valueArgs []reflect.Value
argIdx := 0
for idx := 0; idx < method.Type().NumIn(); idx++ {
argType := method.Type().In(idx)
if idx == 0 && argType == contextRType {
valueArgs = append(valueArgs, reflect.ValueOf(ctx))
continue
}
if argType == uiContextRType {
if webCall.UIContext == nil {
return webErrorRtn(fmt.Errorf("missing UIContext for %s.%s", webCall.Service, webCall.Method))
}
valueArgs = append(valueArgs, reflect.ValueOf(*webCall.UIContext))
continue
}
if argIdx >= len(webCall.Args) {
return webErrorRtn(fmt.Errorf("not enough arguments passed %s.%s idx:%d (type %T)", webCall.Service, webCall.Method, idx, argType))
}
nativeArg, err := convertArgument(argType, webCall.Args[argIdx])
if err != nil {
return webErrorRtn(fmt.Errorf("cannot convert argument %s.%s type:%T idx:%d error:%v", webCall.Service, webCall.Method, argType, idx, err))
}
valueArgs = append(valueArgs, reflect.ValueOf(nativeArg))
argIdx++
}
retValArr := method.Call(valueArgs)
return convertReturnValues(retValArr)
}
// ValidateServiceArg validates the argument type for a service method
// does not allow interfaces (and the obvious invalid types)
// arguments + return values have special handling for wave objects
func baseValidateServiceArg(argType reflect.Type) error {
if argType == waveObjUpdateRType {
// has special MarshalJSON method, so it is safe
return nil
}
switch argType.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Array:
return baseValidateServiceArg(argType.Elem())
case reflect.Map:
if argType.Key().Kind() != reflect.String {
return fmt.Errorf("invalid map key type %s", argType.Key())
}
return baseValidateServiceArg(argType.Elem())
case reflect.Struct:
for idx := 0; idx < argType.NumField(); idx++ {
if err := baseValidateServiceArg(argType.Field(idx).Type); err != nil {
return err
}
}
case reflect.Interface:
return fmt.Errorf("invalid argument type %s: contains interface", argType)
case reflect.Chan, reflect.Func, reflect.Complex128, reflect.Complex64, reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer:
return fmt.Errorf("invalid argument type %s", argType)
}
return nil
}
func validateMethodReturnArg(retType reflect.Type) error {
// specifically allow waveobj.WaveObj, []waveobj.WaveObj, map[string]waveobj.WaveObj, and error
if isSpecialWaveArgType(retType) || retType == errorRType {
return nil
}
return baseValidateServiceArg(retType)
}
func validateMethodArg(argType reflect.Type) error {
// specifically allow waveobj.WaveObj, []waveobj.WaveObj, map[string]waveobj.WaveObj, and context.Context
if isSpecialWaveArgType(argType) || argType == contextRType {
return nil
}
return baseValidateServiceArg(argType)
}
func validateServiceMethod(service string, method reflect.Method) error {
for idx := 0; idx < method.Type.NumOut(); idx++ {
if err := validateMethodReturnArg(method.Type.Out(idx)); err != nil {
return fmt.Errorf("invalid return type %s.%s %s: %v", service, method.Name, method.Type.Out(idx), err)
}
}
for idx := 1; idx < method.Type.NumIn(); idx++ {
// skip the first argument which is the receiver
if err := validateMethodArg(method.Type.In(idx)); err != nil {
return fmt.Errorf("invalid argument type %s.%s %s: %v", service, method.Name, method.Type.In(idx), err)
}
}
return nil
}
func validateServiceMetaMethod(service string, method reflect.Method) error {
if method.Type.NumIn() != 1 {
return fmt.Errorf("invalid number of arguments %s.%s: got:%d, expected just the receiver", service, method.Name, method.Type.NumIn())
}
if method.Type.NumOut() != 1 && method.Type.Out(0) != methodMetaRType {
return fmt.Errorf("invalid return type %s.%s: got:%s, expected servicemeta.MethodMeta", service, method.Name, method.Type.Out(0))
}
return nil
}
func ValidateService(serviceName string, svcObj any) error {
svcType := reflect.TypeOf(svcObj)
if svcType.Kind() != reflect.Ptr {
return fmt.Errorf("service object %q must be a pointer", serviceName)
}
svcType = svcType.Elem()
if svcType.Kind() != reflect.Struct {
return fmt.Errorf("service object %q must be a ptr to struct", serviceName)
}
for idx := 0; idx < svcType.NumMethod(); idx++ {
method := svcType.Method(idx)
if strings.HasSuffix(method.Name, "_Meta") {
err := validateServiceMetaMethod(serviceName, method)
if err != nil {
return err
}
}
if err := validateServiceMethod(serviceName, method); err != nil {
return err
}
}
return nil
}
func ValidateServiceMap() error {
for svcName, svcObj := range ServiceMap {
if err := ValidateService(svcName, svcObj); err != nil {
return err
}
}
return nil
}