diff --git a/benchmarks/gluon_bench/benchmark/run.go b/benchmarks/gluon_bench/benchmark/run.go index 290e6795..318096a7 100644 --- a/benchmarks/gluon_bench/benchmark/run.go +++ b/benchmarks/gluon_bench/benchmark/run.go @@ -6,10 +6,10 @@ import ( "fmt" "os" "path/filepath" + "slices" "github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags" "github.com/ProtonMail/gluon/benchmarks/gluon_bench/reporter" - "golang.org/x/exp/slices" ) func RunMain() { diff --git a/connector/dummy.go b/connector/dummy.go index 4a8c1d7c..04ae030a 100644 --- a/connector/dummy.go +++ b/connector/dummy.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "slices" "sync" "sync/atomic" "time" @@ -12,9 +13,8 @@ import ( "github.com/ProtonMail/gluon/constants" "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/internal/ticker" - "github.com/ProtonMail/gluon/internal/utils" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" ) var ( diff --git a/connector/dummy_state.go b/connector/dummy_state.go index fea565de..378d8459 100644 --- a/connector/dummy_state.go +++ b/connector/dummy_state.go @@ -6,9 +6,9 @@ import ( "time" "github.com/ProtonMail/gluon/imap" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/bradenaw/juniper/xslices" "github.com/google/uuid" - "golang.org/x/exp/maps" ) type dummyState struct { @@ -61,7 +61,7 @@ func (state *dummyState) getMailboxes() []imap.Mailbox { state.lock.Lock() defer state.lock.Unlock() - return xslices.Map(maps.Keys(state.mailboxes), func(mboxID imap.MailboxID) imap.Mailbox { + return xslices.Map(utils.Keys(state.mailboxes), func(mboxID imap.MailboxID) imap.Mailbox { return state.toMailbox(mboxID) }) } @@ -132,7 +132,7 @@ func (state *dummyState) getMessages() []imap.Message { state.lock.Lock() defer state.lock.Unlock() - return xslices.Map(maps.Keys(state.messages), func(messageID imap.MessageID) imap.Message { + return xslices.Map(utils.Keys(state.messages), func(messageID imap.MessageID) imap.Message { return state.toMessage(messageID) }) } @@ -149,7 +149,7 @@ func (state *dummyState) getMessageCreatedUpdate(id imap.MessageID) (*imap.Messa return &imap.MessageCreated{ Message: state.toMessage(id), Literal: msg.literal, - MailboxIDs: maps.Keys(msg.mboxIDs), + MailboxIDs: utils.Keys(msg.mboxIDs), ParsedMessage: msg.parsed, }, nil } @@ -170,7 +170,7 @@ func (state *dummyState) getMailboxIDs(messageID imap.MessageID) []imap.MailboxI state.lock.Lock() defer state.lock.Unlock() - return maps.Keys(state.messages[messageID].mboxIDs) + return utils.Keys(state.messages[messageID].mboxIDs) } // nolint: unused diff --git a/go.mod b/go.mod index e7bb1fbf..a15c8581 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 go.uber.org/goleak v1.3.0 - golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 golang.org/x/sys v0.43.0 golang.org/x/text v0.36.0 gopkg.in/yaml.v3 v3.0.1 @@ -31,6 +30,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/sync v0.20.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/imap/flags.go b/imap/flags.go index 29c3d145..24daf469 100644 --- a/imap/flags.go +++ b/imap/flags.go @@ -1,10 +1,10 @@ package imap import ( + "slices" "strings" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" + "github.com/ProtonMail/gluon/pkg/utils" ) const ( @@ -70,7 +70,7 @@ func (fs FlagSet) Len() int { // ToSlice Returns the list of flags in the set as a sorted string slice. The returned list is a hard copy of the internal // slice to avoid direct modifications of the FlagSet value that would break the uniqueness and case insensitivity rules. func (fs FlagSet) ToSlice() []string { - flags := maps.Values(fs) + flags := utils.Values(fs) slices.Sort(flags) @@ -79,7 +79,7 @@ func (fs FlagSet) ToSlice() []string { // ToSliceUnsorted is the same as ToSlice, but does not sort the returned value. func (fs FlagSet) ToSliceUnsorted() []string { - return maps.Values(fs) + return utils.Values(fs) } // Contains returns true if and only if the flag is in the set. @@ -146,11 +146,11 @@ func (fs FlagSet) AddToSelf(flags ...string) { } func (fs FlagSet) AddFlagSet(set FlagSet) FlagSet { - return fs.Add(maps.Values(set)...) + return fs.Add(utils.Values(set)...) } func (fs FlagSet) AddFlagSetToSelf(set FlagSet) { - fs.add(maps.Values(set)...) + fs.add(utils.Values(set)...) } func (fs FlagSet) add(flags ...string) { @@ -191,7 +191,7 @@ func (fs FlagSet) Remove(flags ...string) FlagSet { } func (fs FlagSet) RemoveFlagSet(set FlagSet) FlagSet { - return fs.Remove(maps.Values(set)...) + return fs.Remove(utils.Values(set)...) } func (fs FlagSet) RemoveFromSelf(flags ...string) { @@ -199,7 +199,7 @@ func (fs FlagSet) RemoveFromSelf(flags ...string) { } func (fs FlagSet) RemoveFlagSetFromSelf(set FlagSet) { - fs.Remove(maps.Values(set)...) + fs.Remove(utils.Values(set)...) } func (fs FlagSet) remove(flags ...string) { diff --git a/imap/params.go b/imap/params.go index 4bfca0dc..57959d93 100644 --- a/imap/params.go +++ b/imap/params.go @@ -2,11 +2,11 @@ package imap import ( "net/mail" + "slices" "strconv" "strings" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" + "github.com/ProtonMail/gluon/pkg/utils" ) type parListWriter interface { @@ -114,7 +114,7 @@ func (c *paramList) addNumber(writer parListWriter, v int) *paramList { func (c *paramList) addMap(writer parListWriter, v map[string]string) *paramList { c.onWrite(writer) - keys := maps.Keys(v) + keys := utils.Keys(v) slices.Sort(keys) diff --git a/imap/seqset.go b/imap/seqset.go index 05fe36d8..5727feb2 100644 --- a/imap/seqset.go +++ b/imap/seqset.go @@ -2,11 +2,11 @@ package imap import ( "fmt" + "slices" "strconv" "strings" "github.com/bradenaw/juniper/xslices" - "golang.org/x/exp/slices" ) type SeqVal struct { diff --git a/imap/uid_validity_generator_test.go b/imap/uid_validity_generator_test.go index 3ad26ca6..c8c76020 100644 --- a/imap/uid_validity_generator_test.go +++ b/imap/uid_validity_generator_test.go @@ -1,13 +1,13 @@ package imap import ( + "slices" "testing" "time" "github.com/bradenaw/juniper/parallel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" ) func TestEpochUIDValidityGenerator_Generate(t *testing.T) { diff --git a/internal/backend/connector_updates.go b/internal/backend/connector_updates.go index 4018e9c7..52893acd 100644 --- a/internal/backend/connector_updates.go +++ b/internal/backend/connector_updates.go @@ -7,18 +7,18 @@ import ( "io" "os" "runtime" + "slices" "strings" "github.com/ProtonMail/gluon/db" "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/internal/ids" "github.com/ProtonMail/gluon/internal/state" - "github.com/ProtonMail/gluon/internal/utils" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/rfc822" "github.com/bradenaw/juniper/parallel" "github.com/bradenaw/juniper/xslices" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" ) // apply an incoming update originating from the connector. @@ -344,7 +344,9 @@ func (user *user) applyMessagesCreated(ctx context.Context, update *imap.Message messageForMBox[v] = messageList } - if !slices.ContainsFunc(messageList, func(id db.MessageIDPair) bool { return id.InternalID == internalID }) { + if !slices.ContainsFunc(messageList, func(id db.MessageIDPair) bool { + return id.InternalID == internalID + }) { messageList = append(messageList, db.MessageIDPair{InternalID: internalID, RemoteID: message.Message.ID}) messageForMBox[v] = messageList } diff --git a/internal/backend/user.go b/internal/backend/user.go index 21daa3b8..be80ba10 100644 --- a/internal/backend/user.go +++ b/internal/backend/user.go @@ -16,11 +16,11 @@ import ( "github.com/ProtonMail/gluon/logging" "github.com/ProtonMail/gluon/observability" "github.com/ProtonMail/gluon/observability/metrics" + pkgutils "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/store" "github.com/bradenaw/juniper/xslices" "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" ) type user struct { @@ -298,8 +298,8 @@ func (user *user) removeState(ctx context.Context, st *state.State) error { return nil, fmt.Errorf("no such state") } - messageIDs = utils.Filter(messageIDs, func(messageID imap.InternalMessageID) bool { - return xslices.CountFunc(maps.Values(user.states), func(other *state.State) bool { + messageIDs = pkgutils.Filter(messageIDs, func(messageID imap.InternalMessageID) bool { + return xslices.CountFunc(pkgutils.Values(user.states), func(other *state.State) bool { return st != other && other.HasMessage(messageID) }) == 0 }) @@ -372,7 +372,7 @@ func (user *user) cleanupStaleStoreData(ctx context.Context) error { return err } - idsToDelete := utils.Filter(storeIds, func(id imap.InternalMessageID) bool { + idsToDelete := pkgutils.Filter(storeIds, func(id imap.InternalMessageID) bool { _, ok := dbIdMap[id] return !ok diff --git a/internal/db_impl/sqlite3/migration_test.go b/internal/db_impl/sqlite3/migration_test.go index bc74d6a0..a7065f04 100644 --- a/internal/db_impl/sqlite3/migration_test.go +++ b/internal/db_impl/sqlite3/migration_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "slices" "strings" "testing" "time" @@ -15,7 +16,6 @@ import ( "github.com/bradenaw/juniper/xslices" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" ) func TestMigration_VersionTooHigh(t *testing.T) { diff --git a/internal/response/capability.go b/internal/response/capability.go index b3c30e03..999a17ba 100644 --- a/internal/response/capability.go +++ b/internal/response/capability.go @@ -2,9 +2,9 @@ package response import ( "fmt" + "slices" "github.com/ProtonMail/gluon/imap" - "golang.org/x/exp/slices" ) type capability struct { diff --git a/internal/response/item_capability.go b/internal/response/item_capability.go index 3875696c..4f4bb513 100644 --- a/internal/response/item_capability.go +++ b/internal/response/item_capability.go @@ -2,9 +2,9 @@ package response import ( "fmt" + "slices" "github.com/ProtonMail/gluon/imap" - "golang.org/x/exp/slices" ) type itemCapability struct { diff --git a/internal/response/search.go b/internal/response/search.go index f86ed152..0fd32f39 100644 --- a/internal/response/search.go +++ b/internal/response/search.go @@ -3,7 +3,7 @@ package response import ( "strconv" - "golang.org/x/exp/slices" + "slices" ) type search struct { diff --git a/internal/session/session.go b/internal/session/session.go index 25e7afb3..0c02c647 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -14,6 +14,8 @@ import ( "sync" "time" + "slices" + "github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/events" "github.com/ProtonMail/gluon/imap" @@ -29,7 +31,6 @@ import ( "github.com/ProtonMail/gluon/version" "github.com/emersion/go-imap/utf7" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" ) const maxSessionError = 20 diff --git a/internal/state/actions.go b/internal/state/actions.go index 2ecd9e36..ee16a63c 100644 --- a/internal/state/actions.go +++ b/internal/state/actions.go @@ -4,18 +4,18 @@ import ( "bytes" "context" "fmt" + "slices" "strings" "time" "github.com/ProtonMail/gluon/db" "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/internal/ids" - "github.com/ProtonMail/gluon/internal/utils" "github.com/ProtonMail/gluon/observability" "github.com/ProtonMail/gluon/observability/metrics" + pkgutils "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/rfc822" "github.com/bradenaw/juniper/xslices" - "golang.org/x/exp/slices" ) func (state *State) actionCreateAndGetMailbox(ctx context.Context, tx db.Transaction, name string, uidValidity imap.UID) ([]Update, *db.Mailbox, error) { @@ -250,7 +250,7 @@ func (state *State) actionAddMessagesToMailbox( return nil, nil, err } - if remMessageIDs := utils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { + if remMessageIDs := pkgutils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { return slices.Contains(haveMessageIDs, messageID.InternalID) }); len(remMessageIDs) > 0 { updates, err := state.actionRemoveMessagesFromMailboxUnchecked(ctx, tx, remMessageIDs, mboxID) @@ -300,7 +300,7 @@ func (state *State) actionAddRecoveredMessagesToMailbox( return nil, nil, err } - toAdd := utils.Filter(messageIDs, func(t db.MessageIDPair) bool { + toAdd := pkgutils.Filter(messageIDs, func(t db.MessageIDPair) bool { return !slices.Contains(filter, t.InternalID) }) @@ -510,7 +510,7 @@ func (state *State) actionRemoveMessagesFromMailbox( return nil, err } - messageIDs = utils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { + messageIDs = pkgutils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { return slices.Contains(haveMessageIDs, messageID.InternalID) }) @@ -553,7 +553,7 @@ func (state *State) actionMoveMessages( return nil, nil, err } - if remMessageIDs := utils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { + if remMessageIDs := pkgutils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { return slices.Contains(messageIDsToAdd, messageID.InternalID) }); len(remMessageIDs) > 0 { updates, err := state.actionRemoveMessagesFromMailboxUnchecked(ctx, tx, remMessageIDs, mboxToID) @@ -570,7 +570,7 @@ func (state *State) actionMoveMessages( return nil, nil, err } - messagesIDsToMove := utils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { + messagesIDsToMove := pkgutils.Filter(messageIDs, func(messageID db.MessageIDPair) bool { return slices.Contains(messageInFromMBox, messageID.InternalID) }) diff --git a/internal/state/mailbox_fetch.go b/internal/state/mailbox_fetch.go index cad1bcd4..8a9a7fa5 100644 --- a/internal/state/mailbox_fetch.go +++ b/internal/state/mailbox_fetch.go @@ -14,7 +14,7 @@ import ( "github.com/ProtonMail/gluon/imap/command" "github.com/ProtonMail/gluon/internal/contexts" "github.com/ProtonMail/gluon/internal/response" - "github.com/ProtonMail/gluon/internal/utils" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/rfc822" "github.com/bradenaw/juniper/parallel" "github.com/bradenaw/juniper/xslices" diff --git a/internal/state/mailbox_search.go b/internal/state/mailbox_search.go index 58eb1c66..2fa5dd41 100644 --- a/internal/state/mailbox_search.go +++ b/internal/state/mailbox_search.go @@ -14,7 +14,7 @@ import ( "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/imap/command" "github.com/ProtonMail/gluon/internal/contexts" - "github.com/ProtonMail/gluon/internal/utils" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/rfc5322" "github.com/ProtonMail/gluon/rfc822" "github.com/bradenaw/juniper/parallel" diff --git a/internal/state/paths.go b/internal/state/paths.go index b46615e2..d672fcbf 100644 --- a/internal/state/paths.go +++ b/internal/state/paths.go @@ -1,11 +1,11 @@ package state import ( + "slices" "strings" - "github.com/ProtonMail/gluon/internal/utils" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/bradenaw/juniper/xslices" - "golang.org/x/exp/slices" ) // listSuperiors returns all names superior to the given name, if hierarchies are indicated with the given delimiter. diff --git a/internal/state/snapshot_messages.go b/internal/state/snapshot_messages.go index 556dd1d6..47944895 100644 --- a/internal/state/snapshot_messages.go +++ b/internal/state/snapshot_messages.go @@ -3,10 +3,11 @@ package state import ( "fmt" + "slices" + "github.com/ProtonMail/gluon/db" "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/imap/command" - "golang.org/x/exp/slices" ) var ErrOutOfOrderUIDInsertion = fmt.Errorf("UIDs must be strictly ascending") diff --git a/internal/state/state.go b/internal/state/state.go index e75d81d7..b51b7732 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -13,8 +13,8 @@ import ( "github.com/ProtonMail/gluon/imap" "github.com/ProtonMail/gluon/internal/ids" "github.com/ProtonMail/gluon/internal/response" - "github.com/ProtonMail/gluon/internal/utils" "github.com/ProtonMail/gluon/limits" + "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/rfc822" "github.com/bradenaw/juniper/xmaps" diff --git a/pkg/utils/maps.go b/pkg/utils/maps.go new file mode 100644 index 00000000..6b1afb9d --- /dev/null +++ b/pkg/utils/maps.go @@ -0,0 +1,28 @@ +package utils + +import ( + "maps" + "slices" +) + +// Keys returns a slice of keys from the map. +// Alternative to using maps.Keys which returns an iterator instead of a slice. +func Keys[M ~map[K]V, K comparable, V any](m M) []K { + keys := make([]K, 0, len(m)) + + return slices.AppendSeq( + keys, + maps.Keys(m), + ) +} + +// Values returns a slice of values from the map. +// Alternative to using maps.Values which returns an iterator instead of a slice. +func Values[M ~map[K]V, K comparable, V any](m M) []V { + values := make([]V, 0, len(m)) + + return slices.AppendSeq( + values, + maps.Values(m), + ) +} diff --git a/pkg/utils/maps_test.go b/pkg/utils/maps_test.go new file mode 100644 index 00000000..1a68042c --- /dev/null +++ b/pkg/utils/maps_test.go @@ -0,0 +1,241 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type keysTestCase[K comparable, V any] struct { + name string + expected []K + builder func() map[K]V +} + +type valuesTestCase[K comparable, V any] struct { + name string + expected []V + builder func() map[K]V +} + +func TestMaps_Keys_String(t *testing.T) { + testCases := []keysTestCase[string, int]{ + { + name: "empty", + builder: func() map[string]int { + return make(map[string]int, 0) + }, + expected: []string{}, + }, + { + name: "valid", + builder: func() map[string]int { + return map[string]int{ + "a": 1, + "b": 2, + "c": 3, + } + }, + expected: []string{"a", "b", "c"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Keys(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} + +func TestMaps_Keys_Int(t *testing.T) { + testCases := []keysTestCase[int, int]{ + { + name: "empty", + builder: func() map[int]int { + return make(map[int]int, 0) + }, + expected: []int{}, + }, + { + name: "valid", + builder: func() map[int]int { + return map[int]int{ + 1: 2, + 3: 4, + 5: 6, + } + }, + expected: []int{1, 3, 5}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Keys(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} + +func TestMaps_Keys_Struct(t *testing.T) { + type customStruct struct { + ID string + Name string + } + + testCases := []keysTestCase[customStruct, int]{ + { + name: "empty", + builder: func() map[customStruct]int { + return make(map[customStruct]int, 0) + }, + expected: []customStruct{}, + }, + { + name: "valid", + builder: func() map[customStruct]int { + return map[customStruct]int{ + {ID: "1", Name: "test"}: 1, + } + }, + expected: []customStruct{{ID: "1", Name: "test"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Keys(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} + +func TestMaps_Values_String(t *testing.T) { + testCases := []valuesTestCase[int, string]{ + { + name: "empty", + builder: func() map[int]string { + return make(map[int]string, 0) + }, + expected: []string{}, + }, + { + name: "valid", + builder: func() map[int]string { + return map[int]string{ + 1: "a", + 3: "b", + 5: "c", + } + }, + expected: []string{"a", "b", "c"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Values(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} + +func TestMaps_Values_Int(t *testing.T) { + testCases := []valuesTestCase[int, int]{ + { + name: "empty", + builder: func() map[int]int { + return make(map[int]int, 0) + }, + expected: []int{}, + }, + { + name: "valid", + builder: func() map[int]int { + return map[int]int{ + 1: 2, + 3: 4, + 5: 6, + } + }, + expected: []int{2, 4, 6}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Values(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} + +func TestMaps_Values_Struct(t *testing.T) { + type customStruct struct { + ID string + Name string + } + + testCases := []valuesTestCase[int, customStruct]{ + { + name: "empty", + builder: func() map[int]customStruct { + return make(map[int]customStruct, 0) + }, + expected: []customStruct{}, + }, + { + name: "valid", + builder: func() map[int]customStruct { + return map[int]customStruct{ + 1: {ID: "1", Name: "test"}, + } + }, + expected: []customStruct{{ID: "1", Name: "test"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provided := tc.builder() + + result := Values(provided) + require.Len(t, result, len(tc.expected)) + + for _, v := range result { + require.Contains(t, tc.expected, v) + } + }) + } +} diff --git a/internal/utils/slice_filter.go b/pkg/utils/slice.go similarity index 56% rename from internal/utils/slice_filter.go rename to pkg/utils/slice.go index 92b49bab..b5c1235e 100644 --- a/internal/utils/slice_filter.go +++ b/pkg/utils/slice.go @@ -1,7 +1,9 @@ +// Package utils provide common slice/maps utilities package utils import "slices" +// Filter returns a new slice which is filtered by the provided keep function. func Filter[S ~[]E, E any](s S, keep func(E) bool) S { return slices.DeleteFunc(slices.Clone(s), func(e E) bool { return !keep(e) diff --git a/pkg/utils/slice_test.go b/pkg/utils/slice_test.go new file mode 100644 index 00000000..697ca786 --- /dev/null +++ b/pkg/utils/slice_test.go @@ -0,0 +1,165 @@ +package utils + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type filterTestCase[T any] struct { + name string + input []T + keep func(T) bool + expected []T +} + +func TestSlice_Filter_Int(t *testing.T) { + testCases := []filterTestCase[int]{ + { + name: "empty", + input: []int{}, + keep: func(_ int) bool { + return true + }, + expected: []int{}, + }, + { + name: "all", + input: []int{1, 2, 3}, + keep: func(_ int) bool { + return true + }, + expected: []int{1, 2, 3}, + }, + { + name: "none", + input: []int{1, 2, 3}, + keep: func(_ int) bool { + return false + }, + expected: []int{}, + }, + { + name: "only one", + input: []int{1, 2, 3}, + keep: func(i int) bool { + return i == 2 + }, + expected: []int{2}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := Filter(tc.input, tc.keep) + + require.Equal(t, tc.expected, result) + require.Len(t, result, len(tc.expected)) + }) + } +} + +func TestSlice_Filter_String(t *testing.T) { + testCases := []filterTestCase[string]{ + { + name: "empty", + input: []string{}, + keep: func(_ string) bool { + return true + }, + expected: []string{}, + }, + { + name: "all", + input: []string{"a", "b", "c"}, + keep: func(_ string) bool { + return true + }, + expected: []string{"a", "b", "c"}, + }, + { + name: "none", + input: []string{"a", "b", "c"}, + keep: func(_ string) bool { + return false + }, + expected: []string{}, + }, + { + name: "only one", + input: []string{"a", "b", "c"}, + keep: func(s string) bool { + return s == "b" + }, + expected: []string{"b"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := Filter(tc.input, tc.keep) + + require.Equal(t, tc.expected, result) + require.Len(t, result, len(tc.expected)) + }) + } +} + +func TestSlice_Filter_Struct(t *testing.T) { + type testStruct struct { + ID int + Name string + } + + newRandomTestStruct := func(id int) testStruct { + return testStruct{ + ID: id, + Name: fmt.Sprintf("test-%d", id), + } + } + + testCases := []filterTestCase[testStruct]{ + { + name: "empty", + input: []testStruct{}, + keep: func(_ testStruct) bool { + return true + }, + expected: []testStruct{}, + }, + { + name: "all", + input: []testStruct{newRandomTestStruct(1), newRandomTestStruct(2), newRandomTestStruct(3)}, + keep: func(_ testStruct) bool { + return true + }, + expected: []testStruct{newRandomTestStruct(1), newRandomTestStruct(2), newRandomTestStruct(3)}, + }, + { + name: "none", + input: []testStruct{newRandomTestStruct(1), newRandomTestStruct(2), newRandomTestStruct(3)}, + keep: func(_ testStruct) bool { + return false + }, + expected: []testStruct{}, + }, + { + name: "specific id", + input: []testStruct{newRandomTestStruct(1), newRandomTestStruct(2), newRandomTestStruct(3)}, + keep: func(ts testStruct) bool { + return ts.ID == 2 + }, + expected: []testStruct{newRandomTestStruct(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := Filter(tc.input, tc.keep) + + require.Equal(t, tc.expected, result) + require.Len(t, result, len(tc.expected)) + }) + } +} diff --git a/rfc822/hash.go b/rfc822/hash.go index 668ca02d..2a4a11ba 100644 --- a/rfc822/hash.go +++ b/rfc822/hash.go @@ -6,12 +6,12 @@ import ( "encoding/base64" "io" "mime/quotedprintable" + "slices" "strings" + pkgutils "github.com/ProtonMail/gluon/pkg/utils" "github.com/ProtonMail/gluon/rfc5322" "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" ) // GetMessageHash returns the hash of the given message. @@ -77,7 +77,7 @@ func GetMessageHash(b []byte) (string, error) { return err } - keys := maps.Keys(values) + keys := pkgutils.Keys(values) slices.Sort(keys) for _, k := range keys { diff --git a/tests/connection_test.go b/tests/connection_test.go index a74c1a15..9d379a81 100644 --- a/tests/connection_test.go +++ b/tests/connection_test.go @@ -8,6 +8,7 @@ import ( "io" "net" "regexp" + "slices" "strconv" "strings" "testing" @@ -16,7 +17,6 @@ import ( "github.com/bradenaw/juniper/xslices" "github.com/google/uuid" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" ) func withTag(fn func(string)) { diff --git a/tests/server_test.go b/tests/server_test.go index dd37ecfa..52bf9a3f 100644 --- a/tests/server_test.go +++ b/tests/server_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "path/filepath" + "slices" "testing" "time" @@ -25,7 +26,6 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" ) const defaultPeriod = time.Second diff --git a/tests/store_test.go b/tests/store_test.go index fbabdb51..8bc31ffc 100644 --- a/tests/store_test.go +++ b/tests/store_test.go @@ -2,7 +2,7 @@ package tests import ( "fmt" - "golang.org/x/exp/slices" + "slices" "strings" "testing"