Files
cgrates/utils/reflect.go
2025-10-13 09:57:41 +02:00

812 lines
19 KiB
Go

/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
*/
package utils
import (
"errors"
"fmt"
"net"
"reflect"
"strconv"
"strings"
"time"
"github.com/ericlagergren/decimal"
)
// StringToInterface will parse string into supported types
// if no other conversion possible, original string will be returned
func StringToInterface(s string) any {
if s == EmptyString {
return s
}
// int64
s = strings.Trim(s, `"`)
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
// bool
if b, err := strconv.ParseBool(s); err == nil {
return b
}
// float64
if f, err := strconv.ParseFloat(s, 64); err == nil {
return f
}
// time.Time
if t, err := ParseTimeDetectLayout(s, "Local"); err == nil {
return t
}
// time.Duration
if d, err := time.ParseDuration(s); err == nil {
return d
}
// string
return s
}
func IfaceAsTime(itm any, timezone string) (t time.Time, err error) {
switch v := itm.(type) {
case time.Time:
return v, nil
case string:
return ParseTimeDetectLayout(v, timezone)
default:
err = fmt.Errorf("cannot convert field: %+v to time.Time", itm)
}
return
}
func StringAsBig(itm string) (b *decimal.Big, err error) {
if len(itm) == 0 {
return decimal.New(0, 0), nil
}
if strings.HasSuffix(itm, NsSuffix) ||
strings.HasSuffix(itm, UsSuffix) ||
strings.HasSuffix(itm, µSuffix) ||
strings.HasSuffix(itm, MsSuffix) ||
strings.HasSuffix(itm, SSuffix) ||
strings.HasSuffix(itm, MSuffix) ||
strings.HasSuffix(itm, HSuffix) {
var tm time.Duration
if tm, err = time.ParseDuration(itm); err != nil {
return
}
return decimal.New(int64(tm), 0), nil
}
z, ok := decimal.WithContext(DecimalContext).SetString(itm)
// verify ok and check if the value was converted successfuly
// and the big is a valid number
if !ok || z.IsNaN(0) {
return nil, fmt.Errorf("can't convert <%+v> to decimal", itm)
}
return z, nil
}
func IfaceAsBig(itm any) (b *decimal.Big, err error) {
switch it := itm.(type) {
case time.Duration:
return decimal.New(int64(it), 0), nil
case int: // check every int type
return decimal.New(int64(it), 0), nil
case int8:
return decimal.New(int64(it), 0), nil
case int16:
return decimal.New(int64(it), 0), nil
case int32:
return decimal.New(int64(it), 0), nil
case int64:
return decimal.New(it, 0), nil
case uint:
return decimal.WithContext(DecimalContext).SetUint64(uint64(it)), nil
case uint8:
return decimal.WithContext(DecimalContext).SetUint64(uint64(it)), nil
case uint16:
return decimal.WithContext(DecimalContext).SetUint64(uint64(it)), nil
case uint32:
return decimal.WithContext(DecimalContext).SetUint64(uint64(it)), nil
case uint64:
return decimal.WithContext(DecimalContext).SetUint64(it), nil
case float32: // automatically hitting here also ints
return decimal.WithContext(DecimalContext).SetFloat64(float64(it)), nil
case float64: // automatically hitting here also ints
return decimal.WithContext(DecimalContext).SetFloat64(it), nil
case string:
return StringAsBig(it)
case *Decimal:
return it.Big, nil
case *decimal.Big:
return it, nil
default:
err = fmt.Errorf("cannot convert field: %T to decimal.Big", it)
}
return
}
func IfaceAsDuration(itm any) (d time.Duration, err error) {
switch it := itm.(type) {
case time.Duration:
return it, nil
case int: // check every int type
return time.Duration(int64(it)), nil
case int8:
return time.Duration(int64(it)), nil
case int16:
return time.Duration(int64(it)), nil
case int32:
return time.Duration(int64(it)), nil
case int64:
return time.Duration(int64(it)), nil
case uint:
return time.Duration(int64(it)), nil
case uint8:
return time.Duration(int64(it)), nil
case uint16:
return time.Duration(int64(it)), nil
case uint32:
return time.Duration(int64(it)), nil
case uint64:
return time.Duration(int64(it)), nil
case float32: // automatically hitting here also ints
return time.Duration(int64(it)), nil
case float64: // automatically hitting here also ints
return time.Duration(int64(it)), nil
case string:
return ParseDurationWithNanosecs(it)
default:
err = fmt.Errorf("cannot convert field: %+v to time.Duration", it)
}
return
}
func IfaceAsInt64(itm any) (i int64, err error) {
switch it := itm.(type) {
case int:
return int64(it), nil
case time.Duration:
return it.Nanoseconds(), nil
case int32:
return int64(it), nil
case int64:
return it, nil
case string:
return strconv.ParseInt(it, 10, 64)
default:
err = fmt.Errorf("cannot convert field: %+v to int", it)
}
return
}
// IfaceAsTInt converts interface to type int
func IfaceAsInt(itm any) (i int, err error) {
switch it := itm.(type) {
case int:
return it, nil
case time.Duration:
return int(it.Nanoseconds()), nil
case int32:
return int(it), nil
case int64:
return int(it), nil
case float32:
return int(it), nil
case float64:
return int(it), nil
case string:
return strconv.Atoi(it)
default:
err = fmt.Errorf("cannot convert field<%T>: %+v to int", it, it)
}
return
}
// same function as IfaceAsInt64 but if the value is float round it to int64 instead of returning error
func IfaceAsTInt64(itm any) (i int64, err error) {
switch it := itm.(type) {
case int:
return int64(it), nil
case time.Duration:
return it.Nanoseconds(), nil
case int32:
return int64(it), nil
case int64:
return it, nil
case float32:
return int64(it), nil
case float64:
return int64(it), nil
case string:
return strconv.ParseInt(it, 10, 64)
default:
err = fmt.Errorf("cannot convert field<%T>: %+v to int", it, it)
}
return
}
func IfaceAsFloat64(itm any) (f float64, err error) {
switch it := itm.(type) {
case float64:
return it, nil
case time.Duration:
return float64(it.Nanoseconds()), nil
case int:
return float64(it), nil
case int64:
return float64(it), nil
case string:
return strconv.ParseFloat(it, 64)
default:
err = fmt.Errorf("cannot convert field: %+v to float64", it)
}
return
}
func IfaceAsBool(itm any) (b bool, err error) {
switch v := itm.(type) {
case bool:
return v, nil
case string:
if len(v) == 0 {
return
}
return strconv.ParseBool(v)
case int:
return v > 0, nil
case int32:
return v > 0, nil
case int64:
return v > 0, nil
case uint32:
return v > 0, nil
case uint64:
return v > 0, nil
case float64:
return v > 0, nil
default:
err = fmt.Errorf("cannot convert field: %+v to bool", itm)
}
return
}
func IfaceAsString(fld any) (out string) {
switch value := fld.(type) {
case nil:
return
case int:
return strconv.Itoa(value)
case int32:
return strconv.FormatInt(int64(value), 10)
case int64:
return strconv.FormatInt(value, 10)
case uint32:
return strconv.FormatUint(uint64(value), 10)
case uint64:
return strconv.FormatUint(value, 10)
case bool:
return strconv.FormatBool(value)
case float32:
return strconv.FormatFloat(float64(value), 'f', -1, 64)
case float64:
return strconv.FormatFloat(value, 'f', -1, 64)
case []uint8:
return string(value) // byte is an alias for uint8 conversions implicit
case time.Duration:
return value.String()
case time.Time:
return value.Format(time.RFC3339)
case net.IP:
return value.String()
case string:
return value
default: // Maybe we are lucky and the value converts to string
return ToJSON(fld)
}
}
func GetUniformType(item any) (any, error) {
valItm := reflect.ValueOf(item)
switch valItm.Kind() { // convert evreting to float64
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(valItm.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(valItm.Uint()), nil
case reflect.Float32, reflect.Float64:
return valItm.Float(), nil
case reflect.Struct: // used only for time
return valItm.Interface(), nil
default:
return nil, errors.New("incomparable")
}
}
func GetBasicType(item any) any {
valItm := reflect.ValueOf(item)
switch valItm.Kind() { // convert evreting to float64
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return valItm.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return valItm.Uint()
case reflect.Float32, reflect.Float64:
return valItm.Float()
default:
return item
}
}
// GreaterThan attempts to compare two items
// returns the result or error if not comparable
func GreaterThan(item, oItem any, orEqual bool) (gte bool, err error) {
item = GetBasicType(item)
oItem = GetBasicType(oItem)
typItem := reflect.TypeOf(item)
typOItem := reflect.TypeOf(oItem)
if typItem != typOItem {
if item, err = GetUniformType(item); err == nil { // overwrite type only if possible
typItem = reflect.TypeOf(item)
}
if oItem, err = GetUniformType(oItem); err == nil {
typOItem = reflect.TypeOf(oItem)
}
}
if !typItem.Comparable() ||
!typOItem.Comparable() ||
typItem != typOItem {
return false, fmt.Errorf("incomparable: <%+v> with <%+v>", item, oItem)
}
switch tVal := item.(type) {
case float64:
tOVal := oItem.(float64)
if orEqual {
gte = tVal >= tOVal
} else {
gte = tVal > tOVal
}
case uint64:
tOVal := oItem.(uint64)
if orEqual {
gte = tVal >= tOVal
} else {
gte = tVal > tOVal
}
case int64:
tOVal := oItem.(int64)
if orEqual {
gte = tVal >= tOVal
} else {
gte = tVal > tOVal
}
case time.Time:
tOVal := oItem.(time.Time)
if orEqual {
gte = tVal == tOVal
}
if !gte {
gte = tVal.After(tOVal)
}
default: // unsupported comparison
err = fmt.Errorf("unsupported comparison type: %v, kind: %v", typItem, typItem.Kind())
}
return
}
func EqualTo(item, oItem any) (eq bool, err error) {
item = GetBasicType(item)
oItem = GetBasicType(oItem)
typItem := reflect.TypeOf(item)
typOItem := reflect.TypeOf(oItem)
if typItem != typOItem {
if item, err = GetUniformType(item); err == nil { // overwrite type only if possible
typItem = reflect.TypeOf(item)
}
if oItem, err = GetUniformType(oItem); err == nil {
typOItem = reflect.TypeOf(oItem)
}
}
if !typItem.Comparable() ||
!typOItem.Comparable() ||
typItem != typOItem {
return false, fmt.Errorf("incomparable: <%+v> with <%+v>", item, oItem)
}
switch tVal := item.(type) {
case float64:
tOVal := oItem.(float64)
eq = tVal == tOVal
case uint64:
tOVal := oItem.(uint64)
eq = tVal == tOVal
case int64:
tOVal := oItem.(int64)
eq = tVal == tOVal
case time.Time:
tOVal := oItem.(time.Time)
eq = tVal == tOVal
case string:
tOVal := oItem.(string)
eq = tVal == tOVal
default: // unsupported comparison
err = fmt.Errorf("unsupported comparison type: %v, kind: %v", typItem, typItem.Kind())
}
return
}
// Sum attempts to sum multiple items
// returns the result or error if not comparable
func Sum(items ...any) (sum any, err error) {
//we need at least 2 items to sum them
if len(items) < 2 {
return nil, ErrNotEnoughParameters
}
switch dt := items[0].(type) {
case time.Duration:
s := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsDuration(item); err != nil {
return nil, err
} else {
s += itmVal
}
}
sum = s
case time.Time:
s := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsDuration(item); err != nil {
return nil, err
} else {
s = s.Add(itmVal)
}
}
sum = s
case float64:
s := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsFloat64(item); err != nil {
return nil, err
} else {
s += itmVal
}
}
sum = s
case int64:
s := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
s += itmVal
}
}
sum = s
case int:
s := int64(dt)
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
s += itmVal
}
}
sum = s
}
return
}
// Difference attempts to sum multiple items
// returns the result or error if not comparable
func Difference(tm string, items ...any) (diff any, err error) {
//we need at least 2 items to diff them
if len(items) < 2 {
return nil, ErrNotEnoughParameters
}
switch dt := items[0].(type) {
case time.Duration:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsDuration(item); err != nil {
return nil, err
} else {
d -= itmVal
}
}
diff = d
case time.Time:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsTime(item, tm); err == nil {
diff = d.Sub(itmVal)
return diff, nil
}
if itmVal, err := IfaceAsDuration(item); err != nil {
return nil, err
} else {
d = d.Add(-itmVal)
}
}
diff = d
case float64:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsFloat64(item); err != nil {
return nil, err
} else {
d -= itmVal
}
}
diff = d
case int64:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
d -= itmVal
}
}
diff = d
case int:
d := int64(dt)
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
d -= itmVal
}
}
diff = d
default: // unsupported comparison
return nil, fmt.Errorf("unsupported type")
}
return
}
// Multiply attempts to multiply multiple items
// returns the result or error if not comparable
func Multiply(items ...any) (mlt any, err error) {
//we need at least 2 items to diff them
if len(items) < 2 {
return nil, ErrNotEnoughParameters
}
switch dt := items[0].(type) {
case float64:
m := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsFloat64(item); err != nil {
return nil, err
} else {
m *= itmVal
}
}
mlt = m
case int64:
m := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
m *= itmVal
}
}
mlt = m
case int:
m := int64(dt)
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
m *= itmVal
}
}
mlt = m
default: // unsupported comparison
return nil, fmt.Errorf("unsupported type")
}
return
}
// Divide attempts to divide multiple items
// returns the result or error if not comparable
func Divide(items ...any) (div any, err error) {
//we need at least 2 items to diff them
if len(items) < 2 {
return nil, ErrNotEnoughParameters
}
switch dt := items[0].(type) {
case float64:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsFloat64(item); err != nil {
return nil, err
} else {
d /= itmVal
}
}
div = d
case int64:
d := dt
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
d /= itmVal
}
}
div = d
case int:
d := int64(dt)
for _, item := range items[1:] {
if itmVal, err := IfaceAsInt64(item); err != nil {
return nil, err
} else {
d /= itmVal
}
}
div = d
default: // unsupported comparison
return nil, fmt.Errorf("unsupported type")
}
return
}
// ReflectFieldMethodInterface parses intf attepting to return the field value or error otherwise
// Supports "ExtraFields" where additional fields are dynamically inserted in map with field name: extraFieldsLabel
func ReflectFieldMethodInterface(obj any, fldName string) (retIf any, err error) {
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
var field reflect.Value
switch v.Kind() {
case reflect.Struct:
field = v.FieldByName(fldName)
case reflect.Map:
field = v.MapIndex(reflect.ValueOf(fldName))
case reflect.Slice, reflect.Array:
//convert fldName to int
idx, err := strconv.Atoi(fldName)
if err != nil {
return nil, err
}
if idx >= v.Len() {
return nil, fmt.Errorf("index out of range")
}
field = v.Index(idx)
default:
return nil, fmt.Errorf("unsupported field kind: %v", v.Kind())
}
if !field.IsValid() {
// handle function with pointer
v = reflect.ValueOf(obj)
field = v.MethodByName(fldName)
if !field.IsValid() {
return nil, ErrNotFound
} else {
if field.Type().NumIn() != 0 {
return nil, fmt.Errorf("invalid function called")
}
if field.Type().NumOut() > 2 {
return nil, fmt.Errorf("invalid function called")
}
// the function have two parameters in return and check if the second is of type error
if field.Type().NumOut() == 2 {
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if !field.Type().Out(1).Implements(errorInterface) {
return nil, fmt.Errorf("invalid function called")
}
}
fields := field.Call([]reflect.Value{})
if len(fields) == 2 && !fields[1].IsNil() {
return fields[0].Interface(), fields[1].Interface().(error)
}
return fields[0].Interface(), nil
}
}
return field.Interface(), nil
}
func IfaceAsStringSlice(fld any) (ss []string, err error) {
switch value := fld.(type) {
case nil:
case []string:
ss = value
case []int:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.Itoa(v)
}
case []int32:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatInt(int64(v), 10)
}
case []int64:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatInt(v, 10)
}
case []uint32:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatUint(uint64(v), 10)
}
case []uint64:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatUint(v, 10)
}
case []bool:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatBool(v)
}
case []float32:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatFloat(float64(v), 'f', -1, 64)
}
case []float64:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = strconv.FormatFloat(v, 'f', -1, 64)
}
case []any:
ss = make([]string, len(value))
for i, v := range value {
ss[i] = IfaceAsString(v)
}
case string:
if value != EmptyString {
ss = strings.Split(value, InfieldSep)
}
default: // Maybe we are lucky and the value converts to string
err = fmt.Errorf("cannot convert field: %+v to []string", value)
}
return
}
func OptAsBool(opts map[string]any, name string) (b bool) {
val, has := opts[name]
if !has {
return
}
b, _ = IfaceAsBool(val)
return
}
func OptAsBoolOrDef(opts map[string]any, name string, def bool) (b bool) {
val, has := opts[name]
if !has {
return def
}
b, _ = IfaceAsBool(val)
return
}
func OptAsStringSlice(opts map[string]any, name string) (b []string, err error) {
val, has := opts[name]
if !has {
return
}
if b, err = IfaceAsStringSlice(val); err != nil {
err = fmt.Errorf("error for option <%s>: %s", name, err)
}
return
}