Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
8.4 KiB
289 lines
8.4 KiB
// Copyright 2018 Frank Schroeder. All rights reserved. |
|
// Use of this source code is governed by a BSD-style |
|
// license that can be found in the LICENSE file. |
|
|
|
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"` |
|
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 %s", 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 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 |
|
}
|
|
|