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)) + }) + } +}