github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/config/configstruct/configstruct.go (about)

     1  // Package configstruct parses unstructured maps into structures
     2  package configstruct
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"reflect"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/rclone/rclone/fs/config/configmap"
    12  )
    13  
    14  var matchUpper = regexp.MustCompile("([A-Z]+)")
    15  
    16  // camelToSnake converts CamelCase to snake_case
    17  func camelToSnake(in string) string {
    18  	out := matchUpper.ReplaceAllString(in, "_$1")
    19  	out = strings.ToLower(out)
    20  	out = strings.Trim(out, "_")
    21  	return out
    22  }
    23  
    24  // StringToInterface turns in into an interface{} the same type as def
    25  func StringToInterface(def interface{}, in string) (newValue interface{}, err error) {
    26  	typ := reflect.TypeOf(def)
    27  	if typ.Kind() == reflect.String && typ.Name() == "string" {
    28  		// Pass strings unmodified
    29  		return in, nil
    30  	}
    31  	// Otherwise parse with Sscanln
    32  	//
    33  	// This means any types we use here must implement fmt.Scanner
    34  	o := reflect.New(typ)
    35  	n, err := fmt.Sscanln(in, o.Interface())
    36  	if err != nil {
    37  		return newValue, fmt.Errorf("parsing %q as %T failed: %w", in, def, err)
    38  	}
    39  	if n != 1 {
    40  		return newValue, errors.New("no items parsed")
    41  	}
    42  	return o.Elem().Interface(), nil
    43  }
    44  
    45  // Item describes a single entry in the options structure
    46  type Item struct {
    47  	Name  string // snake_case
    48  	Field string // CamelCase
    49  	Num   int    // number of the field in the struct
    50  	Value interface{}
    51  }
    52  
    53  // Items parses the opt struct and returns a slice of Item objects.
    54  //
    55  // opt must be a pointer to a struct.  The struct should have entirely
    56  // public fields.
    57  //
    58  // The config_name is looked up in a struct tag called "config" or if
    59  // not found is the field name converted from CamelCase to snake_case.
    60  func Items(opt interface{}) (items []Item, err error) {
    61  	def := reflect.ValueOf(opt)
    62  	if def.Kind() != reflect.Ptr {
    63  		return nil, errors.New("argument must be a pointer")
    64  	}
    65  	def = def.Elem() // indirect the pointer
    66  	if def.Kind() != reflect.Struct {
    67  		return nil, errors.New("argument must be a pointer to a struct")
    68  	}
    69  	defType := def.Type()
    70  	for i := 0; i < def.NumField(); i++ {
    71  		field := defType.Field(i)
    72  		fieldName := field.Name
    73  		configName, ok := field.Tag.Lookup("config")
    74  		if !ok {
    75  			configName = camelToSnake(fieldName)
    76  		}
    77  		defaultItem := Item{
    78  			Name:  configName,
    79  			Field: fieldName,
    80  			Num:   i,
    81  			Value: def.Field(i).Interface(),
    82  		}
    83  		items = append(items, defaultItem)
    84  	}
    85  	return items, nil
    86  }
    87  
    88  // Set interprets the field names in defaults and looks up config
    89  // values in the config passed in.  Any values found in config will be
    90  // set in the opt structure.
    91  //
    92  // opt must be a pointer to a struct.  The struct should have entirely
    93  // public fields.  The field names are converted from CamelCase to
    94  // snake_case and looked up in the config supplied or a
    95  // `config:"field_name"` is looked up.
    96  //
    97  // If items are found then they are converted from string to native
    98  // types and set in opt.
    99  //
   100  // All the field types in the struct must implement fmt.Scanner.
   101  func Set(config configmap.Getter, opt interface{}) (err error) {
   102  	defaultItems, err := Items(opt)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	defStruct := reflect.ValueOf(opt).Elem()
   107  	for _, defaultItem := range defaultItems {
   108  		newValue := defaultItem.Value
   109  		if configValue, ok := config.Get(defaultItem.Name); ok {
   110  			var newNewValue interface{}
   111  			newNewValue, err = StringToInterface(newValue, configValue)
   112  			if err != nil {
   113  				// Mask errors if setting an empty string as
   114  				// it isn't valid for all types.  This makes
   115  				// empty string be the equivalent of unset.
   116  				if configValue != "" {
   117  					return fmt.Errorf("couldn't parse config item %q = %q as %T: %w", defaultItem.Name, configValue, defaultItem.Value, err)
   118  				}
   119  			} else {
   120  				newValue = newNewValue
   121  			}
   122  		}
   123  		defStruct.Field(defaultItem.Num).Set(reflect.ValueOf(newValue))
   124  	}
   125  	return nil
   126  }