Skip to content

Commit c069c80

Browse files
committed
feat: additional of some helpers
1 parent 83d2786 commit c069c80

File tree

11 files changed

+379
-4
lines changed

11 files changed

+379
-4
lines changed

.golangci.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ linters-settings:
4141
main:
4242
files:
4343
- $all
44+
- "!**/uuid_test.go"
4445
deny:
4546
- pkg: "github.com/gofrs/uuid"
46-
desc: 'use github.com/google/uuid instead'
47+
desc: 'use github.com/google/uuid instead'
48+
- pkg: "golang.org/x/exp/slices"
49+
desc: 'use "slices" instead'

config/load.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import (
44
"fmt"
55
"os"
66
"reflect"
7+
"slices"
78
"strings"
89
"time"
910

1011
"github.com/fsnotify/fsnotify"
1112
"github.com/joho/godotenv"
1213
"github.com/spf13/viper"
13-
"golang.org/x/exp/slices"
1414
)
1515

1616
func (c *Config) load() {

config/valueloader.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package config
2+
3+
// SingleValueLoader returns a ValueLoader that always returns the same value.
4+
func SingleValueLoader[T any](v T) ValueLoader[T] {
5+
return &loader[T]{v}
6+
}
7+
8+
// ValueLoader is an interface that can be used to load a value.
9+
type ValueLoader[T any] interface {
10+
Load() T
11+
}
12+
13+
// loader is a ValueLoader that always returns the same value.
14+
type loader[T any] struct {
15+
v T
16+
}
17+
18+
// Load returns the value.
19+
func (l *loader[T]) Load() T {
20+
return l.v
21+
}

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/fsnotify/fsnotify v1.7.0
1212
github.com/go-chi/chi/v5 v5.0.12
1313
github.com/go-redis/redis/v8 v8.11.5
14+
github.com/gofrs/uuid v4.4.0+incompatible
1415
github.com/golang/mock v1.6.0
1516
github.com/google/uuid v1.6.0
1617
github.com/joho/godotenv v1.5.1
@@ -47,9 +48,9 @@ require (
4748
go.uber.org/goleak v1.3.0
4849
go.uber.org/zap v1.27.0
4950
golang.org/x/crypto v0.21.0
50-
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
5151
golang.org/x/oauth2 v0.18.0
5252
golang.org/x/sync v0.6.0
53+
golang.org/x/text v0.14.0
5354
google.golang.org/api v0.172.0
5455
google.golang.org/protobuf v1.33.0
5556
gopkg.in/alexcesaro/statsd.v2 v2.0.0
@@ -142,10 +143,10 @@ require (
142143
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
143144
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
144145
go.uber.org/multierr v1.10.0 // indirect
146+
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
145147
golang.org/x/mod v0.14.0 // indirect
146148
golang.org/x/net v0.22.0 // indirect
147149
golang.org/x/sys v0.18.0 // indirect
148-
golang.org/x/text v0.14.0 // indirect
149150
golang.org/x/time v0.5.0 // indirect
150151
golang.org/x/tools v0.17.0 // indirect
151152
google.golang.org/appengine v1.6.8 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC
107107
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
108108
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
109109
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
110+
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
111+
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
110112
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
111113
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
112114
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

sanitize/sanitize.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package sanitize
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
7+
"golang.org/x/text/unicode/rangetable"
8+
)
9+
10+
// invisibleRunes unicode.IsPrint does not include all invisible characters,
11+
// so I got this list from https://invisible-characters.com/
12+
var invisibleRunes = []rune{
13+
'\u0000', // NULL
14+
'\u0009', // CHARACTER TABULATION
15+
'\u00A0', // NO-BREAK SPACE
16+
'\u00AD', // SOFT HYPHEN
17+
'\u034F', // COMBINING GRAPHEME JOINER
18+
'\u061C', // ARABIC LETTER MARK
19+
'\u115F', // HANGUL CHOSEONG FILLER
20+
'\u1160', // HANGUL JUNGSEONG FILLER
21+
'\u17B4', // KHMER VOWEL INHERENT AQ
22+
'\u17B5', // KHMER VOWEL INHERENT AA
23+
'\u180E', // MONGOLIAN VOWEL SEPARATOR
24+
'\u2000', // EN QUAD
25+
'\u2001', // EM QUAD
26+
'\u2002', // EN SPACE
27+
'\u2003', // EM SPACE
28+
'\u2004', // THREE-PER-EM SPACE
29+
'\u2005', // FOUR-PER-EM SPACE
30+
'\u2006', // SIX-PER-EM SPACE
31+
'\u2007', // FIGURE SPACE
32+
'\u2008', // PUNCTUATION SPACE
33+
'\u2009', // THIN SPACE
34+
'\u200A', // HAIR SPACE
35+
'\u200B', // ZERO WIDTH SPACE
36+
'\u200C', // ZERO WIDTH NON-JOINER
37+
'\u200D', // ZERO WIDTH JOINER
38+
'\u200E', // LEFT-TO-RIGHT MARK
39+
'\u200F', // RIGHT-TO-LEFT MARK
40+
'\u202F', // NARROW NO-BREAK SPACE
41+
'\u205F', // MEDIUM MATHEMATICAL SPACE
42+
'\u2060', // WORD JOINER
43+
'\u2061', // FUNCTION APPLICATION
44+
'\u2062', // INVISIBLE TIMES
45+
'\u2063', // INVISIBLE SEPARATOR
46+
'\u2064', // INVISIBLE PLUS
47+
'\u206A', // INHIBIT SYMMETRIC SWAPPING
48+
'\u206B', // ACTIVATE SYMMETRIC SWAPPING
49+
'\u206C', // INHIBIT ARABIC FORM SHAPING
50+
'\u206D', // ACTIVATE ARABIC FORM SHAPING
51+
'\u206E', // NATIONAL DIGIT SHAPES
52+
'\u206F', // NOMINAL DIGIT SHAPES
53+
'\u3000', // IDEOGRAPHIC SPACE
54+
'\u2800', // BRAILLE PATTERN BLANK
55+
'\u3164', // HANGUL FILLER
56+
'\uFEFF', // ZERO WIDTH NO-BREAK SPACE
57+
'\uFFA0', // HALFWIDTH HANGUL FILLER
58+
}
59+
60+
var invisibleRangeTable *unicode.RangeTable
61+
62+
func init() {
63+
invisibleRangeTable = rangetable.New(invisibleRunes...)
64+
}
65+
66+
// Unicode removes irregularly invisible characters from a string.
67+
//
68+
// Irregularly invisible characters are defined as:
69+
// - Non-printable characters according to Go's unicode package (unicode.IsPrint).
70+
// - Characters in the invisibleRunes list (https://invisible-characters.com/).
71+
//
72+
// Note: Regular ASCII space (0x20) is not removed.
73+
func Unicode(str string) string {
74+
return strings.Map(func(r rune) rune {
75+
if unicode.Is(invisibleRangeTable, r) || !unicode.IsPrint(r) {
76+
return -1
77+
}
78+
return r
79+
}, str)
80+
}

sanitize/sanitize_test.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package sanitize
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"unicode"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
var out string
12+
13+
func BenchmarkMessageID(b *testing.B) {
14+
dirtyMessageID := "\u0000 Test foo_bar-baz \u034F 123-222 "
15+
properMessageID := "123e4567-e89b-12d3-a456-426614174000"
16+
17+
b.Run("in-place for loop - dirty", func(b *testing.B) {
18+
for i := 0; i < b.N; i++ {
19+
out = sanitizeMessageIDForLoop(dirtyMessageID)
20+
}
21+
})
22+
23+
b.Run("in-place for loop - proper", func(b *testing.B) {
24+
for i := 0; i < b.N; i++ {
25+
out = sanitizeMessageIDForLoop(properMessageID)
26+
}
27+
})
28+
29+
b.Run("strings map - dirty", func(b *testing.B) {
30+
for i := 0; i < b.N; i++ {
31+
out = Unicode(dirtyMessageID)
32+
}
33+
})
34+
35+
b.Run("strings map - proper", func(b *testing.B) {
36+
for i := 0; i < b.N; i++ {
37+
out = Unicode(properMessageID)
38+
}
39+
})
40+
}
41+
42+
// incorrect implementation of sanitizeMessageID, but used for benchmarking
43+
func sanitizeMessageIDForLoop(messageID string) string {
44+
for i, r := range messageID {
45+
if unicode.IsPrint(r) {
46+
continue
47+
}
48+
if !unicode.Is(invisibleRangeTable, r) {
49+
continue
50+
}
51+
52+
messageID = messageID[:i] + messageID[i+1:]
53+
}
54+
return messageID
55+
}
56+
57+
func TestSanitizeMessageID(t *testing.T) {
58+
testcases := []struct {
59+
in string
60+
out string
61+
}{
62+
{"\u0000 Test \u0000foo_bar-baz 123-222 \u0000", " Test foo_bar-baz 123-222 "},
63+
{"\u0000", ""},
64+
{"\u0000 ", " "},
65+
{"\u0000 \u0000", " "},
66+
{"\u00A0\t\n\r\u034F", ""},
67+
{"τυχαίο;", "τυχαίο;"},
68+
}
69+
70+
for _, tc := range testcases {
71+
cleanMessageID := Unicode(tc.in)
72+
require.Equal(t, tc.out, cleanMessageID, fmt.Sprintf("%#v -> %#v", tc.in, tc.out))
73+
}
74+
75+
for _, r := range invisibleRunes {
76+
cleanMessageID := Unicode(string(r))
77+
require.Empty(t, cleanMessageID, fmt.Sprintf("%U", r))
78+
79+
}
80+
}

stringify/stringify.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package stringify
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
func Data(data any) string {
9+
if data == nil {
10+
return ""
11+
}
12+
switch d := data.(type) {
13+
case string:
14+
return d
15+
default:
16+
dataBytes, err := json.Marshal(d)
17+
if err != nil {
18+
return fmt.Sprint(d)
19+
}
20+
return string(dataBytes)
21+
}
22+
}

stringify/stringify_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package stringify_test
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/rudderlabs/rudder-go-kit/stringify"
10+
)
11+
12+
type failOnJSONMarshal struct{}
13+
14+
func (f failOnJSONMarshal) MarshalJSON() ([]byte, error) {
15+
return nil, errors.New("failed to marshal")
16+
}
17+
18+
func TestStringyData(t *testing.T) {
19+
testCases := []struct {
20+
name string
21+
input any
22+
expected string
23+
}{
24+
{
25+
name: "Nil input",
26+
input: nil,
27+
expected: "",
28+
},
29+
{
30+
name: "String input",
31+
input: "test string",
32+
expected: "test string",
33+
},
34+
{
35+
name: "Struct input",
36+
input: struct {
37+
Name string `json:"name"`
38+
Age int `json:"age"`
39+
}{Name: "John", Age: 30},
40+
expected: `{"name":"John","age":30}`,
41+
},
42+
{
43+
name: "Slice input",
44+
input: []string{
45+
"apple", "banana", "cherry",
46+
},
47+
expected: `["apple","banana","cherry"]`,
48+
},
49+
{
50+
name: "Fail on JSON marshal",
51+
input: failOnJSONMarshal{},
52+
expected: "{}",
53+
},
54+
}
55+
56+
for _, tc := range testCases {
57+
t.Run(tc.name, func(t *testing.T) {
58+
result := stringify.Data(tc.input)
59+
require.Equal(t, tc.expected, result)
60+
})
61+
}
62+
}

uuid/uuid.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package uuid
2+
3+
import (
4+
"crypto/md5"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// GetMD5UUID hashes the given string into md5 and returns it as uuid
10+
func GetMD5UUID(str string) (uuid.UUID, error) {
11+
// To maintain backward compatibility, we are using md5 hash of the string
12+
// We are mimicking github.com/gofrs/uuid behavior:
13+
//
14+
// md5Sum := md5.Sum([]byte(str))
15+
// u, err := uuid.FromBytes(md5Sum[:])
16+
17+
// u.SetVersion(uuid.V4)
18+
// u.SetVariant(uuid.VariantRFC4122)
19+
20+
// google/uuid doesn't allow us to modify the version and variant,
21+
// so we are doing it manually, using gofrs/uuid library implementation.
22+
md5Sum := md5.Sum([]byte(str)) // skipcq: GO-S1023
23+
// SetVariant: VariantRFC4122
24+
md5Sum[8] = md5Sum[8]&(0xff>>2) | (0x02 << 6)
25+
// SetVersion: Version 4
26+
version := byte(4)
27+
md5Sum[6] = (md5Sum[6] & 0x0f) | (version << 4)
28+
29+
return uuid.FromBytes(md5Sum[:])
30+
}

0 commit comments

Comments
 (0)