Skip to content
Snippets Groups Projects
Commit 4d59a554 authored by Jan Semmelink's avatar Jan Semmelink
Browse files

More reflection utilities

parent 54e1dee7
No related branches found
No related tags found
1 merge request!6Search package improvements to retrieve documents with text searches from OpenSearch
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())
}
......@@ -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)
......
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())
}
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"`
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment