From 4d59a554970de40dabfb4e2d4dca023810529a15 Mon Sep 17 00:00:00 2001 From: Jan Semmelink <jan@uafrica.com> Date: Thu, 4 Nov 2021 07:56:36 +0200 Subject: [PATCH] More reflection utilities --- reflection/get.go | 69 +++++++++++++++++ reflection/reflection.go | 2 +- reflection/type_clone.go | 68 +++++++++++++++++ reflection/type_clone_test.go | 134 ++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 reflection/get.go create mode 100644 reflection/type_clone.go create mode 100644 reflection/type_clone_test.go diff --git a/reflection/get.go b/reflection/get.go new file mode 100644 index 0000000..99349f1 --- /dev/null +++ b/reflection/get.go @@ -0,0 +1,69 @@ +package reflection + +import ( + "fmt" + "reflect" + "strings" + + "gitlab.com/uafrica/go-utils/errors" +) + +func Get(v reflect.Value, key string) (reflect.Value, error) { + return get("", v, key) +} + +func get(name string, v reflect.Value, key string) (reflect.Value, error) { + if key == "" { + return v, nil + } + + switch v.Kind() { + case reflect.Ptr: + return get(name, v.Elem(), key) + + case reflect.Struct: + if key[0] != '.' { + return v, errors.Errorf("get(%s): key=\"%s\" does not start with '.'", name, key) + } + fieldName := key[1:] + remainingKey := "" + index := strings.IndexAny(fieldName, ".[") + if index > 0 { + fieldName = key[1 : index+1] + remainingKey = key[index+1:] + } + t := v.Type() + fieldIndex := 0 + for fieldIndex = 0; fieldIndex < t.NumField(); fieldIndex++ { + if strings.SplitN(t.Field(fieldIndex).Tag.Get("json"), ",", 2)[0] == fieldName { + break + } + } + if fieldIndex >= t.NumField() { + return v, errors.Errorf("%s does not have field %s", name, fieldName) + } + return get(name+"."+fieldName, v.Field(fieldIndex), remainingKey) + + case reflect.Slice: + if !strings.HasPrefix(key, "[]") { + return v, errors.Errorf("canot get %s from slice, expecting \"[]\" in the key", key) + } + + //make array of results from each item in the slice + var result reflect.Value + for i := 0; i < v.Len(); i++ { + if vv, err := get(fmt.Sprintf("%s[%d]", name, i), v.Index(i), key[2:]); err != nil { + return v, errors.Wrapf(err, "failed on %s[%d]", name, i) + } else { + if !result.IsValid() { + result = reflect.MakeSlice(reflect.SliceOf(vv.Type()), 0, v.Len()) + } + result = reflect.Append(result, vv) + } + } + return result, nil + + default: + } + return v, errors.Errorf("Cannot get %s from %s", key, v.Kind()) +} diff --git a/reflection/reflection.go b/reflection/reflection.go index 14faf28..b5ddd25 100644 --- a/reflection/reflection.go +++ b/reflection/reflection.go @@ -52,7 +52,7 @@ func SetString(field reflect.Value, value string) { return // Field doesn't exist } if field.Kind() != reflect.String { - logger.Error("Claims: Field is not of type String: %v", field.Kind()) + logger.Errorf("Claims: Field is not of type String: %v", field.Kind()) return } field.SetString(value) diff --git a/reflection/type_clone.go b/reflection/type_clone.go new file mode 100644 index 0000000..9cc2d1c --- /dev/null +++ b/reflection/type_clone.go @@ -0,0 +1,68 @@ +package reflection + +import ( + "reflect" + "strings" + + "gitlab.com/uafrica/go-utils/errors" +) + +func CloneType(t reflect.Type, replace map[string]reflect.Type) (reflect.Type, error) { + return clone("", t, replace) +} + +func clone(name string, t reflect.Type, replace map[string]reflect.Type) (reflect.Type, error) { + switch t.Kind() { + case reflect.Struct: + fields := []reflect.StructField{} + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + n := strings.SplitN(f.Tag.Get("json"), ",", 2)[0] //exclude ,omitempty... + if n == "" { + n = f.Name + } + fieldName := name + "." + n + for replaceName, replaceType := range replace { + if strings.HasPrefix(replaceName, fieldName) { + if replaceName == fieldName { + f.Type = replaceType + } else { + clonedType, err := clone(fieldName, f.Type, replace) + if err != nil { + return t, errors.Wrapf(err, "failed to clone %s", fieldName) + } + f.Type = clonedType + } + } + } + if newType, ok := replace[fieldName]; ok { + f.Type = newType + } + fields = append(fields, f) + } + return reflect.StructOf(fields), nil + + case reflect.Slice: + for replaceName, replaceType := range replace { + if strings.HasPrefix(replaceName, name+"[]") { + if replaceName == name+"[]" { + //full match + return replaceType, nil + } else { + elemType, err := clone(name+"[]", t.Elem(), replace) + if err != nil { + return t, errors.Wrapf(err, "failed to clone slice elem type") + } + return reflect.SliceOf(elemType), nil + } + } else { + return t, nil + } + } + + return t, errors.Errorf("NYI") + + default: + } + return t, errors.Errorf("cannot clone %v %s", t.Kind(), t.Name()) +} diff --git a/reflection/type_clone_test.go b/reflection/type_clone_test.go new file mode 100644 index 0000000..474bc09 --- /dev/null +++ b/reflection/type_clone_test.go @@ -0,0 +1,134 @@ +package reflection_test + +import ( + "encoding/json" + "reflect" + "testing" + + "gitlab.com/uafrica/go-utils/reflection" +) + +func Test1(t *testing.T) { + doc := ` + { + "took":872, + "timed_out":false, + "_shards":{ + "total":38, + "successful":38, + "skipped":0, + "failed":0 + }, + "hits":{ + "total":{ + "value":10, + "relation":"eq" + }, + "max_score":null, + "hits":[ + { + "_index": "go-utils-audit-test-20211030", + "_type": "_doc", + "_id": "Tj9l5XwBWRiAneoYazic", + "_score": 1.2039728, + "_source": { + "@timestamp": "2021-10-30T15:03:20.000000+02:00", + "@end_time": "2021-10-30T15:03:21.000000+02:00", + "@duration_ms": 1000, + "test1": "6", + "test2": "ACC_00098", + "test3": 10, + "http": { + "method": "GET", + "path": "/accounts" + }, + "http_method": "GET", + "http_path": "/accounts" + } + } + ] + } + } + ` + + //using default type, documents in _source:{} are parsed to map[string]interface{} + res := SearchResponseBody{} + if err := json.Unmarshal([]byte(doc), &res); err != nil { + t.Fatalf("cannot unmarshal response into default type: %+v", err) + } + for hitIndex, hit := range res.Hits.Hits { + t.Logf("doc[%d]: (%T)%+v", hitIndex, hit.Source, hit.Source) + } + + //create type with own document type to use for _source field: + cloned, err := reflection.CloneType( + reflect.TypeOf(SearchResponseBody{}), + map[string]reflect.Type{ + ".hits.hits[]._source": reflect.TypeOf(myDoc{}), + }) + if err != nil { + t.Fatal(err) + } + t.Logf("cloned type: %v", cloned) + + //unmarshal using new type to have correct type for each hit + resPtrValue := reflect.New(cloned) + if err := json.Unmarshal([]byte(doc), resPtrValue.Interface()); err != nil { + t.Fatalf("failed to decode into cloned type: %+v", err) + } + // clonedRes := resPtrValue.Interface() + // t.Logf("Coned res: %+v", clonedRes) + + //get the replaced values as an array of docs + v, err := reflection.Get(resPtrValue, ".hits.hits[]._source") + if err != nil { + t.Fatalf("Did not get list: %+v", err) + } + docs, ok := v.Interface().([]myDoc) + if !ok { + t.Fatalf("%T is not []myDoc", v.Interface()) + } + if len(docs) != 1 { + t.Fatalf("Got %d != 1", len(docs)) + } + for _, doc := range docs { + t.Logf("doc: (%T)%+v", doc, doc) + } +} + +type SearchResponseBody struct { + Took int `json:"took"` //milliseconds + TimedOut bool `json:"timed_out"` + Shards SearchResponseShards `json:"_shards"` + Hits SearchResponseHits `json:"hits"` +} + +type SearchResponseShards struct { + Total int `json:"total"` + Successful int `json:"successful"` + Skipped int `json:"skipped"` + Failed int `json:"failed"` +} + +type SearchResponseHits struct { + Total SearchResponseHitsTotal `json:"total"` + MaxScore *float64 `json:"max_score,omitempty"` + Hits []HitDoc `json:"hits"` +} + +type SearchResponseHitsTotal struct { + Value int `json:"value"` //e.g. 0 when no docs matched + Relation string `json:"relation"` //e.g. "eq" +} + +type HitDoc struct { + Index string `json:"_index"` //name of index + Type string `json:"_type"` //_doc + ID string `json:"_id"` + Score float64 `json:"_score"` // + Source map[string]interface{} `json:"_source"` //the document of itemType +} + +type myDoc struct { + Test1 string `json:"test1"` +} -- GitLab