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