Skip to content

Commit

Permalink
api: ToString for vim types
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
akutz committed Aug 26, 2024
1 parent b5a65e8 commit 8491321
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 0 deletions.
75 changes: 75 additions & 0 deletions vim25/types/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ limitations under the License.
package types

import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
Expand Down Expand Up @@ -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.
Expand Down
167 changes: 167 additions & 0 deletions vim25/types/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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))
})
}
}

0 comments on commit 8491321

Please sign in to comment.