Configure structs via tags
diff --git a/.travis.yml b/.travis.yml index bcb8521..ae2f187 100644 --- a/.travis.yml +++ b/.travis.yml
@@ -1,5 +1,10 @@ -sudo: false language: go go: - - auto - + - 1.4 + - 1.4.1 + - 1.4.2 + - 1.4.3 + - 1.5 + - 1.5.1 + - 1.5.2 + - tip
diff --git a/decode.go b/decode.go new file mode 100644 index 0000000..e13b268 --- /dev/null +++ b/decode.go
@@ -0,0 +1,289 @@ +package properties + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +// Decode assigns property values to exported fields of a struct. +// +// Decode traverses v recursively and returns an error if a value cannot be +// converted to the field type or a required value is missing for a field. +// +// The following type dependent decodings are used: +// +// String, boolean, numeric fields have the value of the property key assigned. +// The property key name is the name of the field. A different key and a default +// value can be set in the field's tag. Fields without default value are +// required. If the value cannot be converted to the field type an error is +// returned. +// +// time.Duration fields have the result of time.ParseDuration() assigned. +// +// time.Time fields have the vaule of time.Parse() assigned. The default layout +// is time.RFC3339 but can be set in the field's tag. +// +// Arrays and slices of string, boolean, numeric, time.Duration and time.Time +// fields have the value interpreted as a comma separated list of values. The +// individual values are trimmed of whitespace and empty values are ignored. A +// default value can be provided as a semicolon separated list in the field's +// tag. +// +// Struct fields are decoded recursively using the field name plus "." as +// prefix. The prefix (without dot) can be overridden in the field's tag. +// Default values are not supported in the field's tag. Specify them on the +// fields of the inner struct instead. +// +// Map fields must have a key of type string and are decoded recursively by +// using the field's name plus ".' as prefix and the next element of the key +// name as map key. The prefix (without dot) can be overridden in the field's +// tag. Default values are not supported. + +// Examples: +// +// // Field is ignored. +// Field int `properties:"-"` +// +// // Field is assigned value of 'Field'. +// Field int +// +// // Field is assigned value of 'myName'. +// Field int `properties:"myName"` +// +// // Field is assigned value of key 'myName' and has a default +// // value 15 if the key does not exist. +// Field int `properties:"myName,default=15"` +// +// // Field is assigned value of key 'Field' and has a default +// // value 15 if the key does not exist. +// Field int `properties:",default=15"` +// +// // Field is assigned value of key 'date' and the date +// // is in format 2006-01-02 +// Field time.Time `properties:"date,layout=2006-01-02"` +// +// // Field is assigned the non-empty and whitespace trimmed +// // values of key 'Field' split by commas. +// Field []string +// +// // Field is assigned the non-empty and whitespace trimmed +// // values of key 'Field' split by commas and has a default +// // value ["a", "b", "c"] if the key does not exist. +// Field []string `properties:",default=a;b;c"` +// +// // Field is decoded recursively with "Field." as key prefix. +// Field SomeStruct +// +// // Field is decoded recursively with "myName." as key prefix. +// Field SomeStruct `properties:"myName"` +// +// // Field is decoded recursively with "Field." as key prefix +// // and the next dotted element of the key as map key. +// Field map[string]string +// +// // Field is decoded recursively with "myName." as key prefix +// // and the next dotted element of the key as map key. +// Field map[string]string `properties:"myName"` +// +// TODO(fs): optional structs and maps +// +func (p *Properties) Decode(x interface{}) error { + t, v := reflect.TypeOf(x), reflect.ValueOf(x) + if t.Kind() != reflect.Ptr || v.Elem().Type().Kind() != reflect.Struct { + return fmt.Errorf("not a pointer to struct: %s", t) + } + if err := dec(p, "", nil, nil, v); err != nil { + return err + } + return nil +} + +func dec(p *Properties, key string, def *string, opts map[string]string, v reflect.Value) error { + t := v.Type() + + // value returns the property value for key or the default if provided. + value := func() (string, error) { + if val, ok := p.Get(key); ok { + return val, nil + } + if def != nil { + return *def, nil + } + return "", fmt.Errorf("missing required key %s", key) + } + + // conv converts a string to a value of the given type. + conv := func(s string, t reflect.Type) (val reflect.Value, err error) { + var v interface{} + + switch { + case isDuration(t): + v, err = time.ParseDuration(s) + + case isTime(t): + layout := opts["layout"] + if layout == "" { + layout = time.RFC3339 + } + v, err = time.Parse(layout, s) + + case isBool(t): + v, err = boolVal(s), nil + + case isString(t): + v, err = s, nil + + case isFloat(t): + v, err = strconv.ParseFloat(s, 64) + + case isInt(t): + v, err = strconv.ParseInt(s, 10, 64) + + case isUint(t): + v, err = strconv.ParseUint(s, 10, 64) + + default: + return reflect.Zero(t), fmt.Errorf("unsupported type %s", t) + } + if err != nil { + return reflect.Zero(t), err + } + return reflect.ValueOf(v).Convert(t), nil + } + + // keydef returns the property key and the default value based on the + // name of the struct field and the options in the tag. + keydef := func(f reflect.StructField) (string, *string, map[string]string) { + key, opts := parseTag(f.Tag.Get("properties")) + + var def *string + if d, ok := opts["default"]; ok { + def = &d + } + if key != "" { + return key, def, opts + } + return f.Name, def, opts + } + + switch { + case isDuration(t) || isTime(t) || isBool(t) || isString(t) || isFloat(t) || isInt(t) || isUint(t): + s, err := value() + if err != nil { + return err + } + val, err := conv(s, t) + if err != nil { + return err + } + v.Set(val) + + case isPtr(t): + return dec(p, key, def, opts, v.Elem()) + + case isStruct(t): + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + fk, def, opts := keydef(t.Field(i)) + if !fv.CanSet() { + return fmt.Errorf("cannot set ", t.Field(i).Name) + } + if fk == "-" { + continue + } + if key != "" { + fk = key + "." + fk + } + if err := dec(p, fk, def, opts, fv); err != nil { + return err + } + } + return nil + + case isArray(t): + val, err := value() + if err != nil { + return err + } + vals := split(val, ";") + a := reflect.MakeSlice(t, 0, len(vals)) + for _, s := range vals { + val, err := conv(s, t.Elem()) + if err != nil { + return err + } + a = reflect.Append(a, val) + } + v.Set(a) + + case isMap(t): + valT := t.Elem() + m := reflect.MakeMap(t) + for postfix, _ := range p.FilterStripPrefix(key + ".").m { + pp := strings.SplitN(postfix, ".", 2) + mk, mv := pp[0], reflect.New(valT) + if err := dec(p, key+"."+mk, nil, nil, mv); err != nil { + return err + } + m.SetMapIndex(reflect.ValueOf(mk), mv.Elem()) + } + v.Set(m) + + default: + return fmt.Errorf("unsupported type %s", t) + } + return nil +} + +// split splits a string on sep, trims whitespace of elements +// and omits empty elements +func split(s string, sep string) []string { + var a []string + for _, v := range strings.Split(s, sep) { + if v = strings.TrimSpace(v); v != "" { + a = append(a, v) + } + } + return a +} + +// parseTag parses a "key,k=v,k=v,..." +func parseTag(tag string) (key string, opts map[string]string) { + opts = map[string]string{} + for i, s := range strings.Split(tag, ",") { + if i == 0 { + key = s + continue + } + + pp := strings.SplitN(s, "=", 2) + if len(pp) == 1 { + opts[pp[0]] = "" + } else { + opts[pp[0]] = pp[1] + } + } + return key, opts +} + +func isArray(t reflect.Type) bool { return t.Kind() == reflect.Array || t.Kind() == reflect.Slice } +func isBool(t reflect.Type) bool { return t.Kind() == reflect.Bool } +func isDuration(t reflect.Type) bool { return t == reflect.TypeOf(time.Second) } +func isMap(t reflect.Type) bool { return t.Kind() == reflect.Map } +func isNumeric(t reflect.Type) bool { return isInt(t) || isUint(t) || isFloat(t) } +func isPtr(t reflect.Type) bool { return t.Kind() == reflect.Ptr } +func isString(t reflect.Type) bool { return t.Kind() == reflect.String } +func isStruct(t reflect.Type) bool { return t.Kind() == reflect.Struct } +func isTime(t reflect.Type) bool { return t == reflect.TypeOf(time.Time{}) } +func isFloat(t reflect.Type) bool { + return t.Kind() == reflect.Float32 || t.Kind() == reflect.Float64 +} +func isInt(t reflect.Type) bool { + return t.Kind() == reflect.Int || t.Kind() == reflect.Int8 || t.Kind() == reflect.Int16 || t.Kind() == reflect.Int32 || t.Kind() == reflect.Int64 +} +func isUint(t reflect.Type) bool { + return t.Kind() == reflect.Uint || t.Kind() == reflect.Uint8 || t.Kind() == reflect.Uint16 || t.Kind() == reflect.Uint32 || t.Kind() == reflect.Uint64 +}
diff --git a/decode_test.go b/decode_test.go new file mode 100644 index 0000000..b5ec138 --- /dev/null +++ b/decode_test.go
@@ -0,0 +1,292 @@ +package properties + +import ( + "reflect" + "testing" + "time" +) + +func TestDecodeValues(t *testing.T) { + type S struct { + S string + BT bool + BF bool + I int + I8 int8 + I16 int16 + I32 int32 + I64 int64 + U uint + U8 uint8 + U16 uint16 + U32 uint32 + U64 uint64 + F32 float32 + F64 float64 + D time.Duration + TM time.Time + } + in := ` + S=abc + BT=true + BF=false + I=-1 + I8=-8 + I16=-16 + I32=-32 + I64=-64 + U=1 + U8=8 + U16=16 + U32=32 + U64=64 + F32=3.2 + F64=6.4 + D=5s + TM=2015-01-02T12:34:56Z + ` + out := &S{ + S: "abc", + BT: true, + BF: false, + I: -1, + I8: -8, + I16: -16, + I32: -32, + I64: -64, + U: 1, + U8: 8, + U16: 16, + U32: 32, + U64: 64, + F32: 3.2, + F64: 6.4, + D: 5 * time.Second, + TM: tm(t, time.RFC3339, "2015-01-02T12:34:56Z"), + } + testDecode(t, in, &S{}, out) +} + +func TestDecodeValueDefaults(t *testing.T) { + type S struct { + S string `properties:",default=abc"` + BT bool `properties:",default=true"` + BF bool `properties:",default=false"` + I int `properties:",default=-1"` + I8 int8 `properties:",default=-8"` + I16 int16 `properties:",default=-16"` + I32 int32 `properties:",default=-32"` + I64 int64 `properties:",default=-64"` + U uint `properties:",default=1"` + U8 uint8 `properties:",default=8"` + U16 uint16 `properties:",default=16"` + U32 uint32 `properties:",default=32"` + U64 uint64 `properties:",default=64"` + F32 float32 `properties:",default=3.2"` + F64 float64 `properties:",default=6.4"` + D time.Duration `properties:",default=5s"` + TM time.Time `properties:",default=2015-01-02T12:34:56Z"` + } + out := &S{ + S: "abc", + BT: true, + BF: false, + I: -1, + I8: -8, + I16: -16, + I32: -32, + I64: -64, + U: 1, + U8: 8, + U16: 16, + U32: 32, + U64: 64, + F32: 3.2, + F64: 6.4, + D: 5 * time.Second, + TM: tm(t, time.RFC3339, "2015-01-02T12:34:56Z"), + } + testDecode(t, "", &S{}, out) +} + +func TestDecodeArrays(t *testing.T) { + type S struct { + S []string + B []bool + I []int + I8 []int8 + I16 []int16 + I32 []int32 + I64 []int64 + U []uint + U8 []uint8 + U16 []uint16 + U32 []uint32 + U64 []uint64 + F32 []float32 + F64 []float64 + D []time.Duration + TM []time.Time + } + in := ` + S=a;b + B=true;false + I=-1;-2 + I8=-8;-9 + I16=-16;-17 + I32=-32;-33 + I64=-64;-65 + U=1;2 + U8=8;9 + U16=16;17 + U32=32;33 + U64=64;65 + F32=3.2;3.3 + F64=6.4;6.5 + D=4s;5s + TM=2015-01-01T00:00:00Z;2016-01-01T00:00:00Z + ` + out := &S{ + S: []string{"a", "b"}, + B: []bool{true, false}, + I: []int{-1, -2}, + I8: []int8{-8, -9}, + I16: []int16{-16, -17}, + I32: []int32{-32, -33}, + I64: []int64{-64, -65}, + U: []uint{1, 2}, + U8: []uint8{8, 9}, + U16: []uint16{16, 17}, + U32: []uint32{32, 33}, + U64: []uint64{64, 65}, + F32: []float32{3.2, 3.3}, + F64: []float64{6.4, 6.5}, + D: []time.Duration{4 * time.Second, 5 * time.Second}, + TM: []time.Time{tm(t, time.RFC3339, "2015-01-01T00:00:00Z"), tm(t, time.RFC3339, "2016-01-01T00:00:00Z")}, + } + testDecode(t, in, &S{}, out) +} + +func TestDecodeArrayDefaults(t *testing.T) { + type S struct { + S []string `properties:",default=a;b"` + B []bool `properties:",default=true;false"` + I []int `properties:",default=-1;-2"` + I8 []int8 `properties:",default=-8;-9"` + I16 []int16 `properties:",default=-16;-17"` + I32 []int32 `properties:",default=-32;-33"` + I64 []int64 `properties:",default=-64;-65"` + U []uint `properties:",default=1;2"` + U8 []uint8 `properties:",default=8;9"` + U16 []uint16 `properties:",default=16;17"` + U32 []uint32 `properties:",default=32;33"` + U64 []uint64 `properties:",default=64;65"` + F32 []float32 `properties:",default=3.2;3.3"` + F64 []float64 `properties:",default=6.4;6.5"` + D []time.Duration `properties:",default=4s;5s"` + TM []time.Time `properties:",default=2015-01-01T00:00:00Z;2016-01-01T00:00:00Z"` + } + out := &S{ + S: []string{"a", "b"}, + B: []bool{true, false}, + I: []int{-1, -2}, + I8: []int8{-8, -9}, + I16: []int16{-16, -17}, + I32: []int32{-32, -33}, + I64: []int64{-64, -65}, + U: []uint{1, 2}, + U8: []uint8{8, 9}, + U16: []uint16{16, 17}, + U32: []uint32{32, 33}, + U64: []uint64{64, 65}, + F32: []float32{3.2, 3.3}, + F64: []float64{6.4, 6.5}, + D: []time.Duration{4 * time.Second, 5 * time.Second}, + TM: []time.Time{tm(t, time.RFC3339, "2015-01-01T00:00:00Z"), tm(t, time.RFC3339, "2016-01-01T00:00:00Z")}, + } + testDecode(t, "", &S{}, out) +} + +func TestDecodeSkipUndef(t *testing.T) { + type S struct { + X string `properties:"-"` + Undef string `properties:",default=some value"` + } + in := `X=ignore` + out := &S{"", "some value"} + testDecode(t, in, &S{}, out) +} + +func TestDecodeStruct(t *testing.T) { + type A struct { + S string + T string `properties:"t"` + U string `properties:"u,default=uuu"` + } + type S struct { + A A + B A `properties:"b"` + } + in := ` + A.S=sss + A.t=ttt + b.S=SSS + b.t=TTT + ` + out := &S{ + A{S: "sss", T: "ttt", U: "uuu"}, + A{S: "SSS", T: "TTT", U: "uuu"}, + } + testDecode(t, in, &S{}, out) +} + +func TestDecodeMap(t *testing.T) { + type S struct { + A string `properties:"a"` + } + type X struct { + A map[string]string + B map[string][]string + C map[string]map[string]string + D map[string]S + } + in := ` + A.foo=bar + A.bar=bang + B.foo=a;b;c + B.bar=1;2;3 + C.foo.one=1 + C.foo.two=2 + C.bar.three=3 + C.bar.four=4 + D.foo.a=bar + ` + out := &X{ + A: map[string]string{"foo": "bar", "bar": "bang"}, + B: map[string][]string{"foo": []string{"a", "b", "c"}, "bar": []string{"1", "2", "3"}}, + C: map[string]map[string]string{"foo": map[string]string{"one": "1", "two": "2"}, "bar": map[string]string{"three": "3", "four": "4"}}, + D: map[string]S{"foo": S{"bar"}}, + } + testDecode(t, in, &X{}, out) +} + +func testDecode(t *testing.T, in string, v, out interface{}) { + p, err := parse(in) + if err != nil { + t.Fatalf("got %v want nil", err) + } + if err := p.Decode(v); err != nil { + t.Fatalf("got %v want nil", err) + } + if got, want := v, out; !reflect.DeepEqual(got, want) { + t.Fatalf("\ngot %+v\nwant %+v", got, want) + } +} + +func tm(t *testing.T, layout, s string) time.Time { + tm, err := time.Parse(layout, s) + if err != nil { + t.Fatalf("got %v want nil", err) + } + return tm +}
diff --git a/properties.go b/properties.go index 4abb4ab..bd5cba1 100644 --- a/properties.go +++ b/properties.go
@@ -181,12 +181,16 @@ func (p *Properties) getBool(key string) (value bool, err error) { if v, ok := p.Get(key); ok { - v = strings.ToLower(v) - return v == "1" || v == "true" || v == "yes" || v == "on", nil + return boolVal(v), nil } return false, invalidKeyError(key) } +func boolVal(v string) bool { + v = strings.ToLower(v) + return v == "1" || v == "true" || v == "yes" || v == "on" +} + // ---------------------------------------------------------------------------- // GetDuration parses the expanded value as an time.Duration (in ns) if the @@ -442,8 +446,8 @@ return pp } -// FilterPrefix returns a new properties object which contains all properties -// for which the key starts with the prefix. +// FilterPrefix returns a new properties object with a subset of all keys +// with the given prefix. func (p *Properties) FilterPrefix(prefix string) *Properties { pp := NewProperties() for _, k := range p.k { @@ -454,6 +458,19 @@ return pp } +// FilterStripPrefix returns a new properties object with a subset of all keys +// with the given prefix and the prefix removed from the keys. +func (p *Properties) FilterStripPrefix(prefix string) *Properties { + pp := NewProperties() + n := len(prefix) + for _, k := range p.k { + if len(k) > len(prefix) && strings.HasPrefix(k, prefix) { + pp.Set(k[n:], p.m[k]) + } + } + return pp +} + // Len returns the number of keys. func (p *Properties) Len() int { return len(p.m)
diff --git a/properties_test.go b/properties_test.go index 6da6e33..b01dbef 100644 --- a/properties_test.go +++ b/properties_test.go
@@ -403,6 +403,25 @@ // ---------------------------------------------------------------------------- +var filterStripPrefixTests = []struct { + input string + prefix string + keys []string +}{ + {"", "", []string{}}, + {"", "abc", []string{}}, + {"key=value", "", []string{"key"}}, + {"key=value", "key=", []string{}}, + {"key=value\nfoo=bar", "", []string{"foo", "key"}}, + {"key=value\nfoo=bar", "f", []string{"foo"}}, + {"key=value\nfoo=bar", "fo", []string{"foo"}}, + {"key=value\nfoo=bar", "foo", []string{"foo"}}, + {"key=value\nfoo=bar", "fooo", []string{}}, + {"key=value\nkey2=value2\nfoo=bar", "key", []string{"key", "key2"}}, +} + +// ---------------------------------------------------------------------------- + var setTests = []struct { input string key, value string