From 8491321a9e22b05f0e53fd961fed7d031bf01fff Mon Sep 17 00:00:00 2001 From: akutz Date: Mon, 26 Aug 2024 08:24:05 -0500 Subject: [PATCH] api: ToString for vim types This patch adds a ToString helper function for invoking on any vim type. If the specified value is a primitive type, the returned string is generated using Sprintf. Otherwise the returned string is created first by attempting to marshal the data using the vimtype JSON encoder, and if that fails, the stdlib JSON encoder. If that fails, then fmt.Sprintf("%v") is used. --- vim25/types/helpers.go | 75 ++++++++++++++++ vim25/types/helpers_test.go | 167 ++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) diff --git a/vim25/types/helpers.go b/vim25/types/helpers.go index 94fb50df4..531b90d84 100644 --- a/vim25/types/helpers.go +++ b/vim25/types/helpers.go @@ -17,6 +17,9 @@ limitations under the License. package types import ( + "bytes" + "encoding/json" + "fmt" "net/url" "reflect" "strings" @@ -316,6 +319,78 @@ func (ci VirtualMachineConfigInfo) ToConfigSpec() VirtualMachineConfigSpec { return cs } +// ToString returns the string-ified version of the provided input value by +// first attempting to encode the value to JSON using the vimtype JSON encoder, +// and if that should fail, using the standard JSON encoder, and if that fails, +// returning the value formatted with Sprintf("%v"). +// +// Please note, this function is not intended to replace marshaling the data +// to JSON using the normal workflows. This function is for when a string-ified +// version of the data is needed for things like logging. +func ToString(in AnyType) (s string) { + if in == nil { + return "null" + } + + marshalWithSprintf := func() string { + return fmt.Sprintf("%v", in) + } + + defer func() { + if err := recover(); err != nil { + s = marshalWithSprintf() + } + }() + + rv := reflect.ValueOf(in) + switch rv.Kind() { + + case reflect.Bool, + reflect.Complex64, reflect.Complex128, + reflect.Float32, reflect.Float64: + + return fmt.Sprintf("%v", in) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uintptr: + + return fmt.Sprintf("%d", in) + + case reflect.String: + return in.(string) + + case reflect.Interface, reflect.Pointer: + if rv.IsZero() { + return "null" + } + return ToString(rv.Elem().Interface()) + } + + marshalWithStdlibJSONEncoder := func() string { + data, err := json.Marshal(in) + if err != nil { + return marshalWithSprintf() + } + return string(data) + } + + defer func() { + if err := recover(); err != nil { + s = marshalWithStdlibJSONEncoder() + } + }() + + var w bytes.Buffer + enc := NewJSONEncoder(&w) + if err := enc.Encode(in); err != nil { + return marshalWithStdlibJSONEncoder() + } + + // Do not include the newline character added by the vimtype JSON encoder. + return strings.TrimSuffix(w.String(), "\n") +} + func init() { // Known 6.5 issue where this event type is sent even though it is internal. // This workaround allows us to unmarshal and avoid NPEs. diff --git a/vim25/types/helpers_test.go b/vim25/types/helpers_test.go index f680945ed..da01c7bff 100644 --- a/vim25/types/helpers_test.go +++ b/vim25/types/helpers_test.go @@ -17,8 +17,13 @@ limitations under the License. package types import ( + "fmt" + "reflect" + "slices" "testing" + "github.com/stretchr/testify/assert" + "github.com/vmware/govmomi/vim25/xml" ) @@ -306,3 +311,165 @@ func TestVirtualMachineConfigInfoToConfigSpec(t *testing.T) { }) } } + +type toStringTestCase struct { + name string + in any + expected string +} + +func newToStringTestCases[T any](in T, expected string) []toStringTestCase { + return newToStringTestCasesWithTestCaseName( + in, expected, reflect.TypeOf(in).Name()) +} + +func newToStringTestCasesWithTestCaseName[T any]( + in T, expected, testCaseName string) []toStringTestCase { + + return []toStringTestCase{ + { + name: testCaseName, + in: in, + expected: expected, + }, + { + name: "*" + testCaseName, + in: &[]T{in}[0], + expected: expected, + }, + { + name: "(any)(" + testCaseName + ")", + in: (any)(in), + expected: expected, + }, + { + name: "(any)(*" + testCaseName + ")", + in: (any)(&[]T{in}[0]), + expected: expected, + }, + { + name: "(any)((*" + testCaseName + ")(nil))", + in: (any)((*T)(nil)), + expected: "null", + }, + } +} + +type toStringTypeWithErr struct { + errOnCall []int + callCount *int + doPanic bool +} + +func (t toStringTypeWithErr) String() string { + return "{}" +} + +func (t toStringTypeWithErr) MarshalJSON() ([]byte, error) { + defer func() { + *t.callCount++ + }() + if !slices.Contains(t.errOnCall, *t.callCount) { + return []byte{'{', '}'}, nil + } + if t.doPanic { + panic(fmt.Errorf("marshal json panic'd")) + } + return nil, fmt.Errorf("marshal json failed") +} + +func TestToString(t *testing.T) { + const ( + helloWorld = "Hello, world." + ) + + testCases := []toStringTestCase{ + { + name: "nil", + in: nil, + expected: "null", + }, + } + + testCases = append(testCases, newToStringTestCases( + "Hello, world.", "Hello, world.")...) + + testCases = append(testCases, newToStringTestCasesWithTestCaseName( + byte(1), "1", "byte")...) + testCases = append(testCases, newToStringTestCasesWithTestCaseName( + 'a', "97", "rune")...) + + testCases = append(testCases, newToStringTestCases( + true, "true")...) + + testCases = append(testCases, newToStringTestCases( + complex(float32(1), float32(4)), "(1+4i)")...) + testCases = append(testCases, newToStringTestCases( + complex(float64(1), float64(4)), "(1+4i)")...) + + testCases = append(testCases, newToStringTestCases( + float32(1.1), "1.1")...) + testCases = append(testCases, newToStringTestCases( + float64(1.1), "1.1")...) + + testCases = append(testCases, newToStringTestCases( + int(1), "1")...) + testCases = append(testCases, newToStringTestCases( + int8(1), "1")...) + testCases = append(testCases, newToStringTestCases( + int16(1), "1")...) + testCases = append(testCases, newToStringTestCases( + int32(1), "1")...) + testCases = append(testCases, newToStringTestCases( + int64(1), "1")...) + + testCases = append(testCases, newToStringTestCases( + uint(1), "1")...) + testCases = append(testCases, newToStringTestCases( + uint8(1), "1")...) + testCases = append(testCases, newToStringTestCases( + uint16(1), "1")...) + testCases = append(testCases, newToStringTestCases( + uint32(1), "1")...) + testCases = append(testCases, newToStringTestCases( + uint64(1), "1")...) + + testCases = append(testCases, newToStringTestCases( + VirtualMachineConfigSpec{}, + `{"_typeName":"VirtualMachineConfigSpec"}`)...) + testCases = append(testCases, newToStringTestCasesWithTestCaseName( + VirtualMachineConfigSpec{ + VAppConfig: (*VmConfigSpec)(nil), + }, + `{"_typeName":"VirtualMachineConfigSpec","vAppConfig":null}`, + "VirtualMachineConfigSpec w nil iface")...) + + testCases = append(testCases, toStringTestCase{ + name: "MarshalJSON returns error on special encode", + in: toStringTypeWithErr{callCount: new(int), errOnCall: []int{0}}, + expected: "{}", + }) + testCases = append(testCases, toStringTestCase{ + name: "MarshalJSON returns error on special and stdlib encode", + in: toStringTypeWithErr{callCount: new(int), errOnCall: []int{0, 1}}, + expected: "{}", + }) + testCases = append(testCases, toStringTestCase{ + name: "MarshalJSON panics on special encode", + in: toStringTypeWithErr{callCount: new(int), doPanic: true, errOnCall: []int{0}}, + expected: "{}", + }) + testCases = append(testCases, toStringTestCase{ + name: "MarshalJSON panics on special and stdlib encode", + in: toStringTypeWithErr{callCount: new(int), doPanic: true, errOnCall: []int{0, 1}}, + expected: "{}", + }) + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.expected, ToString(tc.in)) + }) + } +}