diff --git a/src/restic/options/options.go b/src/restic/options/options.go index 0c620c8a3..49d3fd309 100644 --- a/src/restic/options/options.go +++ b/src/restic/options/options.go @@ -1,8 +1,11 @@ package options import ( + "reflect" "restic/errors" + "strconv" "strings" + "time" ) // Options holds options in the form key=value. @@ -64,3 +67,59 @@ func (o Options) Extract(ns string) Options { return opts } + +// Apply sets the options on dst via reflection, using the struct tag `option`. +func (o Options) Apply(dst interface{}) error { + v := reflect.ValueOf(dst).Elem() + + fields := make(map[string]reflect.StructField) + + for i := 0; i < v.NumField(); i++ { + f := v.Type().Field(i) + tag := f.Tag.Get("option") + + if tag == "" { + continue + } + + if _, ok := fields[tag]; ok { + panic("option tag " + tag + " is not unique in " + v.Type().Name()) + } + + fields[tag] = f + } + + for key, value := range o { + field, ok := fields[key] + if !ok { + return errors.Fatalf("option %v is not known", key) + } + + i := field.Index[0] + switch v.Type().Field(i).Type.Name() { + case "string": + v.Field(i).SetString(value) + + case "int": + vi, err := strconv.ParseInt(value, 0, 32) + if err != nil { + return err + } + + v.Field(i).SetInt(vi) + + case "Duration": + d, err := time.ParseDuration(value) + if err != nil { + return err + } + + v.Field(i).SetInt(int64(d)) + + default: + panic("type " + v.Type().Field(i).Type.Name() + " not handled") + } + } + + return nil +} diff --git a/src/restic/options/options_test.go b/src/restic/options/options_test.go index 2717bc9c3..5a255591c 100644 --- a/src/restic/options/options_test.go +++ b/src/restic/options/options_test.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "testing" + "time" ) var optsTests = []struct { @@ -117,3 +118,99 @@ func TestOptionsExtract(t *testing.T) { }) } } + +// Target is used for Apply() tests +type Target struct { + Name string `option:"name"` + ID int `option:"id"` + Timeout time.Duration `option:"timeout"` + Other string +} + +var setTests = []struct { + input Options + output Target +}{ + { + Options{ + "name": "foobar", + }, + Target{ + Name: "foobar", + }, + }, + { + Options{ + "name": "foobar", + "id": "1234", + }, + Target{ + Name: "foobar", + ID: 1234, + }, + }, + { + Options{ + "timeout": "10m3s", + }, + Target{ + Timeout: time.Duration(10*time.Minute + 3*time.Second), + }, + }, +} + +func TestOptionsApply(t *testing.T) { + for i, test := range setTests { + t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { + var dst Target + err := test.input.Apply(&dst) + if err != nil { + t.Fatal(err) + } + + if dst != test.output { + t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, dst) + } + }) + } +} + +var invalidSetTests = []struct { + input Options + err string +}{ + { + Options{ + "first_name": "foobar", + }, + "option first_name is not known", + }, + { + Options{ + "id": "foobar", + }, + `strconv.ParseInt: parsing "foobar": invalid syntax`, + }, + { + Options{ + "timeout": "2134", + }, + `time: missing unit in duration 2134`, + }, +} + +func TestOptionsApplyInvalid(t *testing.T) { + for i, test := range invalidSetTests { + t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { + var dst Target + err := test.input.Apply(&dst) + if err == nil { + t.Fatalf("expected error %v not found", test.err) + } + + if err.Error() != test.err { + t.Fatalf("expected error %q, got %q", test.err, err.Error()) + } + }) + } +}