Skip to content

Instantly share code, notes, and snippets.

@blakelead
Last active July 9, 2020 11:42
Show Gist options
  • Save blakelead/10e31bd46875323430bba96b75eaf66f to your computer and use it in GitHub Desktop.
Save blakelead/10e31bd46875323430bba96b75eaf66f to your computer and use it in GitHub Desktop.
Learning reflection in go by mimicking yaml.Unmarshal
field: value
package main
import (
"fmt"
"reflect"
)
type Config struct {
Field string `file:"field"`
}
func main() {
blob, _ := read("config.yaml")
var in interface{}
yaml.Unmarshal(blob, &in)
config := Config{}
Unmarshal(in, &config)
}
// Unmarshal populates `dst` structure with `in` data
func Unmarshal(in interface{}, dst interface{}) {
src := FlattenInterface(&in)
obj := reflect.ValueOf(dst)
unmarshalWithPrefix(src, obj, "")
}
func unmarshalWithPrefix(src map[string]interface{}, obj reflect.Value, prefix string) {
switch obj.Kind() {
case reflect.Ptr, reflect.Interface:
val := obj.Elem()
unmarshalWithPrefix(src, val, "")
case reflect.Struct:
for i := 0; i < obj.NumField(); i++ {
if name, ok := obj.Type().Field(i).Tag.Lookup("file"); ok {
unmarshalWithPrefix(src, obj.Field(i), fmt.Sprintf("%s.%s", prefix, name))
}
}
case reflect.Slice:
if val, found := src[prefix]; found {
v := reflect.ValueOf(val)
if v.Kind() == reflect.Slice {
obj.Set(reflect.MakeSlice(obj.Type(), v.Len(), v.Cap()))
for i := 0; i < v.Len(); i++ {
unmarshalWithPrefix(src, obj.Index(i), fmt.Sprintf("%s.%d", prefix, i))
}
}
}
case reflect.String:
if val, found := src[prefix]; found {
v := reflect.ValueOf(val)
if v.Kind() == reflect.String {
obj.SetString(v.String())
}
}
case reflect.Float64:
if val, found := src[prefix]; found {
v := reflect.ValueOf(val)
if v.Kind() == reflect.Float64 {
obj.SetFloat(v.Float())
}
}
case reflect.Int:
if val, found := src[prefix]; found {
v := reflect.ValueOf(val)
if v.Kind() == reflect.Int {
obj.SetInt(v.Int())
}
}
case reflect.Bool:
if val, found := src[prefix]; found {
v := reflect.ValueOf(val)
if v.Kind() == reflect.Bool {
obj.SetBool(v.Bool())
}
}
}
}
// FlattenInterface takes an interface and flatten it
// into a map with key being a string representation of
// field path. e.g. ".param.nested" -> "value"
func FlattenInterface(in interface{}) map[string]interface{} {
obj := reflect.ValueOf(&in)
return flattenWithPrefix(obj, "")
}
func flattenWithPrefix(obj reflect.Value, prefix string) map[string]interface{} {
o := make(map[string]interface{})
switch obj.Kind() {
case reflect.Ptr:
res := flattenWithPrefix(obj.Elem(), "")
o = mergeMaps(o, res)
case reflect.Interface:
res := flattenWithPrefix(obj.Elem(), prefix)
o = mergeMaps(o, res)
case reflect.Map:
for _, key := range obj.MapKeys() {
res := flattenWithPrefix(obj.MapIndex(key), fmt.Sprintf("%s.%s", prefix, key.Elem().String()))
o = mergeMaps(o, res)
}
case reflect.Slice:
o[prefix] = obj.Interface()
for i := 0; i < obj.Len(); i++ {
res := flattenWithPrefix(obj.Index(i), fmt.Sprintf("%s.%d", prefix, i))
o = mergeMaps(o, res)
}
case reflect.String:
o[prefix] = obj.Interface()
case reflect.Float64:
o[prefix] = obj.Interface()
case reflect.Int:
o[prefix] = obj.Interface()
case reflect.Bool:
o[prefix] = obj.Interface()
}
return o
}
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
for k, v := range b {
a[k] = v
}
return a
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment