From 4ba78a18044f17f848d769c2e9cf1d2bf8879a17 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 25 May 2024 18:37:05 -0600 Subject: [PATCH] working on waveobj -- oids --- go.mod | 1 + go.sum | 2 + pkg/waveobj/waveobj.go | 269 ++++++++++++++++++++++++++++++++++++ pkg/waveobj/waveobj_test.go | 23 +++ pkg/wstore/wstore.go | 4 + 5 files changed, 299 insertions(+) create mode 100644 pkg/waveobj/waveobj.go create mode 100644 pkg/waveobj/waveobj_test.go diff --git a/go.mod b/go.mod index 7ea697ba4..e630cfd58 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/uuid v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/mattn/go-sqlite3 v1.14.22 + github.com/mitchellh/mapstructure v1.5.0 github.com/sawka/txwrap v0.2.0 github.com/wailsapp/wails/v3 v3.0.0-alpha.0 github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 diff --git a/go.sum b/go.sum index 217bbb7f5..a346f06c7 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go new file mode 100644 index 000000000..9486000cc --- /dev/null +++ b/pkg/waveobj/waveobj.go @@ -0,0 +1,269 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" + "sync" + + "github.com/mitchellh/mapstructure" +) + +const ( + OTypeKeyName = "otype" + OIDKeyName = "oid" +) + +type waveObjDesc struct { + RType reflect.Type + OIDField reflect.StructField +} + +var globalLock = &sync.Mutex{} +var waveObjMap = make(map[string]*waveObjDesc) +var waveObj WaveObj +var waveObjRType = reflect.TypeOf(&waveObj).Elem() + +func RegisterType(w WaveObj) { + globalLock.Lock() + defer globalLock.Unlock() + oidType := w.GetOType() + if waveObjMap[oidType] != nil { + panic(fmt.Sprintf("duplicate WaveObj registration: %T", w)) + } + rtype := reflect.TypeOf(w) + field := findOIDField(rtype) + if field == nil { + panic(fmt.Sprintf("cannot register WaveObj without OID field -- mark with tag `waveobj:\"oid\"`")) + } + waveObjMap[oidType] = &waveObjDesc{ + RType: rtype, + OIDField: *field, + } +} + +func findOIDField(rtype reflect.Type) *reflect.StructField { + for idx := 0; idx < rtype.NumField(); idx++ { + field := rtype.Field(idx) + if field.PkgPath != "" { + // private + continue + } + waveObjTag := field.Tag.Get("waveobj") + if waveObjTag == "oid" { + if field.Type.Kind() != reflect.String { + panic(fmt.Sprintf("in %v marked oid field is not type 'string'", rtype)) + } + return &field + } + } + return nil +} + +func getObjDescForOIDType(oidType string) *waveObjDesc { + globalLock.Lock() + defer globalLock.Unlock() + return waveObjMap[oidType] +} + +type WaveObj interface { + GetOType() string +} + +func ToJson(w WaveObj) ([]byte, error) { + m := make(map[string]any) + err := mapstructure.Decode(w, &m) + if err != nil { + return nil, err + } + desc := getObjDescForOIDType(w.GetOType()) + if desc == nil { + return nil, fmt.Errorf("otype %q (%T) not registered", w.GetOType(), w) + } + m[OTypeKeyName] = w.GetOType() + m[OIDKeyName] = reflect.ValueOf(w).FieldByIndex(desc.OIDField.Index).String() + return json.Marshal(m) +} + +func FromJson(data []byte) (WaveObj, error) { + var m map[string]any + err := json.Unmarshal(data, &m) + if err != nil { + return nil, err + } + otype, ok := m[OTypeKeyName].(string) + if !ok { + return nil, fmt.Errorf("missing otype") + } + oid, ok := m[OIDKeyName].(string) + if !ok { + return nil, fmt.Errorf("missing oid") + } + desc := getObjDescForOIDType(otype) + if desc == nil { + return nil, fmt.Errorf("unknown oid type: %s", otype) + } + objVal := reflect.New(desc.RType) + oidField := objVal.FieldByIndex(desc.OIDField.Index) + oidField.SetString(oid) + obj := objVal.Interface().(WaveObj) + err = mapstructure.Decode(m, obj) + if err != nil { + return nil, err + } + return obj, nil +} + +func FromJsonGen[T WaveObj](data []byte) (T, error) { + obj, err := FromJson(data) + if err != nil { + var zero T + return zero, err + } + rtn, ok := obj.(T) + if !ok { + var zero T + return zero, fmt.Errorf("type mismatch got %T, expected %T", obj, zero) + } + return rtn, nil +} + +func getTSFieldName(field reflect.StructField) string { + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + namePart := parts[0] + if namePart != "" { + if namePart == "-" { + return "" + } + return namePart + } + // if namePart is empty, still uses default + } + 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) (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()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("%s[]", elemType), subTypes + case reflect.Map: + if t.Key().Kind() != reflect.String { + return "", nil + } + elemType, subTypes := typeToTSType(t.Elem()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes + case reflect.Struct: + return t.Name(), []reflect.Type{t} + case reflect.Ptr: + return typeToTSType(t.Elem()) + case reflect.Interface: + return "any", nil + default: + return "", nil + } +} + +func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { + var buf bytes.Buffer + waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem() + buf.WriteString(fmt.Sprintf("type %s = {\n", rtype.Name())) + if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { + buf.WriteString(fmt.Sprintf(" %s: string;\n", OTypeKeyName)) + buf.WriteString(fmt.Sprintf(" %s: string;\n", OIDKeyName)) + } + 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 + } + optMarker := "" + if isFieldOmitEmpty(field) { + optMarker = "?" + } + tsTypeTag := field.Tag.Get("tstype") + if tsTypeTag != "" { + buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) + continue + } + tsType, fieldSubTypes := typeToTSType(field.Type) + if tsType == "" { + continue + } + subTypes = append(subTypes, fieldSubTypes...) + 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("type WaveObj {\n") + buf.WriteString(" otype: string;\n") + buf.WriteString(" oid: string;\n") + buf.WriteString("}\n") + return buf.String() +} + +func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) { + if rtype == nil { + return + } + if rtype.Kind() == reflect.Ptr { + rtype = rtype.Elem() + } + if _, ok := tsTypesMap[rtype]; ok { + return + } + if rtype == waveObjRType { + tsTypesMap[rtype] = GenerateWaveObjTSType() + return + } + tsType, subTypes := generateTSTypeInternal(rtype) + tsTypesMap[rtype] = tsType + for _, subType := range subTypes { + GenerateTSType(subType, tsTypesMap) + } +} diff --git a/pkg/waveobj/waveobj_test.go b/pkg/waveobj/waveobj_test.go new file mode 100644 index 000000000..e3fd4a7b6 --- /dev/null +++ b/pkg/waveobj/waveobj_test.go @@ -0,0 +1,23 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +import ( + "log" + "reflect" + "testing" + + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +func TestGenerate(t *testing.T) { + log.Printf("Testing Generate\n") + tsMap := make(map[reflect.Type]string) + var waveObj WaveObj + GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsMap) + GenerateTSType(reflect.TypeOf(wstore.Block{}), tsMap) + for k, v := range tsMap { + log.Printf("Type: %v, TS:\n%s\n", k, v) + } +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 9fb6cd1de..142104699 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -116,6 +116,10 @@ type Block struct { RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` } +func (b *Block) GetOType() string { + return "block" +} + func (b Block) GetId() string { return b.BlockId }