Skip to content

Commit

Permalink
Merge pull request #3529 from akutz/feature/vimtype-to-string
Browse files Browse the repository at this point in the history
api: ToString for vim types
  • Loading branch information
akutz committed Aug 26, 2024
2 parents b5a65e8 + 8491321 commit 7be4a88
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 7be4a88

Please sign in to comment.