port.go

  1package apijson
  2
  3import (
  4	"fmt"
  5	"reflect"
  6)
  7
  8// Port copies over values from one struct to another struct.
  9func Port(from any, to any) error {
 10	toVal := reflect.ValueOf(to)
 11	fromVal := reflect.ValueOf(from)
 12
 13	if toVal.Kind() != reflect.Ptr || toVal.IsNil() {
 14		return fmt.Errorf("destination must be a non-nil pointer")
 15	}
 16
 17	for toVal.Kind() == reflect.Ptr {
 18		toVal = toVal.Elem()
 19	}
 20	toType := toVal.Type()
 21
 22	for fromVal.Kind() == reflect.Ptr {
 23		fromVal = fromVal.Elem()
 24	}
 25	fromType := fromVal.Type()
 26
 27	if toType.Kind() != reflect.Struct {
 28		return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind())
 29	}
 30
 31	values := map[string]reflect.Value{}
 32	fields := map[string]reflect.Value{}
 33
 34	fromJSON := fromVal.FieldByName("JSON")
 35	toJSON := toVal.FieldByName("JSON")
 36
 37	// Iterate through the fields of v and load all the "normal" fields in the struct to the map of
 38	// string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j.
 39	var getFields func(t reflect.Type, v reflect.Value)
 40	getFields = func(t reflect.Type, v reflect.Value) {
 41		j := v.FieldByName("JSON")
 42
 43		// Recurse into anonymous fields first, since the fields on the object should win over the fields in the
 44		// embedded object.
 45		for i := 0; i < t.NumField(); i++ {
 46			field := t.Field(i)
 47			if field.Anonymous {
 48				getFields(field.Type, v.Field(i))
 49				continue
 50			}
 51		}
 52
 53		for i := 0; i < t.NumField(); i++ {
 54			field := t.Field(i)
 55			ptag, ok := parseJSONStructTag(field)
 56			if !ok || ptag.name == "-" || ptag.name == "" {
 57				continue
 58			}
 59			values[ptag.name] = v.Field(i)
 60			if j.IsValid() {
 61				fields[ptag.name] = j.FieldByName(field.Name)
 62			}
 63		}
 64	}
 65	getFields(fromType, fromVal)
 66
 67	// Use the values from the previous step to populate the 'to' struct.
 68	for i := 0; i < toType.NumField(); i++ {
 69		field := toType.Field(i)
 70		ptag, ok := parseJSONStructTag(field)
 71		if !ok {
 72			continue
 73		}
 74		if ptag.name == "-" {
 75			continue
 76		}
 77		if value, ok := values[ptag.name]; ok {
 78			delete(values, ptag.name)
 79			if field.Type.Kind() == reflect.Interface {
 80				toVal.Field(i).Set(value)
 81			} else {
 82				switch value.Kind() {
 83				case reflect.String:
 84					toVal.Field(i).SetString(value.String())
 85				case reflect.Bool:
 86					toVal.Field(i).SetBool(value.Bool())
 87				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 88					toVal.Field(i).SetInt(value.Int())
 89				case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 90					toVal.Field(i).SetUint(value.Uint())
 91				case reflect.Float32, reflect.Float64:
 92					toVal.Field(i).SetFloat(value.Float())
 93				default:
 94					toVal.Field(i).Set(value)
 95				}
 96			}
 97		}
 98
 99		if fromJSONField, ok := fields[ptag.name]; ok {
100			if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() {
101				toJSONField.Set(fromJSONField)
102			}
103		}
104	}
105
106	// Finally, copy over the .JSON.raw and .JSON.ExtraFields
107	if toJSON.IsValid() {
108		if raw := toJSON.FieldByName("raw"); raw.IsValid() {
109			setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON())
110		}
111
112		if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() {
113			if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() {
114				setUnexportedField(toExtraFields, fromExtraFields.Interface())
115			}
116		}
117	}
118
119	return nil
120}