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.
653 lines
14 KiB
653 lines
14 KiB
package mapping |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"git.diulo.com/mogfee/kit/core/lang" |
|
"git.diulo.com/mogfee/kit/core/stringx" |
|
"math" |
|
"reflect" |
|
"strconv" |
|
"strings" |
|
"sync" |
|
) |
|
|
|
const ( |
|
defaultOption = "default" |
|
envOption = "env" |
|
inheritOption = "inherit" |
|
stringOption = "string" |
|
optionalOption = "optional" |
|
optionsOption = "options" |
|
rangeOption = "range" |
|
optionSeparator = "|" |
|
equalToken = "=" |
|
escapeChar = '\\' |
|
leftBracket = '(' |
|
rightBracket = ')' |
|
leftSquareBracket = '[' |
|
rightSquareBracket = ']' |
|
segmentSeparator = ',' |
|
) |
|
|
|
var ( |
|
errUnsupportedType = errors.New("unsupported type on setting field value") |
|
errNumberRange = errors.New("wrong number range setting") |
|
optionsCache = make(map[string]optionsCacheValue) |
|
cacheLock sync.RWMutex |
|
structRequiredCache = make(map[reflect.Type]requiredCacheValue) |
|
structCacheLock sync.RWMutex |
|
) |
|
|
|
type ( |
|
optionsCacheValue struct { |
|
key string |
|
options *fieldOptions |
|
err error |
|
} |
|
|
|
requiredCacheValue struct { |
|
required bool |
|
err error |
|
} |
|
) |
|
|
|
// Deref dereferences a type, if pointer type, returns its element type. |
|
func Deref(t reflect.Type) reflect.Type { |
|
for t.Kind() == reflect.Ptr { |
|
t = t.Elem() |
|
} |
|
|
|
return t |
|
} |
|
|
|
// Repr returns the string representation of v. |
|
func Repr(v any) string { |
|
return lang.Repr(v) |
|
} |
|
|
|
// SetValue sets target to value, pointers are processed automatically. |
|
func SetValue(tp reflect.Type, value, target reflect.Value) { |
|
value.Set(convertTypeOfPtr(tp, target)) |
|
} |
|
|
|
// SetMapIndexValue sets target to value at key position, pointers are processed automatically. |
|
func SetMapIndexValue(tp reflect.Type, value, key, target reflect.Value) { |
|
value.SetMapIndex(key, convertTypeOfPtr(tp, target)) |
|
} |
|
|
|
// ValidatePtr validates v if it's a valid pointer. |
|
func ValidatePtr(v *reflect.Value) error { |
|
// sequence is very important, IsNil must be called after checking Kind() with reflect.Ptr, |
|
// panic otherwise |
|
if !v.IsValid() || v.Kind() != reflect.Ptr || v.IsNil() { |
|
return fmt.Errorf("not a valid pointer: %v", v) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func convertTypeFromString(kind reflect.Kind, str string) (any, error) { |
|
switch kind { |
|
case reflect.Bool: |
|
switch strings.ToLower(str) { |
|
case "1", "true": |
|
return true, nil |
|
case "0", "false": |
|
return false, nil |
|
default: |
|
return false, errTypeMismatch |
|
} |
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
intValue, err := strconv.ParseInt(str, 10, 64) |
|
if err != nil { |
|
return 0, fmt.Errorf("the value %q cannot parsed as int", str) |
|
} |
|
|
|
return intValue, nil |
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
|
uintValue, err := strconv.ParseUint(str, 10, 64) |
|
if err != nil { |
|
return 0, fmt.Errorf("the value %q cannot parsed as uint", str) |
|
} |
|
|
|
return uintValue, nil |
|
case reflect.Float32, reflect.Float64: |
|
floatValue, err := strconv.ParseFloat(str, 64) |
|
if err != nil { |
|
return 0, fmt.Errorf("the value %q cannot parsed as float", str) |
|
} |
|
|
|
return floatValue, nil |
|
case reflect.String: |
|
return str, nil |
|
default: |
|
return nil, errUnsupportedType |
|
} |
|
} |
|
|
|
func convertTypeOfPtr(tp reflect.Type, target reflect.Value) reflect.Value { |
|
// keep the original value is a pointer |
|
if tp.Kind() == reflect.Ptr && target.CanAddr() { |
|
tp = tp.Elem() |
|
target = target.Addr() |
|
} |
|
|
|
for tp.Kind() == reflect.Ptr { |
|
p := reflect.New(target.Type()) |
|
p.Elem().Set(target) |
|
target = p |
|
tp = tp.Elem() |
|
} |
|
|
|
return target |
|
} |
|
|
|
func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fieldOptions, error) { |
|
segments := parseSegments(value) |
|
key := strings.TrimSpace(segments[0]) |
|
options := segments[1:] |
|
|
|
if len(options) == 0 { |
|
return key, nil, nil |
|
} |
|
|
|
var fieldOpts fieldOptions |
|
for _, segment := range options { |
|
option := strings.TrimSpace(segment) |
|
if err := parseOption(&fieldOpts, field.Name, option); err != nil { |
|
return "", nil, err |
|
} |
|
} |
|
|
|
return key, &fieldOpts, nil |
|
} |
|
|
|
// ensureValue ensures nested members not to be nil. |
|
// If pointer value is nil, set to a new value. |
|
func ensureValue(v reflect.Value) reflect.Value { |
|
for { |
|
if v.Kind() != reflect.Ptr { |
|
break |
|
} |
|
|
|
if v.IsNil() { |
|
v.Set(reflect.New(v.Type().Elem())) |
|
} |
|
v = v.Elem() |
|
} |
|
|
|
return v |
|
} |
|
|
|
func implicitValueRequiredStruct(tag string, tp reflect.Type) (bool, error) { |
|
numFields := tp.NumField() |
|
for i := 0; i < numFields; i++ { |
|
childField := tp.Field(i) |
|
if usingDifferentKeys(tag, childField) { |
|
return true, nil |
|
} |
|
|
|
_, opts, err := parseKeyAndOptions(tag, childField) |
|
if err != nil { |
|
return false, err |
|
} |
|
|
|
if opts == nil { |
|
if childField.Type.Kind() != reflect.Struct { |
|
return true, nil |
|
} |
|
|
|
if required, err := implicitValueRequiredStruct(tag, childField.Type); err != nil { |
|
return false, err |
|
} else if required { |
|
return true, nil |
|
} |
|
} else if !opts.Optional && len(opts.Default) == 0 { |
|
return true, nil |
|
} else if len(opts.OptionalDep) > 0 && opts.OptionalDep[0] == notSymbol { |
|
return true, nil |
|
} |
|
} |
|
|
|
return false, nil |
|
} |
|
|
|
func isLeftInclude(b byte) (bool, error) { |
|
switch b { |
|
case '[': |
|
return true, nil |
|
case '(': |
|
return false, nil |
|
default: |
|
return false, errNumberRange |
|
} |
|
} |
|
|
|
func isRightInclude(b byte) (bool, error) { |
|
switch b { |
|
case ']': |
|
return true, nil |
|
case ')': |
|
return false, nil |
|
default: |
|
return false, errNumberRange |
|
} |
|
} |
|
|
|
func maybeNewValue(fieldType reflect.Type, value reflect.Value) { |
|
if fieldType.Kind() == reflect.Ptr && value.IsNil() { |
|
value.Set(reflect.New(value.Type().Elem())) |
|
} |
|
} |
|
|
|
func parseGroupedSegments(val string) []string { |
|
val = strings.TrimLeftFunc(val, func(r rune) bool { |
|
return r == leftBracket || r == leftSquareBracket |
|
}) |
|
val = strings.TrimRightFunc(val, func(r rune) bool { |
|
return r == rightBracket || r == rightSquareBracket |
|
}) |
|
return parseSegments(val) |
|
} |
|
|
|
// don't modify returned fieldOptions, it's cached and shared among different calls. |
|
func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fieldOptions, error) { |
|
value := field.Tag.Get(tagName) |
|
if len(value) == 0 { |
|
return field.Name, nil, nil |
|
} |
|
|
|
cacheLock.RLock() |
|
cache, ok := optionsCache[value] |
|
cacheLock.RUnlock() |
|
if ok { |
|
return stringx.TakeOne(cache.key, field.Name), cache.options, cache.err |
|
} |
|
|
|
key, options, err := doParseKeyAndOptions(field, value) |
|
cacheLock.Lock() |
|
optionsCache[value] = optionsCacheValue{ |
|
key: key, |
|
options: options, |
|
err: err, |
|
} |
|
cacheLock.Unlock() |
|
|
|
return stringx.TakeOne(key, field.Name), options, err |
|
} |
|
|
|
// support below notations: |
|
// [:5] (:5] [:5) (:5) |
|
// [1:] [1:) (1:] (1:) |
|
// [1:5] [1:5) (1:5] (1:5) |
|
func parseNumberRange(str string) (*numberRange, error) { |
|
if len(str) == 0 { |
|
return nil, errNumberRange |
|
} |
|
|
|
leftInclude, err := isLeftInclude(str[0]) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
str = str[1:] |
|
if len(str) == 0 { |
|
return nil, errNumberRange |
|
} |
|
|
|
rightInclude, err := isRightInclude(str[len(str)-1]) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
str = str[:len(str)-1] |
|
fields := strings.Split(str, ":") |
|
if len(fields) != 2 { |
|
return nil, errNumberRange |
|
} |
|
|
|
if len(fields[0]) == 0 && len(fields[1]) == 0 { |
|
return nil, errNumberRange |
|
} |
|
|
|
var left float64 |
|
if len(fields[0]) > 0 { |
|
var err error |
|
if left, err = strconv.ParseFloat(fields[0], 64); err != nil { |
|
return nil, err |
|
} |
|
} else { |
|
left = -math.MaxFloat64 |
|
} |
|
|
|
var right float64 |
|
if len(fields[1]) > 0 { |
|
var err error |
|
if right, err = strconv.ParseFloat(fields[1], 64); err != nil { |
|
return nil, err |
|
} |
|
} else { |
|
right = math.MaxFloat64 |
|
} |
|
|
|
if left > right { |
|
return nil, errNumberRange |
|
} |
|
|
|
// [2:2] valid |
|
// [2:2) invalid |
|
// (2:2] invalid |
|
// (2:2) invalid |
|
if left == right { |
|
if !leftInclude || !rightInclude { |
|
return nil, errNumberRange |
|
} |
|
} |
|
|
|
return &numberRange{ |
|
left: left, |
|
leftInclude: leftInclude, |
|
right: right, |
|
rightInclude: rightInclude, |
|
}, nil |
|
} |
|
|
|
func parseOption(fieldOpts *fieldOptions, fieldName, option string) error { |
|
switch { |
|
case option == inheritOption: |
|
fieldOpts.Inherit = true |
|
case option == stringOption: |
|
fieldOpts.FromString = true |
|
case strings.HasPrefix(option, optionalOption): |
|
segs := strings.Split(option, equalToken) |
|
switch len(segs) { |
|
case 1: |
|
fieldOpts.Optional = true |
|
case 2: |
|
fieldOpts.Optional = true |
|
fieldOpts.OptionalDep = segs[1] |
|
default: |
|
return fmt.Errorf("field %s has wrong optional", fieldName) |
|
} |
|
case option == optionalOption: |
|
fieldOpts.Optional = true |
|
case strings.HasPrefix(option, optionsOption): |
|
val, err := parseProperty(fieldName, optionsOption, option) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
fieldOpts.Options = parseOptions(val) |
|
case strings.HasPrefix(option, defaultOption): |
|
val, err := parseProperty(fieldName, defaultOption, option) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
fieldOpts.Default = val |
|
case strings.HasPrefix(option, envOption): |
|
val, err := parseProperty(fieldName, envOption, option) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
fieldOpts.EnvVar = val |
|
case strings.HasPrefix(option, rangeOption): |
|
val, err := parseProperty(fieldName, rangeOption, option) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
nr, err := parseNumberRange(val) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
fieldOpts.Range = nr |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// parseOptions parses the given options in tag. |
|
// for example: `json:"name,options=foo|bar"` or `json:"name,options=[foo,bar]"` |
|
func parseOptions(val string) []string { |
|
if len(val) == 0 { |
|
return nil |
|
} |
|
|
|
if val[0] == leftSquareBracket { |
|
return parseGroupedSegments(val) |
|
} |
|
|
|
return strings.Split(val, optionSeparator) |
|
} |
|
|
|
func parseProperty(field, tag, val string) (string, error) { |
|
segs := strings.Split(val, equalToken) |
|
if len(segs) != 2 { |
|
return "", fmt.Errorf("field %s has wrong %s", field, tag) |
|
} |
|
|
|
return strings.TrimSpace(segs[1]), nil |
|
} |
|
|
|
func parseSegments(val string) []string { |
|
var segments []string |
|
var escaped, grouped bool |
|
var buf strings.Builder |
|
|
|
for _, ch := range val { |
|
if escaped { |
|
buf.WriteRune(ch) |
|
escaped = false |
|
continue |
|
} |
|
|
|
switch ch { |
|
case segmentSeparator: |
|
if grouped { |
|
buf.WriteRune(ch) |
|
} else { |
|
// need to trim spaces, but we cannot ignore empty string, |
|
// because the first segment stands for the key might be empty. |
|
// if ignored, the later tag will be used as the key. |
|
segments = append(segments, strings.TrimSpace(buf.String())) |
|
buf.Reset() |
|
} |
|
case escapeChar: |
|
if grouped { |
|
buf.WriteRune(ch) |
|
} else { |
|
escaped = true |
|
} |
|
case leftBracket, leftSquareBracket: |
|
buf.WriteRune(ch) |
|
grouped = true |
|
case rightBracket, rightSquareBracket: |
|
buf.WriteRune(ch) |
|
grouped = false |
|
default: |
|
buf.WriteRune(ch) |
|
} |
|
} |
|
|
|
last := strings.TrimSpace(buf.String()) |
|
// ignore last empty string |
|
if len(last) > 0 { |
|
segments = append(segments, last) |
|
} |
|
|
|
return segments |
|
} |
|
|
|
func setMatchedPrimitiveValue(kind reflect.Kind, value reflect.Value, v any) error { |
|
switch kind { |
|
case reflect.Bool: |
|
value.SetBool(v.(bool)) |
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
value.SetInt(v.(int64)) |
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
|
value.SetUint(v.(uint64)) |
|
case reflect.Float32, reflect.Float64: |
|
value.SetFloat(v.(float64)) |
|
case reflect.String: |
|
value.SetString(v.(string)) |
|
default: |
|
return errUnsupportedType |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func setValueFromString(kind reflect.Kind, value reflect.Value, str string) error { |
|
if !value.CanSet() { |
|
return errValueNotSettable |
|
} |
|
|
|
value = ensureValue(value) |
|
v, err := convertTypeFromString(kind, str) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return setMatchedPrimitiveValue(kind, value, v) |
|
} |
|
|
|
func structValueRequired(tag string, tp reflect.Type) (bool, error) { |
|
structCacheLock.RLock() |
|
val, ok := structRequiredCache[tp] |
|
structCacheLock.RUnlock() |
|
if ok { |
|
return val.required, val.err |
|
} |
|
|
|
required, err := implicitValueRequiredStruct(tag, tp) |
|
structCacheLock.Lock() |
|
structRequiredCache[tp] = requiredCacheValue{ |
|
required: required, |
|
err: err, |
|
} |
|
structCacheLock.Unlock() |
|
|
|
return required, err |
|
} |
|
|
|
func toFloat64(v any) (float64, bool) { |
|
switch val := v.(type) { |
|
case int: |
|
return float64(val), true |
|
case int8: |
|
return float64(val), true |
|
case int16: |
|
return float64(val), true |
|
case int32: |
|
return float64(val), true |
|
case int64: |
|
return float64(val), true |
|
case uint: |
|
return float64(val), true |
|
case uint8: |
|
return float64(val), true |
|
case uint16: |
|
return float64(val), true |
|
case uint32: |
|
return float64(val), true |
|
case uint64: |
|
return float64(val), true |
|
case float32: |
|
return float64(val), true |
|
case float64: |
|
return val, true |
|
default: |
|
return 0, false |
|
} |
|
} |
|
|
|
func usingDifferentKeys(key string, field reflect.StructField) bool { |
|
if len(field.Tag) > 0 { |
|
if _, ok := field.Tag.Lookup(key); !ok { |
|
return true |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
func validateAndSetValue(kind reflect.Kind, value reflect.Value, str string, opts *fieldOptionsWithContext) error { |
|
if !value.CanSet() { |
|
return errValueNotSettable |
|
} |
|
|
|
v, err := convertTypeFromString(kind, str) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if err := validateValueRange(v, opts); err != nil { |
|
return err |
|
} |
|
|
|
return setMatchedPrimitiveValue(kind, value, v) |
|
} |
|
|
|
func validateJsonNumberRange(v json.Number, opts *fieldOptionsWithContext) error { |
|
if opts == nil || opts.Range == nil { |
|
return nil |
|
} |
|
|
|
fv, err := v.Float64() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return validateNumberRange(fv, opts.Range) |
|
} |
|
|
|
func validateNumberRange(fv float64, nr *numberRange) error { |
|
if nr == nil { |
|
return nil |
|
} |
|
|
|
if (nr.leftInclude && fv < nr.left) || (!nr.leftInclude && fv <= nr.left) { |
|
return errNumberRange |
|
} |
|
|
|
if (nr.rightInclude && fv > nr.right) || (!nr.rightInclude && fv >= nr.right) { |
|
return errNumberRange |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func validateValueInOptions(val any, options []string) error { |
|
if len(options) > 0 { |
|
switch v := val.(type) { |
|
case string: |
|
if !stringx.Contains(options, v) { |
|
return fmt.Errorf(`error: value "%s" is not defined in options "%v"`, v, options) |
|
} |
|
default: |
|
if !stringx.Contains(options, Repr(v)) { |
|
return fmt.Errorf(`error: value "%v" is not defined in options "%v"`, val, options) |
|
} |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func validateValueRange(mapValue any, opts *fieldOptionsWithContext) error { |
|
if opts == nil || opts.Range == nil { |
|
return nil |
|
} |
|
|
|
fv, ok := toFloat64(mapValue) |
|
if !ok { |
|
return errNumberRange |
|
} |
|
|
|
return validateNumberRange(fv, opts.Range) |
|
}
|
|
|