1
0
Fork 0
golang-github-nicholas-fedo.../pkg/format/formatter.go
Daniel Baumann c0c4addb85
Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 10:16:14 +02:00

509 lines
14 KiB
Go

package format
import (
"errors"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
"github.com/nicholas-fedor/shoutrrr/pkg/util"
)
// Constants for map parsing and type sizes.
const (
KeyValuePairSize = 2 // Number of elements in a key:value pair
Int32BitSize = 32 // Bit size for 32-bit integers
Int64BitSize = 64 // Bit size for 64-bit integers
)
// Errors defined as static variables for better error handling.
var (
ErrInvalidEnumValue = errors.New("not a valid enum value")
ErrInvalidBoolValue = errors.New("accepted values are 1, true, yes or 0, false, no")
ErrUnsupportedFieldKey = errors.New("field key format is not supported")
ErrInvalidFieldValue = errors.New("invalid field value format")
ErrUnsupportedField = errors.New("field format is not supported")
ErrInvalidFieldCount = errors.New("invalid field value count")
ErrInvalidFieldKind = errors.New("invalid field kind")
ErrUnsupportedMapValue = errors.New("map value format is not supported")
ErrInvalidFieldValueData = errors.New("invalid field value")
ErrFailedToSetEnumValue = errors.New("failed to set enum value")
ErrUnexpectedUintKind = errors.New("unexpected uint kind")
ErrUnexpectedIntKind = errors.New("unexpected int kind")
ErrParseIntFailed = errors.New("failed to parse integer")
ErrParseUintFailed = errors.New("failed to parse unsigned integer")
)
// GetServiceConfig extracts the inner config from a service.
func GetServiceConfig(service types.Service) types.ServiceConfig {
serviceValue := reflect.Indirect(reflect.ValueOf(service))
configField, ok := serviceValue.Type().FieldByName("Config")
if !ok {
panic("service does not have a Config field")
}
configRef := serviceValue.FieldByIndex(configField.Index)
if configRef.IsNil() {
configType := configField.Type
if configType.Kind() == reflect.Ptr {
configType = configType.Elem()
}
newConfig := reflect.New(configType).Interface()
if config, ok := newConfig.(types.ServiceConfig); ok {
return config
}
panic("failed to create new config instance")
}
if config, ok := configRef.Interface().(types.ServiceConfig); ok {
return config
}
panic("config reference is not a ServiceConfig")
}
// ColorFormatTree generates a color-highlighted string representation of a node tree.
func ColorFormatTree(rootNode *ContainerNode, withValues bool) string {
return ConsoleTreeRenderer{WithValues: withValues}.RenderTree(rootNode, "")
}
// GetServiceConfigFormat retrieves type and field information from a service's config.
func GetServiceConfigFormat(service types.Service) *ContainerNode {
return GetConfigFormat(GetServiceConfig(service))
}
// GetConfigFormat retrieves type and field information from a ServiceConfig.
func GetConfigFormat(config types.ServiceConfig) *ContainerNode {
return getRootNode(config)
}
// SetConfigField updates a config field with a deserialized value from a string.
func SetConfigField(config reflect.Value, field FieldInfo, inputValue string) (bool, error) {
configField := config.FieldByName(field.Name)
if field.EnumFormatter != nil {
return setEnumField(configField, field, inputValue)
}
switch field.Type.Kind() {
case reflect.String:
configField.SetString(inputValue)
return true, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return setIntField(configField, field, inputValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return setUintField(configField, field, inputValue)
case reflect.Bool:
return setBoolField(configField, inputValue)
case reflect.Map:
return setMapField(configField, field, inputValue)
case reflect.Struct:
return setStructField(configField, field, inputValue)
case reflect.Slice, reflect.Array:
return setSliceOrArrayField(configField, field, inputValue)
case reflect.Invalid,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Pointer,
reflect.UnsafePointer:
return false, fmt.Errorf("%w: %v", ErrInvalidFieldKind, field.Type.Kind())
default:
return false, fmt.Errorf("%w: %v", ErrInvalidFieldKind, field.Type.Kind())
}
}
// setIntField handles integer field setting.
func setIntField(configField reflect.Value, field FieldInfo, inputValue string) (bool, error) {
number, base := util.StripNumberPrefix(inputValue)
value, err := strconv.ParseInt(number, base, field.Type.Bits())
if err != nil {
return false, fmt.Errorf("%w: %w", ErrParseIntFailed, err)
}
configField.SetInt(value)
return true, nil
}
// setUintField handles unsigned integer field setting.
func setUintField(configField reflect.Value, field FieldInfo, inputValue string) (bool, error) {
number, base := util.StripNumberPrefix(inputValue)
value, err := strconv.ParseUint(number, base, field.Type.Bits())
if err != nil {
return false, fmt.Errorf("%w: %w", ErrParseUintFailed, err)
}
configField.SetUint(value)
return true, nil
}
// setBoolField handles boolean field setting.
func setBoolField(configField reflect.Value, inputValue string) (bool, error) {
value, ok := ParseBool(inputValue, false)
if !ok {
return false, ErrInvalidBoolValue
}
configField.SetBool(value)
return true, nil
}
// setMapField handles map field setting.
func setMapField(configField reflect.Value, field FieldInfo, inputValue string) (bool, error) {
if field.Type.Key().Kind() != reflect.String {
return false, ErrUnsupportedFieldKey
}
mapValue := reflect.MakeMap(field.Type)
pairs := strings.Split(inputValue, ",")
for _, pair := range pairs {
elems := strings.Split(pair, ":")
if len(elems) != KeyValuePairSize {
return false, ErrInvalidFieldValue
}
key, valueRaw := elems[0], elems[1]
value, err := getMapValue(field.Type.Elem(), valueRaw)
if err != nil {
return false, err
}
mapValue.SetMapIndex(reflect.ValueOf(key), value)
}
configField.Set(mapValue)
return true, nil
}
// setStructField handles struct field setting.
func setStructField(configField reflect.Value, field FieldInfo, inputValue string) (bool, error) {
valuePtr, err := GetConfigPropFromString(field.Type, inputValue)
if err != nil {
return false, err
}
configField.Set(valuePtr.Elem())
return true, nil
}
// setSliceOrArrayField handles slice or array field setting.
func setSliceOrArrayField(
configField reflect.Value,
field FieldInfo,
inputValue string,
) (bool, error) {
elemType := field.Type.Elem()
elemKind := elemType.Kind()
if elemKind == reflect.Ptr {
elemKind = elemType.Elem().Kind()
}
if elemKind != reflect.Struct && elemKind != reflect.String {
return false, ErrUnsupportedField
}
values := strings.Split(inputValue, string(field.ItemSeparator))
if field.Type.Kind() == reflect.Array && len(values) != field.Type.Len() {
return false, fmt.Errorf("%w: needs to be %d", ErrInvalidFieldCount, field.Type.Len())
}
return setSliceOrArrayValues(configField, field, elemType, values)
}
// setSliceOrArrayValues sets the actual values for slice or array fields.
func setSliceOrArrayValues(
configField reflect.Value,
field FieldInfo,
elemType reflect.Type,
values []string,
) (bool, error) {
isPtrSlice := elemType.Kind() == reflect.Ptr
baseType := elemType
if isPtrSlice {
baseType = elemType.Elem()
}
if baseType.Kind() == reflect.Struct {
slice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, len(values))
for _, v := range values {
propPtr, err := GetConfigPropFromString(baseType, v)
if err != nil {
return false, err
}
if isPtrSlice {
slice = reflect.Append(slice, propPtr)
} else {
slice = reflect.Append(slice, propPtr.Elem())
}
}
configField.Set(slice)
return true, nil
}
// Handle string slice/array
value := reflect.ValueOf(values)
if field.Type.Kind() == reflect.Array {
arr := reflect.Indirect(reflect.New(field.Type))
reflect.Copy(arr, value)
configField.Set(arr)
} else {
configField.Set(value)
}
return true, nil
}
// setEnumField handles enum field setting.
func setEnumField(configField reflect.Value, field FieldInfo, inputValue string) (bool, error) {
value := field.EnumFormatter.Parse(inputValue)
if value == EnumInvalid {
return false, fmt.Errorf(
"%w: accepted values are %v",
ErrInvalidEnumValue,
field.EnumFormatter.Names(),
)
}
configField.SetInt(int64(value))
if actual := int(configField.Int()); actual != value {
return false, fmt.Errorf(
"%w: expected %d, got %d (canSet: %v)",
ErrFailedToSetEnumValue,
value,
actual,
configField.CanSet(),
)
}
return true, nil
}
// getMapValue converts a raw string to a map value based on type.
func getMapValue(valueType reflect.Type, valueRaw string) (reflect.Value, error) {
switch valueType.Kind() {
case reflect.String:
return reflect.ValueOf(valueRaw), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return getMapUintValue(valueRaw, valueType)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return getMapIntValue(valueRaw, valueType)
case reflect.Invalid,
reflect.Bool,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.Array,
reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Pointer,
reflect.Slice,
reflect.Struct,
reflect.UnsafePointer:
return reflect.Value{}, ErrUnsupportedMapValue
default:
return reflect.Value{}, ErrUnsupportedMapValue
}
}
// getMapUintValue converts a string to an unsigned integer map value.
func getMapUintValue(valueRaw string, valueType reflect.Type) (reflect.Value, error) {
number, base := util.StripNumberPrefix(valueRaw)
numValue, err := strconv.ParseUint(number, base, valueType.Bits())
if err != nil {
return reflect.Value{}, fmt.Errorf("%w: %w", ErrParseUintFailed, err)
}
switch valueType.Kind() {
case reflect.Uint:
return reflect.ValueOf(uint(numValue)), nil
case reflect.Uint8:
if numValue > math.MaxUint8 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds uint8 range",
ErrParseUintFailed,
numValue,
)
}
return reflect.ValueOf(uint8(numValue)), nil
case reflect.Uint16:
if numValue > math.MaxUint16 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds uint16 range",
ErrParseUintFailed,
numValue,
)
}
return reflect.ValueOf(uint16(numValue)), nil
case reflect.Uint32:
if numValue > math.MaxUint32 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds uint32 range",
ErrParseUintFailed,
numValue,
)
}
return reflect.ValueOf(uint32(numValue)), nil
case reflect.Uint64:
return reflect.ValueOf(numValue), nil
case reflect.Invalid,
reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.Array,
reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Pointer,
reflect.Slice,
reflect.String,
reflect.Struct,
reflect.UnsafePointer:
return reflect.Value{}, ErrUnexpectedUintKind
default:
return reflect.Value{}, ErrUnexpectedUintKind
}
}
// getMapIntValue converts a string to a signed integer map value.
func getMapIntValue(valueRaw string, valueType reflect.Type) (reflect.Value, error) {
number, base := util.StripNumberPrefix(valueRaw)
numValue, err := strconv.ParseInt(number, base, valueType.Bits())
if err != nil {
return reflect.Value{}, fmt.Errorf("%w: %w", ErrParseIntFailed, err)
}
switch valueType.Kind() {
case reflect.Int:
bits := valueType.Bits()
if bits == Int32BitSize {
if numValue < math.MinInt32 || numValue > math.MaxInt32 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds int%d range",
ErrParseIntFailed,
numValue,
bits,
)
}
}
return reflect.ValueOf(int(numValue)), nil
case reflect.Int8:
if numValue < math.MinInt8 || numValue > math.MaxInt8 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds int8 range",
ErrParseIntFailed,
numValue,
)
}
return reflect.ValueOf(int8(numValue)), nil
case reflect.Int16:
if numValue < math.MinInt16 || numValue > math.MaxInt16 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds int16 range",
ErrParseIntFailed,
numValue,
)
}
return reflect.ValueOf(int16(numValue)), nil
case reflect.Int32:
if numValue < math.MinInt32 || numValue > math.MaxInt32 {
return reflect.Value{}, fmt.Errorf(
"%w: value %d exceeds int32 range",
ErrParseIntFailed,
numValue,
)
}
return reflect.ValueOf(int32(numValue)), nil
case reflect.Int64:
return reflect.ValueOf(numValue), nil
case reflect.Invalid,
reflect.Bool,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.Array,
reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Pointer,
reflect.Slice,
reflect.String,
reflect.Struct,
reflect.UnsafePointer:
return reflect.Value{}, ErrUnexpectedIntKind
default:
return reflect.Value{}, ErrUnexpectedIntKind
}
}
// GetConfigFieldString converts a config field value to its string representation.
func GetConfigFieldString(config reflect.Value, field FieldInfo) (string, error) {
configField := config.FieldByName(field.Name)
if field.IsEnum() {
return field.EnumFormatter.Print(int(configField.Int())), nil
}
strVal, token := getValueNodeValue(configField, &field)
if token == ErrorToken {
return "", ErrInvalidFieldValueData
}
return strVal, nil
}