Add free-form options parsing

This commit is contained in:
Alexander Neumann 2017-03-25 14:36:39 +01:00
parent 2e0b19f63f
commit d0a5e86da1
2 changed files with 168 additions and 0 deletions

View file

@ -0,0 +1,61 @@
package options
import (
"restic/errors"
"strings"
)
// Options holds options in the form key=value.
type Options map[string]string
// splitKeyValue splits at the first equals (=) sign.
func splitKeyValue(s string) (key string, value string) {
data := strings.SplitN(s, "=", 2)
key = strings.ToLower(strings.TrimSpace(data[0]))
if len(data) == 1 {
// no equals sign is treated as the empty value
return key, ""
}
return key, strings.TrimSpace(data[1])
}
// Parse takes a slice of key=value pairs and returns an Options type.
// The key may include namespaces, separated by dots. Example: "foo.bar=value".
// Keys are converted to lower-case.
func Parse(in []string) (Options, error) {
opts := make(Options, len(in))
for _, opt := range in {
key, value := splitKeyValue(opt)
if key == "" {
return Options{}, errors.Fatalf("empty key is not a valid option")
}
opts[key] = value
}
return opts, nil
}
// Extract returns an Options type with all keys in namespace ns, which is
// also stripped from the keys. ns must end with a dot.
func (o Options) Extract(ns string) Options {
l := len(ns)
if ns[l-1] != '.' {
ns += "."
l++
}
opts := make(Options)
for k, v := range o {
if !strings.HasPrefix(k, ns) {
continue
}
opts[k[l:]] = v
}
return opts
}

View file

@ -0,0 +1,107 @@
package options
import (
"fmt"
"reflect"
"testing"
)
var optsTests = []struct {
input []string
output Options
}{
{
[]string{"foo=bar", "bar=baz ", "k="},
Options{
"foo": "bar",
"bar": "baz",
"k": "",
},
},
{
[]string{"Foo=23", "baR", "k=thing with spaces"},
Options{
"foo": "23",
"bar": "",
"k": "thing with spaces",
},
},
{
[]string{"k=thing with spaces", "k2=more spaces = not evil"},
Options{
"k": "thing with spaces",
"k2": "more spaces = not evil",
},
},
}
func TestParseOptions(t *testing.T) {
for i, test := range optsTests {
t.Run(fmt.Sprintf("test-%v", i), func(t *testing.T) {
opts, err := Parse(test.input)
if err != nil {
t.Fatalf("unable to parse options: %v", err)
}
if !reflect.DeepEqual(opts, test.output) {
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts)
}
})
}
}
var invalidOptsTests = []struct {
input []string
err string
}{
{
[]string{"=bar", "bar=baz", "k="},
"empty key is not a valid option",
},
}
func TestParseInvalidOptions(t *testing.T) {
for _, test := range invalidOptsTests {
t.Run(test.err, func(t *testing.T) {
_, err := Parse(test.input)
if err == nil {
t.Fatalf("expected error (%v) not found, err is nil", test.err)
}
if err.Error() != test.err {
t.Fatalf("expected error %q, got %q", test.err, err.Error())
}
})
}
}
var extractTests = []struct {
input Options
ns string
output Options
}{
{
input: Options{
"foo.bar:": "baz",
"s3.timeout": "10s",
"sftp.timeout": "5s",
"global": "foobar",
},
ns: "s3",
output: Options{
"timeout": "10s",
},
},
}
func TestOptionsExtract(t *testing.T) {
for _, test := range extractTests {
t.Run(test.ns, func(t *testing.T) {
opts := test.input.Extract(test.ns)
if !reflect.DeepEqual(opts, test.output) {
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts)
}
})
}
}