rclone/fs/config/configstruct/configstruct.go
Nick Craig-Wood e43b5ce5e5 Remove github.com/pkg/errors and replace with std library version
This is possible now that we no longer support go1.12 and brings
rclone into line with standard practices in the Go world.

This also removes errors.New and errors.Errorf from lib/errors and
prefers the stdlib errors package over lib/errors.
2021-11-07 11:53:30 +00:00

128 lines
3.7 KiB
Go

// Package configstruct parses unstructured maps into structures
package configstruct
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/rclone/rclone/fs/config/configmap"
)
var matchUpper = regexp.MustCompile("([A-Z]+)")
// camelToSnake converts CamelCase to snake_case
func camelToSnake(in string) string {
out := matchUpper.ReplaceAllString(in, "_$1")
out = strings.ToLower(out)
out = strings.Trim(out, "_")
return out
}
// StringToInterface turns in into an interface{} the same type as def
func StringToInterface(def interface{}, in string) (newValue interface{}, err error) {
typ := reflect.TypeOf(def)
switch typ.Kind() {
case reflect.String:
// Pass strings unmodified
return in, nil
}
// Otherwise parse with Sscanln
//
// This means any types we use here must implement fmt.Scanner
o := reflect.New(typ)
n, err := fmt.Sscanln(in, o.Interface())
if err != nil {
return newValue, fmt.Errorf("parsing %q as %T failed: %w", in, def, err)
}
if n != 1 {
return newValue, errors.New("no items parsed")
}
return o.Elem().Interface(), nil
}
// Item describes a single entry in the options structure
type Item struct {
Name string // snake_case
Field string // CamelCase
Num int // number of the field in the struct
Value interface{}
}
// Items parses the opt struct and returns a slice of Item objects.
//
// opt must be a pointer to a struct. The struct should have entirely
// public fields.
//
// The config_name is looked up in a struct tag called "config" or if
// not found is the field name converted from CamelCase to snake_case.
func Items(opt interface{}) (items []Item, err error) {
def := reflect.ValueOf(opt)
if def.Kind() != reflect.Ptr {
return nil, errors.New("argument must be a pointer")
}
def = def.Elem() // indirect the pointer
if def.Kind() != reflect.Struct {
return nil, errors.New("argument must be a pointer to a struct")
}
defType := def.Type()
for i := 0; i < def.NumField(); i++ {
field := defType.Field(i)
fieldName := field.Name
configName, ok := field.Tag.Lookup("config")
if !ok {
configName = camelToSnake(fieldName)
}
defaultItem := Item{
Name: configName,
Field: fieldName,
Num: i,
Value: def.Field(i).Interface(),
}
items = append(items, defaultItem)
}
return items, nil
}
// Set interprets the field names in defaults and looks up config
// values in the config passed in. Any values found in config will be
// set in the opt structure.
//
// opt must be a pointer to a struct. The struct should have entirely
// public fields. The field names are converted from CamelCase to
// snake_case and looked up in the config supplied or a
// `config:"field_name"` is looked up.
//
// If items are found then they are converted from string to native
// types and set in opt.
//
// All the field types in the struct must implement fmt.Scanner.
func Set(config configmap.Getter, opt interface{}) (err error) {
defaultItems, err := Items(opt)
if err != nil {
return err
}
defStruct := reflect.ValueOf(opt).Elem()
for _, defaultItem := range defaultItems {
newValue := defaultItem.Value
if configValue, ok := config.Get(defaultItem.Name); ok {
var newNewValue interface{}
newNewValue, err = StringToInterface(newValue, configValue)
if err != nil {
// Mask errors if setting an empty string as
// it isn't valid for all types. This makes
// empty string be the equivalent of unset.
if configValue != "" {
return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, configValue, defaultItem.Value, err)
}
} else {
newValue = newNewValue
}
}
defStruct.Field(defaultItem.Num).Set(reflect.ValueOf(newValue))
}
return nil
}