go-superstruct

[library] uses reflection to merge structs
git clone https://hhvn.uk/go-superstruct
git clone git://hhvn.uk/go-superstruct
Log | Files | Refs

commit ccf1f90ce50f719925365caca31c9932a7869eaf
Author: hhvn <dev@hhvn.uk>
Date:   Sun, 11 Feb 2024 20:41:53 +0000

Init

Diffstat:
Ago.mod | 5+++++
Ago.sum | 2++
Asuperstruct.go | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asuperstruct_example.go | 31+++++++++++++++++++++++++++++++
Asuperstruct_test.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest_output.txt | 11+++++++++++
6 files changed, 254 insertions(+), 0 deletions(-)

diff --git a/go.mod b/go.mod @@ -0,0 +1,5 @@ +module hhvn.uk/go-superstruct + +go 1.21.5 + +require github.com/jinzhu/copier v0.4.0 // indirect diff --git a/go.sum b/go.sum @@ -0,0 +1,2 @@ +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= diff --git a/superstruct.go b/superstruct.go @@ -0,0 +1,152 @@ +// Package superstruct uses reflection to merge multiple structs into one. +// +// The resulting struct's fields cannot be accessed without reflection. +// +// The intended use of this package is to quickly whack together structures for +// json.Marshal. As json.Marshal uses reflection under the hood, a superstruct +// can be passed to it like any other struct. +package superstruct + +import ( + "fmt" + "sort" + "errors" + "strings" + "reflect" +) + +var ( + ErrNotStruct = errors.New("not a structure") +) + +func copyWalk(vdst, src reflect.Value) { + dst := vdst.Elem() + + switch dst.Kind() { + case reflect.Bool: fallthrough + case reflect.Int: fallthrough + case reflect.Int8: fallthrough + case reflect.Int16: fallthrough + case reflect.Int32: fallthrough + case reflect.Int64: fallthrough + case reflect.Uint: fallthrough + case reflect.Uint8: fallthrough + case reflect.Uint16: fallthrough + case reflect.Uint32: fallthrough + case reflect.Uint64: fallthrough + case reflect.Float32: fallthrough + case reflect.Float64: fallthrough + case reflect.Array: fallthrough + case reflect.Slice: + dst.Set(src) + + case reflect.String: + dst.SetString(src.String()) + + case reflect.Map: + keys := src.MapKeys() + dst.Set(reflect.MakeMapWithSize(dst.Type(), len(keys))) + + for _, k := range keys { + dst.SetMapIndex(k, src.MapIndex(k)) + } + + case reflect.Struct: + for _, f := range reflect.VisibleFields(src.Type()) { + switch dst.FieldByName(f.Name).Kind() { + case reflect.Pointer: fallthrough + case reflect.Interface: + d := dst.FieldByName(f.Name) + s := src.FieldByName(f.Name) + d.Set(s) + default: + d := dst.FieldByName(f.Name).Addr() + s := src.FieldByName(f.Name) + copyWalk(d, s) + } + } + } +} + +func do(cp bool, in... interface{}) (interface{}, error) { + v := make([]reflect.Value, len(in)) + for i := range in { + v[i] = reflect.ValueOf(in[i]) + if v[i].Kind() != reflect.Struct { + return nil, ErrNotStruct + } + } + + t := make([]reflect.Type, len(in)) + for i := range in { + t[i] = v[i].Type() + } + + f := make([][]reflect.StructField, len(in)) + for i := range in { + f[i] = reflect.VisibleFields(t[i]) + } + + fm := make(map[string]reflect.StructField) + + for i := range in { + for _, v := range f[i] { + fm[v.Name] = v + } + } + + fs := make([]reflect.StructField, len(fm)) + + i := 0 + for _, v := range fm { + fs[i] = v + i++ + } + + rv := reflect.New(reflect.StructOf(fs)) + + if cp { + for i := range in { + copyWalk(rv, v[i]) + } + } + + return rv.Elem().Interface(), nil +} + +// Create combines multiple structs into a single struct and returns it ready to use. +func Create(i... interface{}) (interface{}, error) { + return do(false, i...) +} + +// CreateAndCopy also combines multiple structs into a single struct, +// but copies the values in for you. The fields from structs passed later +// take precedence over those passed earlier. +func CreateAndCopy(i... interface{}) (interface{}, error) { + return do(true, i...) +} + +// SortedStructString produces a string representation of the fields and values +// of a struct. Each field is printed on its own line. Lines are sorted with +// sort.Strings() +// +// The main use for this function is in creating a deterministic output for the +// test and example included with this package, though it is exported anyway +// for convenience. +func SortedStructString(s interface{}) string { + var lines []string + + vs := reflect.ValueOf(s) + + for _, v := range reflect.VisibleFields(vs.Type()) { + f := vs.FieldByName(v.Name) + if f.Kind() == reflect.Pointer || f.Kind() == reflect.Interface { + f = f.Elem() + } + lines = append(lines, fmt.Sprintf("%v: %+v", v.Name, f.Interface())) + } + + sort.Strings(lines) + + return strings.Join(lines, "\n") +} diff --git a/superstruct_example.go b/superstruct_example.go @@ -0,0 +1,31 @@ +package superstruct + +import ( + "fmt" +) + +type First struct { + A string + C string +} + +type Second struct { + B string + C string +} + + +func Example() { + a := First{"a", "c"} + b := Second{"b", "d"} + c, err := CreateAndCopy(a, b) + if err != nil { + return + } + + fmt.Println(SortedStructString(c)) + // Output: + // A: a + // B: b + // C: d +} diff --git a/superstruct_test.go b/superstruct_test.go @@ -0,0 +1,53 @@ +package superstruct + +import ( + "strings" + "testing" + + _ "embed" +) + +type A struct { + A string + C string +} + +type B struct { + B string + C string + D *string + E interface{} + F int16 + G uint32 + H float64 + I [5]int + J []int + K map[string]int +} + +//go:embed test_output.txt +var expected string + +func Test(t *testing.T) { + m := make(map[string]int) + m["l"] = 99 + m["m"] = 98 + + str := "5" + arr := [5]int{10, 9, 8, 7, 6} + sl := []int{3, 2} + a := A{"1", "2"} + b := B{"3", "4", &str, "hi", 16, 32, 64.0, arr, sl, m} + + c, err := CreateAndCopy(a, b) + if err != nil { + t.Fatalf("%v", err) + } + + expected = strings.TrimSuffix(expected, "\n") + got := SortedStructString(c) + + if expected != got { + t.Fatalf("\nExpected:\n%v\nGot:\n%v", expected, got) + } +} diff --git a/test_output.txt b/test_output.txt @@ -0,0 +1,11 @@ +A: 1 +B: 3 +C: 4 +D: 5 +E: hi +F: 16 +G: 32 +H: 64 +I: [10 9 8 7 6] +J: [3 2] +K: map[l:99 m:98]