Repository: vmware/govmomi
Pull request #3473 (2024-06-20 14:07:24)

api: OptionValueList helper
## Description

This patch introduces the type `OptionValueList` in the `object` package. This new type simplifies working with `[]vimtypes.BaseOptionValue` properties such as ExtraConfig.

Closes: `NA`

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

- [x] Running `go test -v -count 1 -run TestOptionValueList ./object`:

    ```shell
    === RUN   TestOptionValueList
    === RUN   TestOptionValueList/OptionValueListFromMap
    === RUN   TestOptionValueList/OptionValueListFromMap/a_nil_map_should_return_nil
    === RUN   TestOptionValueList/OptionValueListFromMap/a_map_with_string_values_should_return_OptionValues
    === RUN   TestOptionValueList/OptionValueListFromMap/a_map_with_values_of_varying_numeric_types_should_return_OptionValues
    === RUN   TestOptionValueList/OptionValueListFromMap/a_map_with_pointer_values_should_return_OptionValues
    === RUN   TestOptionValueList/Get
    === RUN   TestOptionValueList/Get/a_nil_receiver_should_not_panic_and_return_nil,_false
    === RUN   TestOptionValueList/Get/a_non-existent_key_should_return_nil,_false
    === RUN   TestOptionValueList/Get/an_existing_key_should_return_its_value,_true
    === RUN   TestOptionValueList/Get/an_existing_key_should_return_its_value,_true_when_data_includes_a_nillable_types.BaseOptionValue
    === RUN   TestOptionValueList/GetString
    === RUN   TestOptionValueList/GetString/a_nil_receiver_should_not_panic_and_return_"",_false
    === RUN   TestOptionValueList/GetString/a_non-existent_key_should_return_"",_false
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_string_value_should_return_its_string_value,_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_*string_value_that_is_not_nil_should_return_its_string_value,_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_*string_value_that_is_nil_should_return_"",_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_an_int32_value_should_return_its_string_value,_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_*uint64_value_that_is_not_nil_should_return_its_string_value,_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_*uint64_value_that_is_nil_should_return_"",_true
    === RUN   TestOptionValueList/GetString/an_existing_key_for_a_string_value_should_return_its_string_value,_true_when_data_includes_a_nillable_types.BaseOptionValue
    === RUN   TestOptionValueList/Map
    === RUN   TestOptionValueList/Map/a_nil_receiver_should_not_panic_and_return_nil
    === RUN   TestOptionValueList/Map/data_with_homogeneous_values_should_return_a_map
    === RUN   TestOptionValueList/Map/data_with_heterogeneous_values_should_return_a_map
    === RUN   TestOptionValueList/Map/data_with_just_a_nillable_types.BaseOptionValue_should_return_nil
    === RUN   TestOptionValueList/StringMap
    === RUN   TestOptionValueList/StringMap/a_nil_receiver_should_not_panic_and_return_nil
    === RUN   TestOptionValueList/StringMap/data_with_homogeneous_values_should_return_a_map
    === RUN   TestOptionValueList/StringMap/data_with_heterogeneous_values_should_return_a_map
    === RUN   TestOptionValueList/StringMap/data_with_just_a_nillable_types.BaseOptionValue_should_return_nil
    === RUN   TestOptionValueList/Additions
    === RUN   TestOptionValueList/Additions/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil
    === RUN   TestOptionValueList/Additions/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_diff
    === RUN   TestOptionValueList/Additions/a_non-nil_receiver_and_nil_input_should_return_nil
    === RUN   TestOptionValueList/Additions/a_non-nil_receiver_and_non-nil_input_should_return_the_diff
    === RUN   TestOptionValueList/Diff
    === RUN   TestOptionValueList/Diff/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil
    === RUN   TestOptionValueList/Diff/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_diff
    === RUN   TestOptionValueList/Diff/a_non-nil_receiver_and_nil_input_should_return_nil
    === RUN   TestOptionValueList/Diff/a_non-nil_receiver_and_non-nil_input_should_return_the_diff
    === RUN   TestOptionValueList/Join
    === RUN   TestOptionValueList/Join/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil
    === RUN   TestOptionValueList/Join/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_joined_data
    === RUN   TestOptionValueList/Join/a_non-nil_receiver_and_nil_input_should_return_the_joined_data
    === RUN   TestOptionValueList/Join/a_non-nil_receiver_and_non-nil_input_should_return_the_joined_data
    === RUN   TestOptionValueList/Join/a_non-nil_receiver_and_non-nil_input,_flipping_left_and_right,_should_return_the_joined_data
    --- PASS: TestOptionValueList (0.00s)
        --- PASS: TestOptionValueList/OptionValueListFromMap (0.00s)
            --- PASS: TestOptionValueList/OptionValueListFromMap/a_nil_map_should_return_nil (0.00s)
            --- PASS: TestOptionValueList/OptionValueListFromMap/a_map_with_string_values_should_return_OptionValues (0.00s)
            --- PASS: TestOptionValueList/OptionValueListFromMap/a_map_with_values_of_varying_numeric_types_should_return_OptionValues (0.00s)
            --- PASS: TestOptionValueList/OptionValueListFromMap/a_map_with_pointer_values_should_return_OptionValues (0.00s)
        --- PASS: TestOptionValueList/Get (0.00s)
            --- PASS: TestOptionValueList/Get/a_nil_receiver_should_not_panic_and_return_nil,_false (0.00s)
            --- PASS: TestOptionValueList/Get/a_non-existent_key_should_return_nil,_false (0.00s)
            --- PASS: TestOptionValueList/Get/an_existing_key_should_return_its_value,_true (0.00s)
            --- PASS: TestOptionValueList/Get/an_existing_key_should_return_its_value,_true_when_data_includes_a_nillable_types.BaseOptionValue (0.00s)
        --- PASS: TestOptionValueList/GetString (0.00s)
            --- PASS: TestOptionValueList/GetString/a_nil_receiver_should_not_panic_and_return_"",_false (0.00s)
            --- PASS: TestOptionValueList/GetString/a_non-existent_key_should_return_"",_false (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_string_value_should_return_its_string_value,_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_*string_value_that_is_not_nil_should_return_its_string_value,_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_*string_value_that_is_nil_should_return_"",_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_an_int32_value_should_return_its_string_value,_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_*uint64_value_that_is_not_nil_should_return_its_string_value,_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_*uint64_value_that_is_nil_should_return_"",_true (0.00s)
            --- PASS: TestOptionValueList/GetString/an_existing_key_for_a_string_value_should_return_its_string_value,_true_when_data_includes_a_nillable_types.BaseOptionValue (0.00s)
        --- PASS: TestOptionValueList/Map (0.00s)
            --- PASS: TestOptionValueList/Map/a_nil_receiver_should_not_panic_and_return_nil (0.00s)
            --- PASS: TestOptionValueList/Map/data_with_homogeneous_values_should_return_a_map (0.00s)
            --- PASS: TestOptionValueList/Map/data_with_heterogeneous_values_should_return_a_map (0.00s)
            --- PASS: TestOptionValueList/Map/data_with_just_a_nillable_types.BaseOptionValue_should_return_nil (0.00s)
        --- PASS: TestOptionValueList/StringMap (0.00s)
            --- PASS: TestOptionValueList/StringMap/a_nil_receiver_should_not_panic_and_return_nil (0.00s)
            --- PASS: TestOptionValueList/StringMap/data_with_homogeneous_values_should_return_a_map (0.00s)
            --- PASS: TestOptionValueList/StringMap/data_with_heterogeneous_values_should_return_a_map (0.00s)
            --- PASS: TestOptionValueList/StringMap/data_with_just_a_nillable_types.BaseOptionValue_should_return_nil (0.00s)
        --- PASS: TestOptionValueList/Additions (0.00s)
            --- PASS: TestOptionValueList/Additions/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil (0.00s)
            --- PASS: TestOptionValueList/Additions/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_diff (0.00s)
            --- PASS: TestOptionValueList/Additions/a_non-nil_receiver_and_nil_input_should_return_nil (0.00s)
            --- PASS: TestOptionValueList/Additions/a_non-nil_receiver_and_non-nil_input_should_return_the_diff (0.00s)
        --- PASS: TestOptionValueList/Diff (0.00s)
            --- PASS: TestOptionValueList/Diff/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil (0.00s)
            --- PASS: TestOptionValueList/Diff/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_diff (0.00s)
            --- PASS: TestOptionValueList/Diff/a_non-nil_receiver_and_nil_input_should_return_nil (0.00s)
            --- PASS: TestOptionValueList/Diff/a_non-nil_receiver_and_non-nil_input_should_return_the_diff (0.00s)
        --- PASS: TestOptionValueList/Join (0.00s)
            --- PASS: TestOptionValueList/Join/a_nil_receiver_and_nil_input_should_not_panic_and_return_nil (0.00s)
            --- PASS: TestOptionValueList/Join/a_nil_receiver_and_non-nil_input_should_not_panic_and_return_the_joined_data (0.00s)
            --- PASS: TestOptionValueList/Join/a_non-nil_receiver_and_nil_input_should_return_the_joined_data (0.00s)
            --- PASS: TestOptionValueList/Join/a_non-nil_receiver_and_non-nil_input_should_return_the_joined_data (0.00s)
            --- PASS: TestOptionValueList/Join/a_non-nil_receiver_and_non-nil_input,_flipping_left_and_right,_should_return_the_joined_data (0.00s)
    PASS
    ok  	github.com/vmware/govmomi/object	0.306s
    ```

- [x] Demonstrating 100% code-coverage for the new code:
<img width="536" alt="image" src="https://github.com/vmware/govmomi/assets/101085/c68611d6-3473-4c1b-b852-b3bb832edf71">
<img width="536" alt="image" src="https://github.com/vmware/govmomi/assets/101085/1ed538ae-e297-42a8-98af-cc7c9b083427">
<img width="536" alt="image" src="https://github.com/vmware/govmomi/assets/101085/ce2a6e3b-944b-43c0-ab1f-49c110a09d1c">


## Checklist:

- [x] My code follows the `CONTRIBUTION` [guidelines] of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged

[guidelines]: https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md


akutz: Thanks @dougm , I addressed everything. Please note the one length check was to avoid initializing the map unless required. I updated that. Thanks again!

Patch:
diff --git a/object/option_value_list.go b/object/option_value_list.go
new file mode 100644
index 00000000..9f91253c
--- /dev/null
+++ b/object/option_value_list.go
@@ -0,0 +1,206 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package object
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+// OptionValueList simplifies manipulation of properties that are arrays of
+// types.BaseOptionValue, such as ExtraConfig.
+type OptionValueList []types.BaseOptionValue
+
+// OptionValueListFromMap returns a new OptionValueList object from the provided
+// map.
+func OptionValueListFromMap[T any](in map[string]T) OptionValueList {
+	if len(in) == 0 {
+		return nil
+	}
+	var (
+		i   int
+		out = make(OptionValueList, len(in))
+	)
+	for k, v := range in {
+		out[i] = &types.OptionValue{Key: k, Value: v}
+		i++
+	}
+	return out
+}
+
+// Get returns the value if exists, otherwise nil is returned. The second return
+// value is a flag indicating whether the value exists or nil was the actual
+// value.
+func (ov OptionValueList) Get(key string) (any, bool) {
+	if ov == nil {
+		return nil, false
+	}
+	for i := range ov {
+		if optVal := ov[i].GetOptionValue(); optVal != nil {
+			if optVal.Key == key {
+				return optVal.Value, true
+			}
+		}
+	}
+	return nil, false
+}
+
+// GetString returns the value as a string if the value exists.
+func (ov OptionValueList) GetString(key string) (string, bool) {
+	if ov == nil {
+		return "", false
+	}
+	for i := range ov {
+		if optVal := ov[i].GetOptionValue(); optVal != nil {
+			if optVal.Key == key {
+				return getOptionValueAsString(optVal.Value), true
+			}
+		}
+	}
+	return "", false
+}
+
+// Additions returns a diff that includes only the elements from the provided
+// list that do not already exist.
+func (ov OptionValueList) Additions(in ...types.BaseOptionValue) OptionValueList {
+	return ov.diff(in, true)
+}
+
+// Diff returns a diff that includes the elements from the provided list that do
+// not already exist or have different values.
+func (ov OptionValueList) Diff(in ...types.BaseOptionValue) OptionValueList {
+	return ov.diff(in, false)
+}
+
+func (ov OptionValueList) diff(in OptionValueList, addOnly bool) OptionValueList {
+	if ov == nil && in == nil {
+		return nil
+	}
+	var (
+		out         OptionValueList
+		leftOptVals = ov.Map()
+	)
+	for i := range in {
+		if rightOptVal := in[i].GetOptionValue(); rightOptVal != nil {
+			k, v := rightOptVal.Key, rightOptVal.Value
+			if ov == nil {
+				out = append(out, &types.OptionValue{Key: k, Value: v})
+			} else if leftOptVal, ok := leftOptVals[k]; !ok {
+				out = append(out, &types.OptionValue{Key: k, Value: v})
+			} else if !addOnly && v != leftOptVal {
+				out = append(out, &types.OptionValue{Key: k, Value: v})
+			}
+		}
+	}
+	if len(out) == 0 {
+		return nil
+	}
+	return out
+}
+
+// Join combines this list with the provided one and returns the result, joining
+// the two lists on their shared keys.
+// Please note, Join(left, right) means the values from right will be appended
+// to left, without overwriting any values that have shared keys. To overwrite
+// the shared keys in left from right, use Join(right, left) instead.
+func (ov OptionValueList) Join(in ...types.BaseOptionValue) OptionValueList {
+	var (
+		out     OptionValueList
+		outKeys map[string]struct{}
+	)
+
+	// Init the out slice from the left side.
+	if len(ov) > 0 {
+		outKeys = map[string]struct{}{}
+		for i := range ov {
+			if optVal := ov[i].GetOptionValue(); optVal != nil {
+				kv := &types.OptionValue{Key: optVal.Key, Value: optVal.Value}
+				out = append(out, kv)
+				outKeys[optVal.Key] = struct{}{}
+			}
+		}
+	}
+
+	// Join the values from the right side.
+	for i := range in {
+		if rightOptVal := in[i].GetOptionValue(); rightOptVal != nil {
+			k, v := rightOptVal.Key, rightOptVal.Value
+			if _, ok := outKeys[k]; !ok {
+				out = append(out, &types.OptionValue{Key: k, Value: v})
+			}
+		}
+	}
+
+	if len(out) == 0 {
+		return nil
+	}
+
+	return out
+}
+
+// Map returns the list of option values as a map. A nil value is returned if
+// the list is empty.
+func (ov OptionValueList) Map() map[string]any {
+	if len(ov) == 0 {
+		return nil
+	}
+	out := map[string]any{}
+	for i := range ov {
+		if optVal := ov[i].GetOptionValue(); optVal != nil {
+			out[optVal.Key] = optVal.Value
+		}
+	}
+	if len(out) == 0 {
+		return nil
+	}
+	return out
+}
+
+// StringMap returns the list of option values as a map where the values are
+// strings. A nil value is returned if the list is empty.
+func (ov OptionValueList) StringMap() map[string]string {
+	if len(ov) == 0 {
+		return nil
+	}
+	out := map[string]string{}
+	for i := range ov {
+		if optVal := ov[i].GetOptionValue(); optVal != nil {
+			out[optVal.Key] = getOptionValueAsString(optVal.Value)
+		}
+	}
+	if len(out) == 0 {
+		return nil
+	}
+	return out
+}
+
+func getOptionValueAsString(val any) string {
+	switch tval := val.(type) {
+	case string:
+		return tval
+	default:
+		if rv := reflect.ValueOf(val); rv.Kind() == reflect.Pointer {
+			if rv.IsNil() {
+				return ""
+			}
+			return fmt.Sprintf("%v", rv.Elem().Interface())
+		}
+		return fmt.Sprintf("%v", tval)
+	}
+}


Test patch:
diff --git a/object/option_value_list_test.go b/object/option_value_list_test.go
new file mode 100644
index 00000000..57d979e2
--- /dev/null
+++ b/object/option_value_list_test.go
@@ -0,0 +1,603 @@
+/*
+Copyright (c) 2024-2024 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package object_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type nillableOptionValue struct{}
+
+func (ov nillableOptionValue) GetOptionValue() *types.OptionValue {
+	return nil
+}
+
+func TestOptionValueList(t *testing.T) {
+	const (
+		sza   = "a"
+		szb   = "b"
+		szc   = "c"
+		szd   = "d"
+		sze   = "e"
+		sz1   = "1"
+		sz2   = "2"
+		sz3   = "3"
+		sz4   = "4"
+		sz5   = "5"
+		i32_1 = int32(1)
+		u64_2 = uint64(2)
+		f32_3 = float32(3)
+		f64_4 = float64(4)
+		b_5   = byte(5) //nolint:revive,stylecheck
+	)
+
+	var (
+		psz1   = &[]string{sz1}[0]
+		pu64_2 = &[]uint64{u64_2}[0]
+		pf32_3 = &[]float32{f32_3}[0]
+		pb_5   = &[]byte{b_5}[0] //nolint:revive,stylecheck
+	)
+
+	t.Run("OptionValueListFromMap", func(t *testing.T) {
+
+		t.Run("a nil map should return nil", func(t *testing.T) {
+			assert.Nil(t, object.OptionValueListFromMap[any](nil))
+		})
+
+		t.Run("a map with string values should return OptionValues", func(t *testing.T) {
+			assert.ElementsMatch(
+				t,
+				object.OptionValueListFromMap(map[string]string{
+					szc: sz3,
+					sza: sz1,
+					szb: sz2,
+				}),
+				[]types.BaseOptionValue{
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+			)
+		})
+
+		t.Run("a map with values of varying numeric types should return OptionValues", func(t *testing.T) {
+			assert.ElementsMatch(
+				t,
+				object.OptionValueListFromMap(map[string]any{
+					szc: f32_3,
+					sza: i32_1,
+					szb: u64_2,
+				}),
+				[]types.BaseOptionValue{
+					&types.OptionValue{Key: szb, Value: u64_2},
+					&types.OptionValue{Key: sza, Value: i32_1},
+					&types.OptionValue{Key: szc, Value: f32_3},
+				},
+			)
+		})
+
+		t.Run("a map with pointer values should return OptionValues", func(t *testing.T) {
+			assert.ElementsMatch(
+				t,
+				object.OptionValueListFromMap(map[string]any{
+					szc: pf32_3,
+					sza: psz1,
+					szb: pu64_2,
+				}),
+				[]types.BaseOptionValue{
+					&types.OptionValue{Key: szb, Value: pu64_2},
+					&types.OptionValue{Key: sza, Value: psz1},
+					&types.OptionValue{Key: szc, Value: pf32_3},
+				},
+			)
+		})
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		testCases := []struct {
+			name string
+			left object.OptionValueList
+			key  string
+			out  any
+			ok   bool
+		}{
+			{
+				name: "a nil receiver should not panic and return nil, false",
+				left: nil,
+				key:  "",
+				out:  nil,
+				ok:   false,
+			},
+			{
+				name: "a non-existent key should return nil, false",
+				left: object.OptionValueList{},
+				key:  "",
+				out:  nil,
+				ok:   false,
+			},
+			{
+				name: "an existing key should return its value, true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+			{
+				name: "an existing key should return its value, true when data includes a nillable types.BaseOptionValue",
+				left: object.OptionValueList{
+					nillableOptionValue{},
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var (
+					out any
+					ok  bool
+				)
+				assert.NotPanics(t, func() { out, ok = tc.left.Get(tc.key) })
+				assert.Equal(t, tc.out, out)
+				assert.Equal(t, tc.ok, ok)
+			})
+		}
+	})
+
+	t.Run("GetString", func(t *testing.T) {
+		testCases := []struct {
+			name string
+			left object.OptionValueList
+			key  string
+			out  string
+			ok   bool
+		}{
+			{
+				name: "a nil receiver should not panic and return \"\", false",
+				left: nil,
+				key:  "",
+				out:  "",
+				ok:   false,
+			},
+			{
+				name: "a non-existent key should return \"\", false",
+				left: object.OptionValueList{},
+				key:  "",
+				out:  "",
+				ok:   false,
+			},
+			{
+				name: "an existing key for a string value should return its string value, true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+			{
+				name: "an existing key for a *string value that is not nil should return its string value, true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: psz1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+			{
+				name: "an existing key for a *string value that is nil should return \"\", true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: (*string)(nil)},
+				},
+				key: sza,
+				out: "",
+				ok:  true,
+			},
+			{
+				name: "an existing key for an int32 value should return its string value, true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: i32_1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+			{
+				name: "an existing key for a *uint64 value that is not nil should return its string value, true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: pu64_2},
+				},
+				key: sza,
+				out: sz2,
+				ok:  true,
+			},
+			{
+				name: "an existing key for a *uint64 value that is nil should return \"\", true",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: (*uint64)(nil)},
+				},
+				key: sza,
+				out: "",
+				ok:  true,
+			},
+			{
+				name: "an existing key for a string value should return its string value, true when data includes a nillable types.BaseOptionValue",
+				left: object.OptionValueList{
+					nillableOptionValue{},
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				key: sza,
+				out: sz1,
+				ok:  true,
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var (
+					out string
+					ok  bool
+				)
+				assert.NotPanics(t, func() { out, ok = tc.left.GetString(tc.key) })
+				assert.Equal(t, tc.out, out)
+				assert.Equal(t, tc.ok, ok)
+			})
+		}
+	})
+
+	t.Run("Map", func(t *testing.T) {
+		testCases := []struct {
+			name string
+			left object.OptionValueList
+			out  map[string]any
+		}{
+			{
+				name: "a nil receiver should not panic and return nil",
+				left: nil,
+				out:  nil,
+			},
+			{
+				name: "data with homogeneous values should return a map",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				out: map[string]any{
+					sza: sz1,
+				},
+			},
+			{
+				name: "data with heterogeneous values should return a map",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: u64_2},
+					&types.OptionValue{Key: szc, Value: pf32_3},
+				},
+				out: map[string]any{
+					sza: sz1,
+					szb: u64_2,
+					szc: pf32_3,
+				},
+			},
+			{
+				name: "data with just a nillable types.BaseOptionValue should return nil",
+				left: object.OptionValueList{
+					nillableOptionValue{},
+				},
+				out: nil,
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var out map[string]any
+				assert.NotPanics(t, func() { out = tc.left.Map() })
+				assert.Equal(t, tc.out, out)
+			})
+		}
+	})
+
+	t.Run("StringMap", func(t *testing.T) {
+		testCases := []struct {
+			name string
+			left object.OptionValueList
+			out  map[string]string
+		}{
+			{
+				name: "a nil receiver should not panic and return nil",
+				left: nil,
+				out:  nil,
+			},
+			{
+				name: "data with homogeneous values should return a map",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+				},
+				out: map[string]string{
+					sza: sz1,
+				},
+			},
+			{
+				name: "data with heterogeneous values should return a map",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: u64_2},
+					&types.OptionValue{Key: szc, Value: pf32_3},
+				},
+				out: map[string]string{
+					sza: sz1,
+					szb: sz2,
+					szc: sz3,
+				},
+			},
+			{
+				name: "data with just a nillable types.BaseOptionValue should return nil",
+				left: object.OptionValueList{
+					nillableOptionValue{},
+				},
+				out: nil,
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var out map[string]string
+				assert.NotPanics(t, func() { out = tc.left.StringMap() })
+				assert.Equal(t, tc.out, out)
+			})
+		}
+	})
+
+	t.Run("Additions", func(t *testing.T) {
+		testCases := []struct {
+			name  string
+			left  object.OptionValueList
+			right object.OptionValueList
+			out   object.OptionValueList
+		}{
+			{
+				name:  "a nil receiver and nil input should not panic and return nil",
+				left:  nil,
+				right: nil,
+				out:   nil,
+			},
+			{
+				name: "a nil receiver and non-nil input should not panic and return the diff",
+				left: nil,
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+			{
+				name: "a non-nil receiver and nil input should return nil",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: nil,
+				out:   nil,
+			},
+			{
+				name: "a non-nil receiver and non-nil input should return the diff",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var out object.OptionValueList
+				assert.NotPanics(t, func() { out = tc.left.Additions(tc.right...) })
+				assert.Equal(t, tc.out, out)
+			})
+		}
+	})
+
+	t.Run("Diff", func(t *testing.T) {
+		testCases := []struct {
+			name  string
+			left  object.OptionValueList
+			right object.OptionValueList
+			out   object.OptionValueList
+		}{
+			{
+				name:  "a nil receiver and nil input should not panic and return nil",
+				left:  nil,
+				right: nil,
+				out:   nil,
+			},
+			{
+				name: "a nil receiver and non-nil input should not panic and return the diff",
+				left: nil,
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+			{
+				name: "a non-nil receiver and nil input should return nil",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: nil,
+				out:   nil,
+			},
+			{
+				name: "a non-nil receiver and non-nil input should return the diff",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var out object.OptionValueList
+				assert.NotPanics(t, func() { out = tc.left.Diff(tc.right...) })
+				assert.Equal(t, tc.out, out)
+			})
+		}
+	})
+
+	t.Run("Join", func(t *testing.T) {
+		testCases := []struct {
+			name  string
+			left  object.OptionValueList
+			right object.OptionValueList
+			out   object.OptionValueList
+		}{
+			{
+				name:  "a nil receiver and nil input should not panic and return nil",
+				left:  nil,
+				right: nil,
+				out:   nil,
+			},
+			{
+				name: "a nil receiver and non-nil input should not panic and return the joined data",
+				left: nil,
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+			{
+				name: "a non-nil receiver and nil input should return the joined data",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: nil,
+				out: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+			},
+			{
+				name: "a non-nil receiver and non-nil input should return the joined data",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				right: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+			},
+			{
+				name: "a non-nil receiver and non-nil input, flipping left and right, should return the joined data",
+				left: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+				},
+				right: object.OptionValueList{
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szb, Value: sz2},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+				out: object.OptionValueList{
+					&types.OptionValue{Key: szb, Value: ""},
+					&types.OptionValue{Key: szd, Value: f64_4},
+					&types.OptionValue{Key: sze, Value: pb_5},
+					&types.OptionValue{Key: sza, Value: sz1},
+					&types.OptionValue{Key: szc, Value: sz3},
+				},
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				var out object.OptionValueList
+				assert.NotPanics(t, func() { out = tc.left.Join(tc.right...) })
+				assert.Equal(t, tc.out, out)
+			})
+		}
+	})
+}