Skip to content

Instantly share code, notes, and snippets.

@ericjster
Created December 25, 2024 15:30
Show Gist options
  • Select an option

  • Save ericjster/70bf443e97e948aca53708ca1f8d9da7 to your computer and use it in GitHub Desktop.

Select an option

Save ericjster/70bf443e97e948aca53708ca1f8d9da7 to your computer and use it in GitHub Desktop.
ej_241223_merge_options_test.go
package main
import (
"encoding/json"
"fmt"
"math"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
type example struct {
S string `json:"tag_s"`
I int `json:"tag_i"`
I32 int32 `json:"tag_i32"`
F64 float64 `json:"tag_f64"`
B bool `json:"tag_b"`
ipriv int `json:"tag_ipriv"` // Private, not addressable, cannot call Set.
}
func TestOptionParse(t *testing.T) {
// See the json annotations in the struct definitions in cassandraops.pb.go.
tests := []struct {
name string
specMain interface{}
specOptions string // json string
specExpected interface{}
expectedErr string
}{
{
name: "empty options",
specMain: &example{
S: "mystring",
I: 123456,
I32: 123567890,
F64: math.Pi,
B: true,
},
specOptions: "",
specExpected: &example{
S: "mystring",
I: 123456,
I32: 123567890,
F64: math.Pi,
B: true,
},
expectedErr: "",
},
{
name: "empty base",
specMain: &example{},
specOptions: `{
"tag_s": "mystring",
"tag_i": 123456,
"tag_i32": 123567890,
"tag_f64": 3.1415926536,
"tag_b": true
}`,
specExpected: &example{
S: "mystring",
I: 123456,
I32: 123567890,
F64: 3.1415926536,
B: true,
},
expectedErr: "",
},
{
name: "overwrite options",
specMain: &example{
S: "mystring",
I: 123456,
I32: 123567890,
F64: math.Pi,
B: true,
},
specOptions: `{
"tag_s": "mystring2",
"tag_i": 666666,
"tag_i32": 999999999,
"tag_f64": 2.71828183,
"tag_b": false
}`,
specExpected: &example{
S: "mystring2",
I: 666666,
I32: 999999999,
F64: 2.71828183,
B: false,
},
expectedErr: "",
},
{
name: "not settable",
specMain: &example{
ipriv: 111,
},
specOptions: `{
"tag_ipriv": 222
}`,
specExpected: &example{},
expectedErr: "not settable",
},
{
name: "different type",
specMain: &example{},
specOptions: `{
"tag_s": 333,
"tag_i": "not valid int",
"tag_i32": 3.333,
"tag_f64": "not valid float",
"tag_b": "not valid bool"
}`,
specExpected: &example{},
expectedErr: "different type",
},
{
name: "no field found",
specMain: &example{},
specOptions: `{
"tag_x": "my x value"
}`,
specExpected: &example{},
expectedErr: "no field found",
},
{
name: "cannot unmarshal json",
specMain: &example{},
specOptions: `{
"tag_x": "extra comma at end",,,,
}`,
specExpected: &example{},
expectedErr: "cannot unmarshal json",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fmt.Printf("test:%v\n", test.name)
specFinal := test.specMain
err := MergeOptions(specFinal, test.specOptions)
if test.expectedErr == "" {
assert.Equal(t, nil, err)
assert.Equal(t, test.specExpected, specFinal)
} else {
assert.ErrorContains(t, err, test.expectedErr)
}
})
}
}
// Merge a json set of options into an existing struct.
// specMain must be a pointer to a struct.
// We only support a single level struct.
func MergeOptions(specMain interface{}, specOptionsJson string) error {
// Special case of empty string, which is not valid json.
if specOptionsJson == "" {
specOptionsJson = "{}"
}
// Json into generic key value.
mapOptions := map[string]interface{}{}
err := json.Unmarshal([]byte(specOptionsJson), &mapOptions)
if err != nil {
return fmt.Errorf("cannot unmarshal json options: %v, error: %s", specOptionsJson, err.Error())
}
fmt.Printf("mapOptions:%+v\n", mapOptions)
// Merge json options into the struct.
for jsonKey, value := range mapOptions {
fmt.Printf("json options: key:%v, value:%v\n", jsonKey, value)
err = setFieldByTag(specMain, "json", jsonKey, value)
if err != nil {
return err
}
}
return nil
}
// Set a field in struct 's' using a tagName (e.g. "json").
func setFieldByTag(s interface{}, tagName, tagValue string, value interface{}) error {
v := reflect.ValueOf(s)
// Ensure we are setting a field on a struct.
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("must be a pointer to a struct")
}
// Dereference the pointer.
v = v.Elem()
// Iterate over struct fields
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
// Check if the field has the specified tag, e.g. "json"
if tag, ok := field.Tag.Lookup(tagName); ok && tag == tagValue {
fieldValue := v.Field(i)
// Check if the field is settable (addressable, public)
if !fieldValue.CanSet() {
return fmt.Errorf("field '%s' is not settable", field.Name)
}
jsonValue := reflect.ValueOf(value)
fmt.Printf("dbg224a field '%s', struct field kind:%v (value %T %v), json value kind:%v (value %T %v)\n",
field.Name,
fieldValue.Kind(),
fieldValue.Interface(),
fieldValue.Interface(),
jsonValue.Kind(),
value,
value)
// Can we convert json value to the struct field type?
// Json numbers become float64.
if jsonValue.CanConvert(fieldValue.Type()) {
newValue := jsonValue.Convert(fieldValue.Type())
fmt.Printf("dbg224b field '%s', struct field kind:%v (value %T %v), json value kind:%v (value %T %v)\n",
field.Name,
fieldValue.Kind(),
fieldValue.Interface(),
fieldValue.Interface(),
jsonValue.Kind(),
newValue,
newValue)
fieldValue.Set(newValue)
return nil
} else {
return fmt.Errorf("field '%s' has a different type than the provided value, struct field kind:%v (value %T %v), json value kind:%v (value %T %v)\n",
field.Name,
fieldValue.Kind(),
fieldValue.Interface(),
fieldValue.Interface(),
jsonValue.Kind(),
value,
value)
}
}
}
return fmt.Errorf("no field found with tag '%s' and value '%s'", tagName, tagValue)
}
module github.com/ericjster/ej_241223_merge_options
go 1.22.5
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment