Skip to content

Commit d262a5e

Browse files
authored
Merge pull request #127 from Icinga/import-utils-from-icinga-notifications
Import Icinga Notifications `internal/utils`
2 parents d004acd + 41e23b6 commit d262a5e

File tree

7 files changed

+202
-11
lines changed

7 files changed

+202
-11
lines changed

database/utils.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"github.com/icinga/icinga-go-library/types"
1111
"github.com/jmoiron/sqlx"
1212
"github.com/pkg/errors"
13+
"slices"
14+
"strings"
1315
)
1416

1517
// CantPerformQuery wraps the given error with the specified query that cannot be executed.
@@ -81,6 +83,19 @@ func InsertObtainID(ctx context.Context, conn TxOrDB, stmt string, arg any) (int
8183
return resultID, nil
8284
}
8385

86+
// BuildInsertStmtWithout builds an insert stmt without the provided columns.
87+
func BuildInsertStmtWithout(db *DB, into interface{}, withoutColumns ...string) string {
88+
columns := slices.DeleteFunc(
89+
db.BuildColumns(into),
90+
func(column string) bool { return slices.Contains(withoutColumns, column) })
91+
92+
return fmt.Sprintf(
93+
`INSERT INTO "%s" ("%s") VALUES (%s)`,
94+
TableName(into), strings.Join(columns, `", "`),
95+
fmt.Sprintf(":%s", strings.Join(columns, ", :")),
96+
)
97+
}
98+
8499
// unsafeSetSessionVariableIfExists sets the given MySQL/MariaDB system variable for the specified database session.
85100
//
86101
// NOTE: It is unsafe to use this function with untrusted/user supplied inputs and poses an SQL injection,

types/int.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ type Int struct {
1414
sql.NullInt64
1515
}
1616

17+
// TransformZeroIntToNull transforms a valid Int carrying a zero value to a SQL NULL.
18+
func TransformZeroIntToNull(i *Int) {
19+
if i.Valid && i.Int64 == 0 {
20+
i.Valid = false
21+
}
22+
}
23+
24+
// MakeInt constructs a new Int.
25+
//
26+
// Multiple transformer functions can be given, each transforming the generated Int, e.g., TransformZeroIntToNull.
27+
func MakeInt(in int64, transformers ...func(*Int)) Int {
28+
i := Int{sql.NullInt64{
29+
Int64: in,
30+
Valid: true,
31+
}}
32+
33+
for _, transformer := range transformers {
34+
transformer(&i)
35+
}
36+
37+
return i
38+
}
39+
1740
// MarshalJSON implements the json.Marshaler interface.
1841
// Supports JSON null.
1942
func (i Int) MarshalJSON() ([]byte, error) {

types/int_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,49 @@ import (
66
"testing"
77
)
88

9+
func TestMakeInt(t *testing.T) {
10+
subtests := []struct {
11+
name string
12+
input int64
13+
transformers []func(*Int)
14+
output sql.NullInt64
15+
}{
16+
{
17+
name: "zero",
18+
input: 0,
19+
output: sql.NullInt64{Int64: 0, Valid: true},
20+
},
21+
{
22+
name: "positive",
23+
input: 1,
24+
output: sql.NullInt64{Int64: 1, Valid: true},
25+
},
26+
{
27+
name: "negative",
28+
input: -1,
29+
output: sql.NullInt64{Int64: -1, Valid: true},
30+
},
31+
{
32+
name: "zero-transform-zero-to-null",
33+
input: 0,
34+
transformers: []func(*Int){TransformZeroIntToNull},
35+
output: sql.NullInt64{Valid: false},
36+
},
37+
{
38+
name: "positive-transform-zero-to-null",
39+
input: 1,
40+
transformers: []func(*Int){TransformZeroIntToNull},
41+
output: sql.NullInt64{Int64: 1, Valid: true},
42+
},
43+
}
44+
45+
for _, st := range subtests {
46+
t.Run(st.name, func(t *testing.T) {
47+
require.Equal(t, Int{NullInt64: st.output}, MakeInt(st.input, st.transformers...))
48+
})
49+
}
50+
}
51+
952
func TestInt_MarshalJSON(t *testing.T) {
1053
subtests := []struct {
1154
name string

types/string.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,27 @@ type String struct {
1414
sql.NullString
1515
}
1616

17-
// MakeString constructs a new non-NULL String from s.
18-
func MakeString(s string) String {
19-
return String{sql.NullString{
20-
String: s,
17+
// TransformEmptyStringToNull transforms a valid String carrying an empty text to a SQL NULL.
18+
func TransformEmptyStringToNull(s *String) {
19+
if s.Valid && s.String == "" {
20+
s.Valid = false
21+
}
22+
}
23+
24+
// MakeString constructs a new String.
25+
//
26+
// Multiple transformer functions can be given, each transforming the generated String, e.g., TransformEmptyStringToNull.
27+
func MakeString(in string, transformers ...func(*String)) String {
28+
s := String{sql.NullString{
29+
String: in,
2130
Valid: true,
2231
}}
32+
33+
for _, transformer := range transformers {
34+
transformer(&s)
35+
}
36+
37+
return s
2338
}
2439

2540
// MarshalJSON implements the json.Marshaler interface.

types/string_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,48 @@ import (
99

1010
func TestMakeString(t *testing.T) {
1111
subtests := []struct {
12-
name string
13-
io string
12+
name string
13+
input string
14+
transformers []func(*String)
15+
output sql.NullString
1416
}{
15-
{"empty", ""},
16-
{"nul", "\x00"},
17-
{"space", " "},
18-
{"multiple", "abc"},
17+
{
18+
name: "empty",
19+
input: "",
20+
output: sql.NullString{String: "", Valid: true},
21+
},
22+
{
23+
name: "nul",
24+
input: "\x00",
25+
output: sql.NullString{String: "\x00", Valid: true},
26+
},
27+
{
28+
name: "space",
29+
input: " ",
30+
output: sql.NullString{String: " ", Valid: true},
31+
},
32+
{
33+
name: "valid-text",
34+
input: "abc",
35+
output: sql.NullString{String: "abc", Valid: true},
36+
},
37+
{
38+
name: "empty-transform-empty-to-null",
39+
input: "",
40+
transformers: []func(*String){TransformEmptyStringToNull},
41+
output: sql.NullString{Valid: false},
42+
},
43+
{
44+
name: "valid-text-transform-empty-to-null",
45+
input: "abc",
46+
transformers: []func(*String){TransformEmptyStringToNull},
47+
output: sql.NullString{String: "abc", Valid: true},
48+
},
1949
}
2050

2151
for _, st := range subtests {
2252
t.Run(st.name, func(t *testing.T) {
23-
require.Equal(t, String{NullString: sql.NullString{String: st.io, Valid: true}}, MakeString(st.io))
53+
require.Equal(t, String{NullString: st.output}, MakeString(st.input, st.transformers...))
2454
})
2555
}
2656
}

utils/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package utils
22

33
import (
4+
"cmp"
45
"context"
56
"crypto/sha1" // #nosec G505 -- Blocklisted import crypto/sha1
67
"fmt"
78
"github.com/go-sql-driver/mysql"
89
"github.com/lib/pq"
910
"github.com/pkg/errors"
1011
"golang.org/x/exp/utf8string"
12+
"iter"
1113
"net"
1214
"os"
1315
"path/filepath"
16+
"slices"
1417
"strings"
1518
"time"
1619
)
@@ -163,3 +166,23 @@ func PrintErrorThenExit(err error, exitCode int) {
163166
fmt.Fprintln(os.Stderr, err)
164167
os.Exit(exitCode)
165168
}
169+
170+
// IterateOrderedMap implements iter.Seq2 to iterate over a map in the key's order.
171+
//
172+
// This function returns a func yielding key-value-pairs from a given map in the order of their keys, if their type
173+
// is cmp.Ordered.
174+
func IterateOrderedMap[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
175+
keys := make([]K, 0, len(m))
176+
for key := range m {
177+
keys = append(keys, key)
178+
}
179+
slices.Sort(keys)
180+
181+
return func(yield func(K, V) bool) {
182+
for _, key := range keys {
183+
if !yield(key, m[key]) {
184+
return
185+
}
186+
}
187+
}
188+
}

utils/utils_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"github.com/stretchr/testify/assert"
45
"github.com/stretchr/testify/require"
56
"testing"
67
)
@@ -52,3 +53,44 @@ func requireClosedEmpty(t *testing.T, ch <-chan int) {
5253
require.Fail(t, "receiving should not block")
5354
}
5455
}
56+
57+
func TestIterateOrderedMap(t *testing.T) {
58+
tests := []struct {
59+
name string
60+
in map[int]string
61+
outKeys []int
62+
}{
63+
{"empty", map[int]string{}, nil},
64+
{"single", map[int]string{1: "foo"}, []int{1}},
65+
{"few-numbers", map[int]string{1: "a", 2: "b", 3: "c"}, []int{1, 2, 3}},
66+
{
67+
"1k-numbers",
68+
func() map[int]string {
69+
m := make(map[int]string)
70+
for i := 0; i < 1000; i++ {
71+
m[i] = "foo"
72+
}
73+
return m
74+
}(),
75+
func() []int {
76+
keys := make([]int, 1000)
77+
for i := 0; i < 1000; i++ {
78+
keys[i] = i
79+
}
80+
return keys
81+
}(),
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
var outKeys []int
88+
for k, v := range IterateOrderedMap(tt.in) {
89+
assert.Equal(t, tt.in[k], v)
90+
outKeys = append(outKeys, k)
91+
}
92+
93+
assert.Equal(t, tt.outKeys, outKeys)
94+
})
95+
}
96+
}

0 commit comments

Comments
 (0)