Skip to content

Commit

Permalink
feat(remotes): base/fill/PathValue used by Config, to enable Remotes
Browse files Browse the repository at this point in the history
Merge pull request #714 from qri-io/path-value-remote
  • Loading branch information
dustmop authored Mar 25, 2019
2 parents b813d05 + 4d45dce commit 7e78857
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 120 deletions.
183 changes: 183 additions & 0 deletions base/fill/path_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package fill

import (
"fmt"
"reflect"
"strconv"
"strings"
)

var (
// ErrNotFound is returned when a field isn't found
ErrNotFound = fmt.Errorf("not found")
)

// CoercionError represents an error when a value cannot be converted from one type to another
type CoercionError struct {
From string
To string
}

// Error displays the text version of the CoercionError
func (c *CoercionError) Error() string {
return fmt.Sprintf("could not coerce from %s to %s", c.From, c.To)
}

// SetPathValue sets a value on a the output struct, accessed using the path of dot-separated fields
func SetPathValue(path string, val interface{}, output interface{}) error {
target := reflect.ValueOf(output)
steps := strings.Split(path, ".")
// Find target place to assign to, field is a non-empty string only if target is a map.
target, field, err := findTargetAtPath(steps, target)
if err != nil {
if err == ErrNotFound {
return fmt.Errorf("path: \"%s\" not found", path)
}
return err
}
if field == "" {
// Convert val into the correct type, parsing ints and bools and so forth.
val, err = coerceToTargetType(val, target)
if err != nil {
if cerr, ok := err.(*CoercionError); ok {
return fmt.Errorf("invalid type for path \"%s\": expected %s, got %s",
path, cerr.To, cerr.From)
}
return err
}
return putValueToPlace(val, target)
}
// A map with a field name to assign to.
// TODO: Only works for map[string]string, not map's with a struct for a value.
target.SetMapIndex(reflect.ValueOf(field), reflect.ValueOf(val))
return nil
}

// GetPathValue gets a value from the input struct, accessed using the path of dot-separated fields
func GetPathValue(path string, input interface{}) (interface{}, error) {
target := reflect.ValueOf(input)
steps := strings.Split(path, ".")
// Find target place to assign to, field is a non-empty string only if target is a map.
target, field, err := findTargetAtPath(steps, target)
if err != nil {
if err == ErrNotFound {
return nil, fmt.Errorf("path: \"%s\" not found", path)
}
return nil, err
}
if field == "" {
return target.Interface(), nil
}
lookup := target.MapIndex(reflect.ValueOf(field))
if lookup.IsValid() {
return lookup.Interface(), nil
}
return nil, fmt.Errorf("invalid path: \"%s\"", path)
}

func findTargetAtPath(steps []string, place reflect.Value) (reflect.Value, string, error) {
if len(steps) == 0 {
return place, "", nil
}
// Get current step of the path, dispatch on its type.
s := steps[0]
rest := steps[1:]
if place.Kind() == reflect.Struct {
field := getFieldCaseInsensitive(place, s)
if !field.IsValid() {
return place, "", ErrNotFound
}
return findTargetAtPath(rest, field)
} else if place.Kind() == reflect.Ptr {
var inner reflect.Value
if place.IsNil() {
alloc := reflect.New(place.Type().Elem())
place.Set(alloc)
inner = alloc.Elem()
} else {
inner = place.Elem()
}
return findTargetAtPath(steps, inner)
} else if place.Kind() == reflect.Map {
if place.IsNil() {
place.Set(reflect.MakeMap(place.Type()))
}
// TODO: Handle case where `rest` has more steps and `val` is a struct: more
// recursive is needed.
return place, s, nil
} else if place.Kind() == reflect.Slice {
num, err := coerceToInt(s)
if err != nil {
return place, "", err
}
if num >= place.Len() {
return place, "", fmt.Errorf("index outside of range: %d, len is %d", num, place.Len())
}
elem := place.Index(num)
return findTargetAtPath(rest, elem)
} else {
return place, "", fmt.Errorf("cannot set field of type %s", place.Kind())
}
}

func coerceToTargetType(val interface{}, place reflect.Value) (interface{}, error) {
switch place.Kind() {
case reflect.Bool:
str, ok := val.(string)
if ok {
str = strings.ToLower(str)
if str == "true" {
return true, nil
} else if str == "false" {
return false, nil
} else {
return nil, fmt.Errorf("could not parse value to bool")
}
}
b, ok := val.(bool)
if ok {
return b, nil
}
return nil, &CoercionError{To: "bool", From: reflect.TypeOf(val).Name()}
case reflect.Int:
num, ok := val.(int)
if ok {
return num, nil
}
str, ok := val.(string)
if ok {
parsed, err := strconv.ParseInt(str, 10, 32)
return int(parsed), err
}
return nil, &CoercionError{To: "int", From: reflect.TypeOf(val).Name()}
case reflect.Ptr:
alloc := reflect.New(place.Type().Elem())
return coerceToTargetType(val, alloc.Elem())
case reflect.String:
str, ok := val.(string)
if ok {
return str, nil
}
return nil, &CoercionError{To: "string", From: reflect.TypeOf(val).Name()}
default:
return nil, fmt.Errorf("unknown kind: %s", place.Kind())
}
}

func coerceToInt(val interface{}) (int, error) {
num, ok := val.(int)
if ok {
return num, nil
}
str, ok := val.(string)
if ok {
parsed, err := strconv.ParseInt(str, 10, 32)
return int(parsed), err
}
return -1, &CoercionError{To: "int", From: reflect.TypeOf(val).Name()}
}

func getFieldCaseInsensitive(place reflect.Value, name string) reflect.Value {
name = strings.ToLower(name)
return place.FieldByNameFunc(func(s string) bool { return strings.ToLower(s) == name })
}
122 changes: 122 additions & 0 deletions base/fill/path_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package fill

import (
"testing"
)

func TestFillPathValue(t *testing.T) {
c := Collection{}
err := SetPathValue("name", "Alice", &c)
if err != nil {
panic(err)
}
if c.Name != "Alice" {
t.Errorf("expected: s.Name should be \"Alice\"")
}

c = Collection{}
err = SetPathValue("age", 42, &c)
if err != nil {
panic(err)
}
if c.Age != 42 {
t.Errorf("expected: s.Age should be 42")
}

c = Collection{}
err = SetPathValue("age", "56", &c)
if err != nil {
panic(err)
}
if c.Age != 56 {
t.Errorf("expected: s.Age should be 56")
}

c = Collection{}
err = SetPathValue("ison", "true", &c)
if err != nil {
panic(err)
}
if !c.IsOn {
t.Errorf("expected: s.IsOn should be true")
}

c = Collection{}
err = SetPathValue("ison", true, &c)
if err != nil {
panic(err)
}
if !c.IsOn {
t.Errorf("expected: s.IsOn should be true")
}

c = Collection{}
err = SetPathValue("ptr", 123, &c)
if err != nil {
panic(err)
}
if *(c.Ptr) != 123 {
t.Errorf("expected: s.Ptr should be 123")
}

c = Collection{}
err = SetPathValue("not_found", "missing", &c)
expect := "path: \"not_found\" not found"
if err == nil {
t.Fatalf("expected: error \"%s\", got no error", expect)
}
if err.Error() != expect {
t.Errorf("expected: error should be \"%s\", got \"%s\"", expect, err.Error())
}

c = Collection{}
err = SetPathValue("sub.num", 7, &c)
if err != nil {
panic(err)
}
if c.Sub.Num != 7 {
t.Errorf("expected: s.Sub.Num should be 7")
}

c = Collection{}
err = SetPathValue("dict.cat", "meow", &c)
if err != nil {
panic(err)
}
if c.Dict["cat"] != "meow" {
t.Errorf("expected: s.Dict[\"cat\"] should be \"meow\"")
}

// Don't allocate a new map.
err = SetPathValue("dict.dog", "bark", &c)
if err != nil {
panic(err)
}
if c.Dict["cat"] != "meow" {
t.Errorf("expected: s.Dict[\"cat\"] should be \"meow\"")
}
if c.Dict["dog"] != "bark" {
t.Errorf("expected: s.Dict[\"dog\"] should be \"bark\"")
}

s := &SubElement{}
err = SetPathValue("things.eel", "zap", &s)
if err != nil {
panic(err)
}
if (*s.Things)["eel"] != "zap" {
t.Errorf("expected: c.Things[\"eel\"] should be \"zap\"")
}

// Don't allocate a new map.
err = SetPathValue("things.frog", "ribbit", &s)
if err != nil {
panic(err)
}
if (*s.Things)["eel"] != "zap" {
t.Errorf("expected: c.Things[\"eel\"] should be \"zap\"")
}
if (*s.Things)["frog"] != "ribbit" {
t.Errorf("expected: c.Things[\"frog\"] should be \"ribbit\"")
}
}
8 changes: 2 additions & 6 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"strings"

"github.com/ghodss/yaml"
"github.com/qri-io/ioes"
"github.com/qri-io/qri/config"
"github.com/qri-io/qri/lib"
Expand Down Expand Up @@ -179,8 +178,9 @@ func (o *ConfigOptions) Set(args []string) (err error) {
profileChanged := false

for i := 0; i < len(args)-1; i = i + 2 {
var value interface{}
path := strings.ToLower(args[i])
value := args[i+1]

if ip[path] {
ErrExit(o.ErrOut, fmt.Errorf("cannot set path %s", path))
}
Expand All @@ -196,10 +196,6 @@ func (o *ConfigOptions) Set(args []string) (err error) {
}
profileChanged = true
} else {
if err = yaml.Unmarshal([]byte(args[i+1]), &value); err != nil {
return err
}

if err = lib.Config.Set(path, value); err != nil {
return err
}
Expand Down
Loading

0 comments on commit 7e78857

Please sign in to comment.