Skip to content
Snippets Groups Projects
type_clone.go 3.52 KiB
Newer Older
package reflection

import (
	"reflect"
	"strings"

	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
// CloneType() clones a type into a new type, replacing some elements of it
// Paramters:
//	t is the existing type to be cloned
//	replace is a list of items to replace with their new types
//			use jq key notation
//			if this is empty map/nil, it will clone the type without changes
//	cloned type or error
//
// Example:
//		newType,err := reflection.CloneType(
//			reflect.TypeOf(myStruct{}),
//			map[string]reflect.Type{
//				".field1": reflect.TypeOf(float64(0)),
//				".list[].name": reflect.TypeOf(int64(0)),
//			},
//		)
//
// This is not a function you will use everyday, but very useful when needed
// See example usage in search to read OpenSearch responses with the correct
// struct type used to parse ".hits.hits[]._source" so that we do not have to
// unmarshal the JSON, then marshal each _source from map[string]interface{}
// back to json then unmarshal again into the correct type!
// It saves a lot of overhead CPU doing it all at once using the correct type
// nested deep into the response body type.
// this function was written for above case... it will likely need extension
// if we want to replace all kinds of other fields, but it meets the current
// requirements.
// After parsing, use reflection.Get() with the same key notation to get the
// result from the nested document.
// Note current shortcoming: partial matching will apply if you have two
// similarly named fields, e.g. "name" and "name2", then replace instruction
// on "name" may partial match name2. To fix this, we need better function
// than strings.HasPrefix() to check for delimiter/end of name to do full
// word matches, and we need to extend the test to illustrate this.
func CloneType(t reflect.Type, replace map[string]reflect.Type) (reflect.Type, error) {
	cloned, err := clone("", t, replace)
	if err != nil {
		return t, err
	}
	if len(replace) > 0 {
		return t, errors.Errorf("unknown replacements: %+v", replace)
	}
	return cloned, nil
}

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
						delete(replace, replaceName)
					} 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
					delete(replace, replaceName)
					return replaceType, nil
				}
				// partial match
				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

	default:
	}
	return t, errors.Errorf("cannot clone %v %s", t.Kind(), t.Name())
}