Created
December 25, 2024 15:30
-
-
Save ericjster/70bf443e97e948aca53708ca1f8d9da7 to your computer and use it in GitHub Desktop.
ej_241223_merge_options_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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