1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 09:46:19 +01:00
Files
incus/shared/scriptlet/marshal.go
Max Asnaashari cb8cc27ab5 shared/scriptlet: Move scriptlet utils to shared
Signed-off-by: Max Asnaashari <max.asna@futurfusion.io>
2025-08-08 15:49:03 -07:00

268 lines
6.2 KiB
Go

package scriptlet
import (
"fmt"
"reflect"
"sort"
"strings"
"go.starlark.net/starlark"
)
// starlarkObject wraps a starlark.Dict and is used to provide custom object types to the Starlark scriptlets.
// This implements the starlark.HasAttrs interface.
type starlarkObject struct {
d *starlark.Dict
typeName string
}
// Type is a starlark object type.
func (s *starlarkObject) Type() string {
return s.typeName
}
// String is a starlark object string.
func (s *starlarkObject) String() string {
return s.d.String()
}
// Freeze freezes the starlark object.
func (s *starlarkObject) Freeze() {
}
// Hash returns a hash of the starlark object.
func (s *starlarkObject) Hash() (uint32, error) {
return 0, fmt.Errorf("Unhashable type %s", s.Type())
}
// Truth returns whether the starlark object is true.
func (s *starlarkObject) Truth() starlark.Bool {
return starlark.True
}
// AttrNames returns the attribute names of the starlark object.
func (s *starlarkObject) AttrNames() []string {
keys := s.d.Keys()
keyNames := make([]string, 0, len(keys))
for _, k := range keys {
keyNames = append(keyNames, k.String())
}
return keyNames
}
// Attr gets an attribute of the starlark object.
func (s *starlarkObject) Attr(name string) (starlark.Value, error) {
field, found, err := s.d.Get(starlark.String(name))
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("Invalid field %q", name)
}
return field, nil
}
// StarlarkMarshal converts input to a starlark Value.
// It only includes exported struct fields, and uses the "json" tag for field names.
func StarlarkMarshal(input any) (starlark.Value, error) {
return starlarkMarshal(input, nil)
}
// starlarkMarshal converts input to a starlark Value.
// It only includes exported struct fields, and uses the "json" tag for field names.
// Takes optional parent Starlark dictionary which will be used to set fields from anonymous (embedded) structs
// in to the parent struct.
func starlarkMarshal(input any, parent *starlark.Dict) (starlark.Value, error) {
if input == nil {
return starlark.None, nil
}
sv, ok := input.(starlark.Value)
if ok {
return sv, nil
}
var err error
v := reflect.ValueOf(input)
switch v.Type().Kind() {
case reflect.String:
sv = starlark.String(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
sv = starlark.MakeInt(int(v.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
sv = starlark.MakeUint(uint(v.Uint()))
case reflect.Float32, reflect.Float64:
sv = starlark.Float(v.Float())
case reflect.Bool:
sv = starlark.Bool(v.Bool())
case reflect.Array, reflect.Slice:
vlen := v.Len()
listElems := make([]starlark.Value, 0, vlen)
for i := range vlen {
lv, err := StarlarkMarshal(v.Index(i).Interface())
if err != nil {
return nil, err
}
listElems = append(listElems, lv)
}
sv = starlark.NewList(listElems)
case reflect.Map:
mKeys := v.MapKeys()
d := starlark.NewDict(len(mKeys))
if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("Only string keys are supported, found %s", v.Type().Key().Kind())
}
sort.Slice(mKeys, func(i, j int) bool {
return mKeys[i].String() < mKeys[j].String()
})
for _, k := range mKeys {
mv := v.MapIndex(k)
dv, err := StarlarkMarshal(mv.Interface())
if err != nil {
return nil, err
}
err = d.SetKey(starlark.String(k.String()), dv)
if err != nil {
return nil, fmt.Errorf("Failed setting map key %q to %v: %w", k.String(), dv, err)
}
}
sv = d
case reflect.Struct:
fieldCount := v.Type().NumField()
d := parent
if d == nil {
d = starlark.NewDict(fieldCount)
}
for i := range fieldCount {
field := v.Type().Field(i)
fieldValue := v.Field(i)
if !field.IsExported() {
continue
}
if field.Anonymous && fieldValue.Kind() == reflect.Struct {
// If anonymous struct field's value is another struct then pass the the current
// starlark dictionary to starlarkMarshal so its fields will be set on the parent.
_, err = starlarkMarshal(fieldValue.Interface(), d)
if err != nil {
return nil, err
}
} else {
dv, err := StarlarkMarshal(fieldValue.Interface())
if err != nil {
return nil, err
}
key, _, _ := strings.Cut(field.Tag.Get("json"), ",")
if key == "" {
key = field.Name
}
err = d.SetKey(starlark.String(key), dv)
if err != nil {
return nil, fmt.Errorf("Failed setting struct field %q to %v: %w", key, dv, err)
}
}
}
// Only convert the top-level struct to a Starlark object.
if parent == nil {
ss := starlarkObject{
d: d,
typeName: v.Type().Name(),
}
sv = &ss
} else {
sv = d
}
case reflect.Pointer:
if v.IsZero() {
sv = starlark.None
} else {
sv, err = StarlarkMarshal(v.Elem().Interface())
if err != nil {
return nil, err
}
}
default:
return nil, fmt.Errorf("Unrecognised type %v for value %+v", v.Type(), v.Interface())
}
return sv, nil
}
// StarlarkUnmarshal converts a Starlark value into a Go value.
// Only NoneType, Bool, Int, Float, String, List and Dict are supported.
func StarlarkUnmarshal(input starlark.Value) (any, error) {
switch v := input.(type) {
case starlark.NoneType:
return nil, nil
case starlark.Bool:
return bool(v), nil
case starlark.Int:
result, _ := v.Int64()
return result, nil
case starlark.Float:
return float64(v), nil
case starlark.String:
return string(v), nil
case *starlark.List:
length := v.Len()
result := make([]any, length)
// Iterate over the Starlark List
for i := range length {
value, err := StarlarkUnmarshal(v.Index(i))
if err != nil {
return nil, err
}
result[i] = value
}
return result, nil
case *starlark.Dict:
result := make(map[string]any)
// Iterate over the Starlark Dict
for _, kv := range v.Items() {
dictKey, dictValue := kv[0], kv[1]
key, ok := starlark.AsString(dictKey)
if !ok {
return nil, fmt.Errorf("Only string keys are supported, found %s", dictKey.Type())
}
value, err := StarlarkUnmarshal(dictValue)
if err != nil {
return nil, err
}
result[key] = value
}
return result, nil
default:
return nil, fmt.Errorf("Unsupported type: %T", v)
}
}