| package properties |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| // Decode assigns property values to exported fields of a struct or a |
| // map[string]interface{}. |
| // |
| // Decode traverses x recursively and returns an error if a value cannot be |
| // converted to the field type or a required value is missing for a field. |
| // If x is a map[string]interface{} the keys are traversed using dot notation. |
| // Values which contain semicolons are decoded as []string and all other |
| // values are decoded as string values. |
| // |
| // For structs 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"` |
| func (p *Properties) Decode(x interface{}) error { |
| t, v := reflect.TypeOf(x), reflect.ValueOf(x) |
| elemT := v.Elem().Type() |
| |
| switch { |
| case isPtr(t) && isMap(elemT): |
| m, ok := x.(*map[string]interface{}) |
| if !ok { |
| return fmt.Errorf("not a map[string]interface{}: %s", elemT) |
| } |
| for key, val := range p.m { |
| decMap(*m, key, val) |
| } |
| return nil |
| |
| case isPtr(t) && isStruct(elemT): |
| if err := decStruct(p, "", nil, nil, v); err != nil { |
| return err |
| } |
| return nil |
| |
| default: |
| return fmt.Errorf("not a pointer to map or struct: %s", t) |
| } |
| } |
| |
| func decMap(m map[string]interface{}, key, val string) { |
| if !strings.Contains(key, ".") { |
| // TODO(cp): why support arrays and why semicolons instead of comma like decStruct? |
| if strings.Contains(val, ";") { |
| m[key] = split(val, ";") |
| } else { |
| m[key] = val |
| } |
| return |
| } |
| |
| kk := strings.SplitN(key, ".", 2) |
| prefix, rest := kk[0], kk[1] |
| if _, ok := m[prefix]; !ok { |
| m[prefix] = map[string]interface{}{} |
| } |
| decMap(m[prefix].(map[string]interface{}), rest, val) |
| } |
| |
| func decStruct(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 decStruct(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 := decStruct(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 := decStruct(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 |
| } |