diff --git a/pkg/format/format_test.go b/pkg/format/format_test.go index b0e53b26..dff3908c 100644 --- a/pkg/format/format_test.go +++ b/pkg/format/format_test.go @@ -3,6 +3,7 @@ package format import ( "errors" "github.com/containrrr/shoutrrr/pkg/types" + "github.com/fatih/color" "reflect" "testing" @@ -30,6 +31,9 @@ func TestFormat(t *testing.T) { var _ = Describe("the format package", func() { BeforeSuite(func() { // logger = log.New(GinkgoWriter, "Test", log.LstdFlags) + + // Disable color output for tests to have them match the string format rather than the colors + color.NoColor = true }) Describe("SetConfigField", func() { @@ -215,7 +219,7 @@ var _ = Describe("the format package", func() { testSetAndFormat(tv, fieldMap["SubPropPtrSlice"], "@diet,@glue", "[ @diet, @glue ]") }) It("should format prop struct slices identical to input", func() { - testSetAndFormat(tv, fieldMap["StrMap"], "one:1,two:2", "{ one: 1, two: 2 }") + testSetAndFormat(tv, fieldMap["StrMap"], "a:1,b:2,c:3", "{ a: 1, b: 2, c: 3 }") }) }) }) diff --git a/pkg/format/formatter.go b/pkg/format/formatter.go index 7e7170a8..8adecbaa 100644 --- a/pkg/format/formatter.go +++ b/pkg/format/formatter.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strconv" "strings" "unsafe" @@ -127,11 +128,11 @@ func (fmtr *formatter) getStructFieldValueString(fieldVal reflect.Value, field F if kind == reflect.Int { valueStr := field.EnumFormatter.Print(int(fieldVal.Int())) return ColorizeEnum(valueStr), len(valueStr) - } else { - err := fmt.Errorf("incorrect enum type '%s' for field '%s'", kind, field.Name) - fmtr.Errors = append(fmtr.Errors, err) - return "", 0 } + err := fmt.Errorf("incorrect enum type '%s' for field '%s'", kind, field.Name) + fmtr.Errors = append(fmtr.Errors, err) + return "", 0 + } else if nextDepth >= fmtr.MaxDepth { return } @@ -274,19 +275,34 @@ func (fmtr *formatter) getFieldValueString(field reflect.Value, base int, depth } if kind == reflect.Map { - sb := strings.Builder{} - sb.WriteString("{ ") + iter := field.MapRange() // initial value for totalLen is surrounding curlies and spaces, and separating commas totalLen := 4 + (field.Len() - 1) + + keys := make([]string, field.Len()) + keyFmtMap := make(map[string]string, field.Len()) + for i := 0; iter.Next(); i++ { - key, keyLen := fmtr.getFieldValueString(iter.Key(), base, nextDepth) + key := iter.Key().String() + keyFmt, keyLen := fmtr.getFieldValueString(iter.Key(), base, nextDepth) value, valueLen := fmtr.getFieldValueString(iter.Value(), base, nextDepth) + + keys[i] = key + keyFmtMap[key] = fmt.Sprintf("%s: %s", keyFmt, value) + totalLen += keyLen + valueLen + 2 + } + + sort.Strings(keys) + + sb := strings.Builder{} + sb.WriteString("{ ") + for _, key := range keys { + if sb.Len() > 2 { sb.WriteString(", ") } - sb.WriteString(fmt.Sprintf("%s: %s", key, value)) - totalLen += keyLen + valueLen + 2 + sb.WriteString(keyFmtMap[key]) } sb.WriteString(" }") @@ -434,6 +450,7 @@ func SetConfigField(config reflect.Value, field FieldInfo, inputValue string) (v } +// GetConfigPropFromString deserializes a config property from a string representation using the ConfigProp interface func GetConfigPropFromString(structType reflect.Type, value string) (reflect.Value, error) { valuePtr := reflect.New(structType) configProp, ok := valuePtr.Interface().(types.ConfigProp) @@ -448,6 +465,7 @@ func GetConfigPropFromString(structType reflect.Type, value string) (reflect.Val return valuePtr, nil } +// GetConfigPropString serializes a config property to a string representation using the ConfigProp interface func GetConfigPropString(propPtr reflect.Value) (string, error) { if propPtr.Kind() != reflect.Ptr { @@ -497,9 +515,10 @@ func GetConfigFieldString(config reflect.Value, field FieldInfo) (value string, kvPairs := []string{} for _, key := range configField.MapKeys() { value := configField.MapIndex(key).Interface() - kvPairs = append(kvPairs, fmt.Sprintf("%s:%s", key, value)) } + // Map key/value-pairs are sorted after concat as it should be identical to sorting the keys before + sort.Strings(kvPairs) return strings.Join(kvPairs, ","), nil } else if fieldKind == reflect.Slice || fieldKind == reflect.Array { sliceLen := configField.Len() diff --git a/pkg/types/config_prop.go b/pkg/types/config_prop.go index c2720ba5..da1f8671 100644 --- a/pkg/types/config_prop.go +++ b/pkg/types/config_prop.go @@ -1,5 +1,6 @@ package types +// ConfigProp interface is used to de-/serialize structs from/to a string representation type ConfigProp interface { SetFromProp(propValue string) error GetPropValue() (string, error)