Skip to content

Commit

Permalink
Introduce fluent.Reflect convenience method.
Browse files Browse the repository at this point in the history
  • Loading branch information
warpfork committed Sep 18, 2020
1 parent 3500324 commit 9dbb1ed
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 0 deletions.
207 changes: 207 additions & 0 deletions fluent/reflect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package fluent

import (
"fmt"
"reflect"
"sort"

"github.com/ipld/go-ipld-prime"
)

func Reflect(np ipld.NodePrototype, i interface{}) (ipld.Node, error) {
return defaultReflector.Reflect(np, i)
}

func ReflectIntoAssembler(na ipld.NodeAssembler, i interface{}) error {
return defaultReflector.ReflectIntoAssembler(na, i)
}

var defaultReflector = Reflector{
MapOrder: func(x, y string) bool {
return x < y
},
}

type Reflector struct {
// MapOrder is used to decide a deterministic order for inserting entries to maps.
// (This is used when converting golang maps, since their iteration order is randomized;
// it is not used when converting other types such as structs, since those have a stable order.)
// MapOrder should return x < y in the same way as sort.Interface.Less.
MapOrder func(x, y string) bool
}

func (rcfg Reflector) Reflect(np ipld.NodePrototype, i interface{}) (ipld.Node, error) {
nb := np.NewBuilder()
if err := rcfg.ReflectIntoAssembler(nb, i); err != nil {
return nil, err
}
return nb.Build(), nil
}

// ReflectIntoAssembler is a handy method for converting some basic golang types into Nodes.
//
// This plays fast and loose in general -- it's meant for demos and simple hacking, not for serious use.
// For example, in reflecting on structs, Reflect assumes no anonymous fields or other complications.
// There is no support for configuring converting struct fields with different names or other transformations.
// And so forth.
// If you need more control: this function is not what you should be using.
func (rcfg Reflector) ReflectIntoAssembler(na ipld.NodeAssembler, i interface{}) error {
// Cover the most common values with a type-switch, as it's faster than reflection.
switch x := i.(type) {
case map[string]string:
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Sort(sortableStrings{keys, rcfg.MapOrder})
ma, err := na.BeginMap(len(x))
if err != nil {
return err
}
for _, k := range keys {
va, err := ma.AssembleEntry(k)
if err != nil {
return err
}
if err := va.AssignString(x[k]); err != nil {
return err
}
}
return ma.Finish()
case map[string]interface{}:
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Sort(sortableStrings{keys, rcfg.MapOrder})
ma, err := na.BeginMap(len(x))
if err != nil {
return err
}
for _, k := range keys {
va, err := ma.AssembleEntry(k)
if err != nil {
return err
}
if err := rcfg.ReflectIntoAssembler(va, x[k]); err != nil {
return err
}
}
return ma.Finish()
case []string:
la, err := na.BeginList(len(x))
if err != nil {
return err
}
for _, v := range x {
if err := la.AssembleValue().AssignString(v); err != nil {
return err
}
}
return la.Finish()
case []interface{}:
la, err := na.BeginList(len(x))
if err != nil {
return err
}
for _, v := range x {
if err := rcfg.ReflectIntoAssembler(la.AssembleValue(), v); err != nil {
return err
}
}
return la.Finish()
case string:
return na.AssignString(x)
case int:
return na.AssignInt(x)
case nil:
return na.AssignNull()
}
// That didn't fly? Reflection time.
rv := reflect.ValueOf(i)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return na.AssignInt(int(rv.Int()))
case reflect.Slice, reflect.Array:
l := rv.Len()
la, err := na.BeginList(l)
if err != nil {
return err
}
for i := 0; i < l; i++ {
if err := rcfg.ReflectIntoAssembler(la.AssembleValue(), rv.Index(i).Interface()); err != nil {
return err
}
}
return la.Finish()
case reflect.Map:
// the keys slice for sorting keeps things in reflect.Value form, because unboxing is cheap,
// but re-boxing is not cheap, and the MapIndex method requires reflect.Value again later.
keys := make([]reflect.Value, 0, rv.Len())
itr := rv.MapRange()
for itr.Next() {
k := itr.Key()
if k.Kind() != reflect.String {
return fmt.Errorf("cannot convert a map with non-string keys (%T)", i)
}
keys = append(keys, k)
}
sort.Sort(sortableReflectStrings{keys, rcfg.MapOrder})
ma, err := na.BeginMap(rv.Len())
if err != nil {
return err
}
for _, k := range keys {
va, err := ma.AssembleEntry(k.String())
if err != nil {
return err
}
if err := rcfg.ReflectIntoAssembler(va, rv.MapIndex(k).Interface()); err != nil {
return err
}
}
return ma.Finish()
case reflect.Struct:
l := rv.NumField()
ma, err := na.BeginMap(l)
if err != nil {
return err
}
for i := 0; i < l; i++ {
fn := rv.Type().Field(i).Name
fv := rv.Field(i)
va, err := ma.AssembleEntry(fn)
if err != nil {
return err
}
if err := rcfg.ReflectIntoAssembler(va, fv.Interface()); err != nil {
return err
}
}
return ma.Finish()
case reflect.Ptr:
if rv.IsNil() {
return na.AssignNull()
}
return rcfg.ReflectIntoAssembler(na, rv.Elem())
}
return fmt.Errorf("fluent.Reflect: unsure how to handle type %T (kind: %v)", i, rv.Kind())
}

type sortableStrings struct {
a []string
less func(x, y string) bool
}

func (a sortableStrings) Len() int { return len(a.a) }
func (a sortableStrings) Swap(i, j int) { a.a[i], a.a[j] = a.a[j], a.a[i] }
func (a sortableStrings) Less(i, j int) bool { return a.less(a.a[i], a.a[j]) }

type sortableReflectStrings struct {
a []reflect.Value
less func(x, y string) bool
}

func (a sortableReflectStrings) Len() int { return len(a.a) }
func (a sortableReflectStrings) Swap(i, j int) { a.a[i], a.a[j] = a.a[j], a.a[i] }
func (a sortableReflectStrings) Less(i, j int) bool { return a.less(a.a[i].String(), a.a[j].String()) }
95 changes: 95 additions & 0 deletions fluent/reflect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package fluent_test

import (
"testing"

. "github.com/warpfork/go-wish"

ipld "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/fluent"
"github.com/ipld/go-ipld-prime/must"
basicnode "github.com/ipld/go-ipld-prime/node/basic"
)

func TestReflect(t *testing.T) {
t.Run("Map", func(t *testing.T) {
n, err := fluent.Reflect(basicnode.Prototype.Any, map[string]interface{}{
"k1": "fine",
"k2": "super",
"k3": map[string]string{
"k31": "thanks",
"k32": "for",
"k33": "asking",
},
})
Wish(t, err, ShouldEqual, nil)
Wish(t, n.ReprKind(), ShouldEqual, ipld.ReprKind_Map)
t.Run("CorrectContents", func(t *testing.T) {
Wish(t, n.Length(), ShouldEqual, 3)
Wish(t, must.String(must.Node(n.LookupByString("k1"))), ShouldEqual, "fine")
Wish(t, must.String(must.Node(n.LookupByString("k2"))), ShouldEqual, "super")
n := must.Node(n.LookupByString("k3"))
Wish(t, n.Length(), ShouldEqual, 3)
Wish(t, must.String(must.Node(n.LookupByString("k31"))), ShouldEqual, "thanks")
Wish(t, must.String(must.Node(n.LookupByString("k32"))), ShouldEqual, "for")
Wish(t, must.String(must.Node(n.LookupByString("k33"))), ShouldEqual, "asking")
})
t.Run("CorrectOrder", func(t *testing.T) {
itr := n.MapIterator()
k, _, _ := itr.Next()
Wish(t, must.String(k), ShouldEqual, "k1")
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "k2")
k, v, _ := itr.Next()
Wish(t, must.String(k), ShouldEqual, "k3")
itr = v.MapIterator()
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "k31")
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "k32")
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "k33")
})
})
t.Run("Struct", func(t *testing.T) {
type Woo struct {
A string
B string
}
type Whee struct {
X string
Z string
M Woo
}
n, err := fluent.Reflect(basicnode.Prototype.Any, Whee{
X: "fine",
Z: "super",
M: Woo{"thanks", "really"},
})
Wish(t, err, ShouldEqual, nil)
Wish(t, n.ReprKind(), ShouldEqual, ipld.ReprKind_Map)
t.Run("CorrectContents", func(t *testing.T) {
Wish(t, n.Length(), ShouldEqual, 3)
Wish(t, must.String(must.Node(n.LookupByString("X"))), ShouldEqual, "fine")
Wish(t, must.String(must.Node(n.LookupByString("Z"))), ShouldEqual, "super")
n := must.Node(n.LookupByString("M"))
Wish(t, n.Length(), ShouldEqual, 2)
Wish(t, must.String(must.Node(n.LookupByString("A"))), ShouldEqual, "thanks")
Wish(t, must.String(must.Node(n.LookupByString("B"))), ShouldEqual, "really")
})
t.Run("CorrectOrder", func(t *testing.T) {
itr := n.MapIterator()
k, _, _ := itr.Next()
Wish(t, must.String(k), ShouldEqual, "X")
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "Z")
k, v, _ := itr.Next()
Wish(t, must.String(k), ShouldEqual, "M")
itr = v.MapIterator()
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "A")
k, _, _ = itr.Next()
Wish(t, must.String(k), ShouldEqual, "B")
})
})
}

0 comments on commit 9dbb1ed

Please sign in to comment.