mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-05 03:17:50 +08:00
Add a new conversion utility vals.ScanMapToGo.
This commit is contained in:
parent
e134ef51e3
commit
de3ac3166d
|
@ -1,12 +1,10 @@
|
||||||
package eval
|
package eval
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"src.elv.sh/pkg/eval/vals"
|
"src.elv.sh/pkg/eval/vals"
|
||||||
"src.elv.sh/pkg/parse"
|
"src.elv.sh/pkg/parse"
|
||||||
"src.elv.sh/pkg/strutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnknownOption is thrown by a native function when called with an unknown option.
|
// UnknownOption is thrown by a native function when called with an unknown option.
|
||||||
|
@ -28,31 +26,18 @@ type RawOptions map[string]interface{}
|
||||||
// with options. A field named FieldName corresponds to the option named
|
// with options. A field named FieldName corresponds to the option named
|
||||||
// field-name. Options that don't have corresponding fields in the struct causes
|
// field-name. Options that don't have corresponding fields in the struct causes
|
||||||
// an error.
|
// an error.
|
||||||
|
//
|
||||||
|
// Similar to vals.ScanMapToGo, but requires rawOpts to contain a subset of keys
|
||||||
|
// supported by the struct.
|
||||||
func scanOptions(rawOpts RawOptions, ptr interface{}) error {
|
func scanOptions(rawOpts RawOptions, ptr interface{}) error {
|
||||||
ptrValue := reflect.ValueOf(ptr)
|
_, keyIdx := vals.StructFieldsInfo(reflect.TypeOf(ptr).Elem())
|
||||||
if ptrValue.Kind() != reflect.Ptr || ptrValue.Elem().Kind() != reflect.Struct {
|
structValue := reflect.ValueOf(ptr).Elem()
|
||||||
return fmt.Errorf(
|
|
||||||
"internal bug: need struct ptr to scan options, got %T", ptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fieldIdxForOpt maps option name to the index of field in `struc`.
|
|
||||||
fieldIdxForOpt := make(map[string]int)
|
|
||||||
struc := ptrValue.Elem()
|
|
||||||
for i := 0; i < struc.Type().NumField(); i++ {
|
|
||||||
if !struc.Field(i).CanSet() {
|
|
||||||
continue // ignore unexported fields
|
|
||||||
}
|
|
||||||
f := struc.Type().Field(i)
|
|
||||||
optName := strutil.CamelToDashed(f.Name)
|
|
||||||
fieldIdxForOpt[optName] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range rawOpts {
|
for k, v := range rawOpts {
|
||||||
fieldIdx, ok := fieldIdxForOpt[k]
|
fieldIdx, ok := keyIdx[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnknownOption{k}
|
return UnknownOption{k}
|
||||||
}
|
}
|
||||||
err := vals.ScanToGo(v, struc.Field(fieldIdx).Addr().Interface())
|
err := vals.ScanToGo(v, structValue.Field(fieldIdx).Addr().Interface())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,30 @@
|
||||||
package eval
|
package eval
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
. "src.elv.sh/pkg/tt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type opts struct {
|
type opts struct {
|
||||||
FooBar string
|
Foo string
|
||||||
Min int
|
bar int
|
||||||
ignore bool // this should be ignored since it isn't exported
|
|
||||||
}
|
|
||||||
|
|
||||||
var scanOptionsTests = []struct {
|
|
||||||
rawOpts RawOptions
|
|
||||||
preScan opts
|
|
||||||
postScan opts
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{RawOptions{"foo-bar": "lorem ipsum"},
|
|
||||||
opts{}, opts{FooBar: "lorem ipsum"}, nil},
|
|
||||||
// Since "ignore" is not exported it will result in an error when used.
|
|
||||||
{RawOptions{"ignore": true},
|
|
||||||
opts{}, opts{ignore: false}, UnknownOption{"ignore"}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScanOptions(t *testing.T) {
|
func TestScanOptions(t *testing.T) {
|
||||||
// scanOptions requires a pointer to struct.
|
// A wrapper of ScanOptions, to make it easier to test
|
||||||
err := scanOptions(RawOptions{}, opts{})
|
wrapper := func(src RawOptions, dstInit interface{}) (interface{}, error) {
|
||||||
if err == nil {
|
ptr := reflect.New(reflect.TypeOf(dstInit))
|
||||||
t.Errorf("Scan should have reported invalid options arg error")
|
ptr.Elem().Set(reflect.ValueOf(dstInit))
|
||||||
|
err := scanOptions(src, ptr.Interface())
|
||||||
|
return ptr.Elem().Interface(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range scanOptionsTests {
|
Test(t, Fn("scanOptions", wrapper), Table{
|
||||||
opts := test.preScan
|
Args(RawOptions{"foo": "lorem ipsum"}, opts{}).
|
||||||
err := scanOptions(test.rawOpts, &opts)
|
Rets(opts{Foo: "lorem ipsum"}, nil),
|
||||||
|
Args(RawOptions{"bar": 20}, opts{bar: 10}).
|
||||||
if ((err == nil) != (test.err == nil)) ||
|
Rets(opts{bar: 10}, UnknownOption{"bar"}),
|
||||||
(err != nil && test.err != nil && err.Error() != test.err.Error()) {
|
})
|
||||||
t.Errorf("Scan error mismatch %v: want %q, got %q", test.rawOpts, test.err, err)
|
|
||||||
}
|
|
||||||
if opts != test.postScan {
|
|
||||||
t.Errorf("Scan %v => %v, want %v", test.rawOpts, opts, test.postScan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,11 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"src.elv.sh/pkg/eval/errs"
|
"src.elv.sh/pkg/eval/errs"
|
||||||
|
"src.elv.sh/pkg/strutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conversion between "Go values" (those expected by native Go functions) and
|
// Conversion between "Go values" (those expected by native Go functions) and
|
||||||
|
@ -215,6 +217,68 @@ func ScanListElementsToGo(src List, ptrs ...interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScanMapToGo scans map elements into ptr, which must be a pointer to a struct.
|
||||||
|
// Struct field names are converted to map keys with CamelToDashed.
|
||||||
|
//
|
||||||
|
// The map may contains keys that don't correspond to struct fields, and it
|
||||||
|
// doesn't have to contain all keys that correspond to struct fields.
|
||||||
|
func ScanMapToGo(src Map, ptr interface{}) error {
|
||||||
|
// Iterate over the struct keys instead of the map: since extra keys are
|
||||||
|
// allowed, the map may be very big, while the size of the struct is bound.
|
||||||
|
keys, _ := StructFieldsInfo(reflect.TypeOf(ptr).Elem())
|
||||||
|
structValue := reflect.ValueOf(ptr).Elem()
|
||||||
|
for i, key := range keys {
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val, ok := src.Index(key)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := ScanToGo(val, structValue.Field(i).Addr().Interface())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructFieldsInfo takes a type for a struct, and returns a slice for each
|
||||||
|
// field name, converted with CamelToDashed, and a reverse index. Unexported
|
||||||
|
// fields result in an empty string in the slice, and is omitted from the
|
||||||
|
// reverse index.
|
||||||
|
func StructFieldsInfo(t reflect.Type) ([]string, map[string]int) {
|
||||||
|
if info, ok := structFieldsInfoCache.Load(t); ok {
|
||||||
|
info := info.(structFieldsInfo)
|
||||||
|
return info.keys, info.keyIdx
|
||||||
|
}
|
||||||
|
info := makeStructFieldsInfo(t)
|
||||||
|
structFieldsInfoCache.Store(t, info)
|
||||||
|
return info.keys, info.keyIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
var structFieldsInfoCache sync.Map
|
||||||
|
|
||||||
|
type structFieldsInfo struct {
|
||||||
|
keys []string
|
||||||
|
keyIdx map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeStructFieldsInfo(t reflect.Type) structFieldsInfo {
|
||||||
|
keys := make([]string, t.NumField())
|
||||||
|
keyIdx := make(map[string]int)
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if field.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strutil.CamelToDashed(field.Name)
|
||||||
|
keyIdx[key] = i
|
||||||
|
keys[i] = key
|
||||||
|
}
|
||||||
|
return structFieldsInfo{keys, keyIdx}
|
||||||
|
}
|
||||||
|
|
||||||
// FromGo converts a Go value to an Elvish value.
|
// FromGo converts a Go value to an Elvish value.
|
||||||
//
|
//
|
||||||
// Exact numbers are normalized to the smallest types that can hold them, and
|
// Exact numbers are normalized to the smallest types that can hold them, and
|
||||||
|
|
|
@ -159,6 +159,35 @@ func TestScanListElementsToGo(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type aStruct struct {
|
||||||
|
Foo int
|
||||||
|
bar interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanMapToGo(t *testing.T) {
|
||||||
|
// A wrapper around ScanMapToGo, to make it easier to test.
|
||||||
|
scanMapToGo := func(src Map, dstInit interface{}) (interface{}, error) {
|
||||||
|
ptr := reflect.New(TypeOf(dstInit))
|
||||||
|
ptr.Elem().Set(reflect.ValueOf(dstInit))
|
||||||
|
err := ScanMapToGo(src, ptr.Interface())
|
||||||
|
return ptr.Elem().Interface(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
Test(t, Fn("ScanListToGo", scanMapToGo), Table{
|
||||||
|
Args(MakeMap("foo", "1"), aStruct{}).Rets(aStruct{Foo: 1}),
|
||||||
|
// More fields is OK
|
||||||
|
Args(MakeMap("foo", "1", "bar", "x"), aStruct{}).Rets(aStruct{Foo: 1}),
|
||||||
|
// Fewer fields is OK
|
||||||
|
Args(MakeMap(), aStruct{}).Rets(aStruct{}),
|
||||||
|
// Unexported fields are ignored
|
||||||
|
Args(MakeMap("bar", 20), aStruct{bar: 10}).Rets(aStruct{bar: 10}),
|
||||||
|
|
||||||
|
// Conversion error
|
||||||
|
Args(MakeMap("foo", "a"), aStruct{}).
|
||||||
|
Rets(aStruct{}, cannotParseAs{"integer", "a"}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestFromGo(t *testing.T) {
|
func TestFromGo(t *testing.T) {
|
||||||
Test(t, Fn("FromGo", FromGo), Table{
|
Test(t, Fn("FromGo", FromGo), Table{
|
||||||
// BigInt -> int, when in range
|
// BigInt -> int, when in range
|
||||||
|
|
Loading…
Reference in New Issue
Block a user