diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 9adc0f53..40c27578 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "cmp" "context" "database/sql" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/icinga/icingadb/pkg/utils" "github.com/jmoiron/sqlx" "github.com/pkg/errors" + "slices" "strings" ) @@ -157,3 +159,27 @@ func RemoveNils[T any](slice []*T) []*T { return ptr == nil }) } + +// IterateOrderedMap implements iter.Seq2 to iterate over a map in the key's order. +// +// This function returns a func yielding key-value-pairs from a given map in the order of their keys, if their type +// is cmp.Ordered. +// +// Please note that currently - being at Go 1.22 - rangefuncs are still an experimental feature and cannot be directly +// used unless compiled with `GOEXPERIMENT=rangefunc`. However, they can still be invoked normally. +// https://go.dev/wiki/RangefuncExperiment +func IterateOrderedMap[K cmp.Ordered, V any](m map[K]V) func(func(K, V) bool) { + keys := make([]K, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + slices.Sort(keys) + + return func(yield func(K, V) bool) { + for _, key := range keys { + if !yield(key, m[key]) { + return + } + } + } +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 15108999..a668d8d2 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -26,3 +26,52 @@ func TestRemoveNils(t *testing.T) { }) } } + +func TestIterateOrderedMap(t *testing.T) { + tests := []struct { + name string + in map[int]string + outKeys []int + }{ + {"empty", map[int]string{}, nil}, + {"single", map[int]string{1: "foo"}, []int{1}}, + {"few-numbers", map[int]string{1: "a", 2: "b", 3: "c"}, []int{1, 2, 3}}, + { + "1k-numbers", + func() map[int]string { + m := make(map[int]string) + for i := 0; i < 1000; i++ { + m[i] = "foo" + } + return m + }(), + func() []int { + keys := make([]int, 1000) + for i := 0; i < 1000; i++ { + keys[i] = i + } + return keys + }(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outKeys []int + + // Either run with GOEXPERIMENT=rangefunc or wait for rangefuncs to land in the next Go release. + // for k, _ := range IterateOrderedMap(tt.in) { + // outKeys = append(outKeys, k) + // } + + // In the meantime, it can be invoked as follows. + IterateOrderedMap(tt.in)(func(k int, v string) bool { + assert.Equal(t, tt.in[k], v) + outKeys = append(outKeys, k) + return true + }) + + assert.Equal(t, tt.outKeys, outKeys) + }) + } +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 36698eb1..b2cb7ab0 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/icinga/icinga-notifications/internal/utils" "github.com/icinga/icinga-notifications/pkg/rpc" "github.com/icinga/icingadb/pkg/types" "io" @@ -201,15 +202,17 @@ func FormatMessage(writer io.Writer, req *NotificationRequest) { } _, _ = fmt.Fprintf(writer, "Object: %s\n\n", req.Object.Url) _, _ = writer.Write([]byte("Tags:\n")) - for k, v := range req.Object.Tags { + utils.IterateOrderedMap(req.Object.Tags)(func(k, v string) bool { _, _ = fmt.Fprintf(writer, "%s: %s\n", k, v) - } + return true + }) if len(req.Object.ExtraTags) > 0 { _, _ = writer.Write([]byte("\nExtra Tags:\n")) - for k, v := range req.Object.ExtraTags { + utils.IterateOrderedMap(req.Object.ExtraTags)(func(k, v string) bool { _, _ = fmt.Fprintf(writer, "%s: %s\n", k, v) - } + return true + }) } _, _ = fmt.Fprintf(writer, "\nIncident: %s", req.Incident.Url)