From 6f04321a60257ff8bb7f34dbeb0adc1f90bcb7bf Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 30 Aug 2022 15:29:24 -0600 Subject: [PATCH] replace scope values with filters (#674) The filters package allows callers to specify both a target to match on, and behavior of the comparison. While data- type scopes always equate to "equals", the control over different comparison behavior is useful for info-type filters. This change integrates filters into scopes for built-in control of those comparisons. --- src/cli/backup/exchange_test.go | 10 +- src/cli/restore/exchange_test.go | 6 +- .../connector/exchange/service_iterators.go | 2 +- src/internal/path/path.go | 1 + src/pkg/filters/filters.go | 94 +++++++- src/pkg/filters/filters_test.go | 43 ++++ src/pkg/selectors/exchange.go | 193 +++++++-------- src/pkg/selectors/exchange_test.go | 225 +++++++++++++----- src/pkg/selectors/helpers_test.go | 34 ++- src/pkg/selectors/onedrive.go | 19 +- src/pkg/selectors/onedrive_test.go | 26 +- src/pkg/selectors/scopes.go | 103 ++++---- src/pkg/selectors/scopes_test.go | 189 +++++++++++++-- src/pkg/selectors/selectors.go | 100 ++++++-- src/pkg/selectors/selectors_test.go | 20 +- 15 files changed, 743 insertions(+), 322 deletions(-) diff --git a/src/cli/backup/exchange_test.go b/src/cli/backup/exchange_test.go index 6e1000da4..be3a9b0d9 100644 --- a/src/cli/backup/exchange_test.go +++ b/src/cli/backup/exchange_test.go @@ -199,13 +199,13 @@ func (suite *ExchangeSuite) TestExchangeBackupCreateSelectors() { name: "many users, events", user: []string{"fnord", "smarf"}, data: []string{dataEvents}, - expectIncludeLen: 2, + expectIncludeLen: 1, }, { name: "many users, events + contacts", user: []string{"fnord", "smarf"}, data: []string{dataEvents, dataContacts}, - expectIncludeLen: 4, + expectIncludeLen: 2, }, } for _, test := range table { @@ -327,7 +327,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() { { name: "multiple users", users: []string{"fnord", "smarf"}, - expectIncludeLen: 6, + expectIncludeLen: 3, }, { name: "any users, any data", @@ -457,7 +457,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() { name: "many users, events", events: []string{"foo", "bar"}, users: []string{"fnord", "smarf"}, - expectIncludeLen: 2, + expectIncludeLen: 1, }, { name: "many users, events + contacts", @@ -465,7 +465,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() { contactFolders: []string{"foo", "bar"}, events: []string{"foo", "bar"}, users: []string{"fnord", "smarf"}, - expectIncludeLen: 6, + expectIncludeLen: 2, }, } for _, test := range table { diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index 229005113..566e34f07 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -162,7 +162,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() { { name: "multiple users", users: []string{"fnord", "smarf"}, - expectIncludeLen: 6, + expectIncludeLen: 3, }, { name: "any users, any data", @@ -292,7 +292,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() { name: "many users, events", events: []string{"foo", "bar"}, users: []string{"fnord", "smarf"}, - expectIncludeLen: 2, + expectIncludeLen: 1, }, { name: "many users, events + contacts", @@ -300,7 +300,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() { contactFolders: []string{"foo", "bar"}, events: []string{"foo", "bar"}, users: []string{"fnord", "smarf"}, - expectIncludeLen: 6, + expectIncludeLen: 2, }, } for _, test := range table { diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index 48b73ffa8..d66e66585 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -266,7 +266,7 @@ func IterateFilterFolderDirectoriesForCollections( return true } - if !scope.Contains(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { + if !scope.Matches(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { return true } diff --git a/src/internal/path/path.go b/src/internal/path/path.go index 9e56a27e9..099aa4ae0 100644 --- a/src/internal/path/path.go +++ b/src/internal/path/path.go @@ -145,6 +145,7 @@ func (pb Builder) String() string { return join(escaped) } +//nolint:unused func (pb Builder) join(start, end int) string { return join(pb.elements[start:end]) } diff --git a/src/pkg/filters/filters.go b/src/pkg/filters/filters.go index d6ff0821e..64ca33c9d 100644 --- a/src/pkg/filters/filters.go +++ b/src/pkg/filters/filters.go @@ -1,6 +1,8 @@ package filters -import "strings" +import ( + "strings" +) type comparator int @@ -14,10 +16,16 @@ const ( Less // a < b < c Between - // "foo" contains "f" + // "foo,bar,baz" contains "foo" Contains - // "f" is found in "foo" + // "foo" is found in "foo,bar,baz" In + // always passes + Pass + // always fails + Fail + // passthrough for the target + Identity ) const delimiter = "," @@ -44,48 +52,72 @@ type Filter struct { Negate bool `json:"negate"` // when true, negate the comparator result } +// ---------------------------------------------------------------------------------------------------- +// Constructors +// ---------------------------------------------------------------------------------------------------- + // NewEquals creates a filter which Matches(v) is true if // target == v func NewEquals(negate bool, category any, target string) Filter { - return Filter{Equal, category, norm(target), negate} + return Filter{Equal, category, target, negate} } // NewGreater creates a filter which Matches(v) is true if // target > v func NewGreater(negate bool, category any, target string) Filter { - return Filter{Greater, category, norm(target), negate} + return Filter{Greater, category, target, negate} } // NewLess creates a filter which Matches(v) is true if // target < v func NewLess(negate bool, category any, target string) Filter { - return Filter{Less, category, norm(target), negate} + return Filter{Less, category, target, negate} } // NewBetween creates a filter which Matches(v) is true if // lesser < v && v < greater func NewBetween(negate bool, category any, lesser, greater string) Filter { - return Filter{Between, category, norm(join(lesser, greater)), negate} + return Filter{Between, category, join(lesser, greater), negate} } // NewContains creates a filter which Matches(v) is true if // super.Contains(v) func NewContains(negate bool, category any, super string) Filter { - return Filter{Contains, category, norm(super), negate} + return Filter{Contains, category, super, negate} } // NewIn creates a filter which Matches(v) is true if // v.Contains(substr) func NewIn(negate bool, category any, substr string) Filter { - return Filter{In, category, norm(substr), negate} + return Filter{In, category, substr, negate} } +// NewPass creates a filter where Matches(v) always returns true +func NewPass() Filter { + return Filter{Pass, nil, "*", false} +} + +// NewFail creates a filter where Matches(v) always returns false +func NewFail() Filter { + return Filter{Fail, nil, "", false} +} + +// NewIdentity creates a filter intended to hold values, rather than +// compare them. Functionally, it'll behave the same as Equals. +func NewIdentity(id string) Filter { + return Filter{Identity, nil, id, false} +} + +// ---------------------------------------------------------------------------------------------------- +// Comparisons +// ---------------------------------------------------------------------------------------------------- + // Checks whether the filter matches the input func (f Filter) Matches(input string) bool { var cmp func(string, string) bool switch f.Comparator { - case Equal: + case Equal, Identity: cmp = equals case Greater: cmp = greater @@ -97,9 +129,13 @@ func (f Filter) Matches(input string) bool { cmp = contains case In: cmp = in + case Pass: + return true + case Fail: + return false } - result := cmp(f.Target, norm(input)) + result := cmp(norm(f.Target), norm(input)) if f.Negate { result = !result } @@ -140,3 +176,39 @@ func contains(target, input string) bool { func in(target, input string) bool { return strings.Contains(input, target) } + +// ---------------------------------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------------------------------- + +// Targets returns the Target value split into a slice. +func (f Filter) Targets() []string { + return split(f.Target) +} + +func (f Filter) String() string { + var prefix string + + switch f.Comparator { + case Equal: + prefix = "eq:" + case Greater: + prefix = "gt:" + case Less: + prefix = "lt:" + case Between: + prefix = "btwn:" + case Contains: + prefix = "cont:" + case In: + prefix = "in:" + case Pass: + return "pass" + case Fail: + return "fail" + case Identity: + default: // no prefix + } + + return prefix + f.Target +} diff --git a/src/pkg/filters/filters_test.go b/src/pkg/filters/filters_test.go index 5f1a7300a..1499bb59c 100644 --- a/src/pkg/filters/filters_test.go +++ b/src/pkg/filters/filters_test.go @@ -126,6 +126,28 @@ func (suite *FiltersSuite) TestContains() { } } +func (suite *FiltersSuite) TestContains_Joined() { + makeFilt := filters.NewContains + f := makeFilt(false, "", "smarf,userid") + nf := makeFilt(true, "", "smarf,userid") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"userid", assert.True, assert.False}, + {"f,userid", assert.True, assert.False}, + {"fnords", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + func (suite *FiltersSuite) TestIn() { makeFilt := filters.NewIn f := makeFilt(false, "", "murf") @@ -146,3 +168,24 @@ func (suite *FiltersSuite) TestIn() { }) } } + +func (suite *FiltersSuite) TestIn_Joined() { + makeFilt := filters.NewIn + f := makeFilt(false, "", "userid") + nf := makeFilt(true, "", "userid") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"smarf,userid", assert.True, assert.False}, + {"arf,user", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index de9e785fe..9b6d19592 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -1,10 +1,9 @@ package selectors import ( - "strings" - "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/filters" ) // --------------------------------------------------------------------------- @@ -165,19 +164,13 @@ func (s *exchange) DiscreteScopes(userPNs []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope { - users = normalize(users) - folders = normalize(folders) - contacts = normalize(contacts) scopes := []ExchangeScope{} - for _, u := range users { - for _, f := range folders { - scopes = append( - scopes, - makeScope[ExchangeScope](u, Item, ExchangeContact, contacts).set(ExchangeContactFolder, f), - ) - } - } + scopes = append( + scopes, + makeScope[ExchangeScope](Item, ExchangeContact, users, contacts). + set(ExchangeContactFolder, folders), + ) return scopes } @@ -188,16 +181,12 @@ func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) ContactFolders(users, folders []string) []ExchangeScope { - users = normalize(users) - folders = normalize(folders) scopes := []ExchangeScope{} - for _, u := range users { - scopes = append( - scopes, - makeScope[ExchangeScope](u, Group, ExchangeContactFolder, folders), - ) - } + scopes = append( + scopes, + makeScope[ExchangeScope](Group, ExchangeContactFolder, users, folders), + ) return scopes } @@ -208,16 +197,12 @@ func (s *exchange) ContactFolders(users, folders []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Events(users, events []string) []ExchangeScope { - users = normalize(users) - events = normalize(events) scopes := []ExchangeScope{} - for _, u := range users { - scopes = append( - scopes, - makeScope[ExchangeScope](u, Item, ExchangeEvent, events), - ) - } + scopes = append( + scopes, + makeScope[ExchangeScope](Item, ExchangeEvent, users, events), + ) return scopes } @@ -228,19 +213,13 @@ func (s *exchange) Events(users, events []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope { - users = normalize(users) - folders = normalize(folders) - mails = normalize(mails) scopes := []ExchangeScope{} - for _, u := range users { - for _, f := range folders { - scopes = append( - scopes, - makeScope[ExchangeScope](u, Item, ExchangeMail, mails).set(ExchangeMailFolder, f), - ) - } - } + scopes = append( + scopes, + makeScope[ExchangeScope](Item, ExchangeMail, users, mails). + set(ExchangeMailFolder, folders), + ) return scopes } @@ -251,16 +230,12 @@ func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) MailFolders(users, folders []string) []ExchangeScope { - users = normalize(users) - folders = normalize(folders) scopes := []ExchangeScope{} - for _, u := range users { - scopes = append( - scopes, - makeScope[ExchangeScope](u, Group, ExchangeMailFolder, folders), - ) - } + scopes = append( + scopes, + makeScope[ExchangeScope](Group, ExchangeMailFolder, users, folders), + ) return scopes } @@ -271,14 +246,13 @@ func (s *exchange) MailFolders(users, folders []string) []ExchangeScope { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Users(users []string) []ExchangeScope { - users = normalize(users) scopes := []ExchangeScope{} - for _, u := range users { - scopes = append(scopes, makeScope[ExchangeScope](u, Group, ExchangeContactFolder, Any())) - scopes = append(scopes, makeScope[ExchangeScope](u, Item, ExchangeEvent, Any())) - scopes = append(scopes, makeScope[ExchangeScope](u, Group, ExchangeMailFolder, Any())) - } + scopes = append(scopes, + makeScope[ExchangeScope](Group, ExchangeContactFolder, users, Any()), + makeScope[ExchangeScope](Item, ExchangeEvent, users, Any()), + makeScope[ExchangeScope](Group, ExchangeMailFolder, users, Any()), + ) return scopes } @@ -292,7 +266,11 @@ func (s *exchange) Users(users []string) []ExchangeScope { // If the input is empty or selectors.None, the scope will always fail comparisons. func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope { return []ExchangeScope{ - makeFilterScope[ExchangeScope](ExchangeMail, ExchangeFilterMailReceivedAfter, []string{timeStrings}), + makeFilterScope[ExchangeScope]( + ExchangeMail, + ExchangeFilterMailReceivedAfter, + []string{timeStrings}, + wrapFilter(filters.NewLess)), } } @@ -302,7 +280,11 @@ func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope // If the input is empty or selectors.None, the scope will always fail comparisons. func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScope { return []ExchangeScope{ - makeFilterScope[ExchangeScope](ExchangeMail, ExchangeFilterMailReceivedBefore, []string{timeStrings}), + makeFilterScope[ExchangeScope]( + ExchangeMail, + ExchangeFilterMailReceivedBefore, + []string{timeStrings}, + wrapFilter(filters.NewGreater)), } } @@ -313,7 +295,11 @@ func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScop // If any slice is empty, it defaults to [selectors.None] func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope { return []ExchangeScope{ - makeFilterScope[ExchangeScope](ExchangeMail, ExchangeFilterMailSender, senderIDs), + makeFilterScope[ExchangeScope]( + ExchangeMail, + ExchangeFilterMailSender, + senderIDs, + wrapFilter(filters.NewIn)), } } @@ -324,7 +310,11 @@ func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope { // If any slice is empty, it defaults to [selectors.None] func (sr *ExchangeRestore) MailSubject(subjectSubstrings []string) []ExchangeScope { return []ExchangeScope{ - makeFilterScope[ExchangeScope](ExchangeMail, ExchangeFilterMailSubject, subjectSubstrings), + makeFilterScope[ExchangeScope]( + ExchangeMail, + ExchangeFilterMailSubject, + subjectSubstrings, + wrapFilter(filters.NewIn)), } } @@ -346,7 +336,7 @@ func (d ExchangeDestination) GetOrDefault(cat exchangeCategory, current string) return current } - return dest + return dest.Target } // Sets the destination value of the provided category. Returns an error @@ -358,10 +348,10 @@ func (d ExchangeDestination) Set(cat exchangeCategory, dest string) error { cs := cat.String() if curr, ok := d[cs]; ok { - return existingDestinationErr(cs, curr) + return existingDestinationErr(cs, curr.Target) } - d[cs] = dest + d[cs] = filterize(dest) return nil } @@ -410,6 +400,8 @@ func (ec exchangeCategory) String() string { // leafCat returns the leaf category of the receiver. // If the receiver category has multiple leaves (ex: User) or no leaves, // (ex: Unknown), the receiver itself is returned. +// If the receiver category is a filter type (ex: ExchangeFilterMailSubject), +// returns the category covered by the filter. // Ex: ExchangeContactFolder.leafCat() => ExchangeContact // Ex: ExchangeEvent.leafCat() => ExchangeEvent // Ex: ExchangeUser.leafCat() => ExchangeUser @@ -417,7 +409,8 @@ func (ec exchangeCategory) leafCat() categorizer { switch ec { case ExchangeContact, ExchangeContactFolder: return ExchangeContact - case ExchangeMail, ExchangeMailFolder: + case ExchangeMail, ExchangeMailFolder, ExchangeFilterMailReceivedAfter, + ExchangeFilterMailReceivedBefore, ExchangeFilterMailSender, ExchangeFilterMailSubject: return ExchangeMail } @@ -504,7 +497,7 @@ var _ scoper = &ExchangeScope{} // Category describes the type of the data in scope. func (s ExchangeScope) Category() exchangeCategory { - return exchangeCategory(s[scopeKeyCategory]) + return exchangeCategory(getCategory(s)) } // categorizer type is a generic wrapper around Category. @@ -513,22 +506,22 @@ func (s ExchangeScope) categorizer() categorizer { return s.Category() } -// Contains returns true if the category is included in the scope's -// data type, and the target string is included in the scope. -func (s ExchangeScope) Contains(cat exchangeCategory, target string) bool { - return contains(s, cat, target) +// Matches returns true if the category is included in the scope's +// data type, and the target string matches that category's comparator. +func (s ExchangeScope) Matches(cat exchangeCategory, target string) bool { + return matches(s, cat, target) } // FilterCategory returns the category enum of the scope filter. // If the scope is not a filter type, returns ExchangeUnknownCategory. func (s ExchangeScope) FilterCategory() exchangeCategory { - return exchangeCategory(s[scopeKeyInfoFilter]) + return exchangeCategory(getFilterCategory(s)) } // Granularity describes the granularity (directory || item) // of the data in scope. func (s ExchangeScope) Granularity() string { - return s[scopeKeyGranularity] + return getGranularity(s) } // IncludeCategory checks whether the scope includes a certain category of data. @@ -552,7 +545,7 @@ func (s ExchangeScope) Get(cat exchangeCategory) []string { } // sets a value by category to the scope. Only intended for internal use. -func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope { +func (s ExchangeScope) set(cat exchangeCategory, v []string) ExchangeScope { return set(s, cat, v) } @@ -561,15 +554,15 @@ func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope { func (s ExchangeScope) setDefaults() { switch s.Category() { case ExchangeContactFolder: - s[ExchangeContact.String()] = AnyTgt + s[ExchangeContact.String()] = passAny case ExchangeMailFolder: - s[ExchangeMail.String()] = AnyTgt + s[ExchangeMail.String()] = passAny case ExchangeUser: - s[ExchangeContactFolder.String()] = AnyTgt - s[ExchangeContact.String()] = AnyTgt - s[ExchangeEvent.String()] = AnyTgt - s[ExchangeMailFolder.String()] = AnyTgt - s[ExchangeMail.String()] = AnyTgt + s[ExchangeContactFolder.String()] = passAny + s[ExchangeContact.String()] = passAny + s[ExchangeEvent.String()] = passAny + s[ExchangeMailFolder.String()] = passAny + s[ExchangeMail.String()] = passAny } } @@ -609,43 +602,19 @@ func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool { return false } - // the scope must define targets to match on filterCat := s.FilterCategory() - targets := s.Get(filterCat) + i := "" - if len(targets) == 0 { - return false + switch filterCat { + case ExchangeFilterMailSender: + i = info.Sender + case ExchangeFilterMailSubject: + i = info.Subject + case ExchangeFilterMailReceivedAfter: + i = common.FormatTime(info.Received) + case ExchangeFilterMailReceivedBefore: + i = common.FormatTime(info.Received) } - if targets[0] == AnyTgt { - return true - } - - if targets[0] == NoneTgt { - return false - } - - // any of the targets for a given info filter may succeed. - for _, target := range targets { - switch filterCat { - case ExchangeFilterMailSender: - if target == info.Sender { - return true - } - case ExchangeFilterMailSubject: - if strings.Contains(info.Subject, target) { - return true - } - case ExchangeFilterMailReceivedAfter: - if target < common.FormatTime(info.Received) { - return true - } - case ExchangeFilterMailReceivedBefore: - if target > common.FormatTime(info.Received) { - return true - } - } - } - - return false + return s.Matches(filterCat, i) } diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index 819481554..6fc03be2b 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/filters" ) type ExchangeSelectorSuite struct { @@ -69,10 +70,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Contacts() { scopes := sel.Excludes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeContactFolder.String()], folder) - assert.Equal(t, scope[ExchangeContact.String()], join(c1, c2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeContactFolder: folder, + ExchangeContact: join(c1, c2), + }, + ) } func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() { @@ -90,10 +96,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() { scopes := sel.Includes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeContactFolder.String()], folder) - assert.Equal(t, scope[ExchangeContact.String()], join(c1, c2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeContactFolder: folder, + ExchangeContact: join(c1, c2), + }, + ) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContact) } @@ -112,10 +123,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_ContactFolders( scopes := sel.Excludes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeContactFolder.String()], join(f1, f2)) - assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeContactFolder: join(f1, f2), + ExchangeContact: AnyTgt, + }, + ) } func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders() { @@ -132,10 +148,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders( scopes := sel.Includes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeContactFolder.String()], join(f1, f2)) - assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeContactFolder: join(f1, f2), + ExchangeContact: AnyTgt, + }, + ) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContactFolder) } @@ -154,9 +175,14 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Events() { scopes := sel.Excludes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeEvent.String()], join(e1, e2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeEvent: join(e1, e2), + }, + ) } func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() { @@ -173,9 +199,14 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() { scopes := sel.Includes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeEvent.String()], join(e1, e2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeEvent: join(e1, e2), + }, + ) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeEvent) } @@ -195,10 +226,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Mails() { scopes := sel.Excludes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeMailFolder.String()], folder) - assert.Equal(t, scope[ExchangeMail.String()], join(m1, m2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeMailFolder: folder, + ExchangeMail: join(m1, m2), + }, + ) } func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() { @@ -216,10 +252,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() { scopes := sel.Includes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeMailFolder.String()], folder) - assert.Equal(t, scope[ExchangeMail.String()], join(m1, m2)) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeMailFolder: folder, + ExchangeMail: join(m1, m2), + }, + ) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeMail) } @@ -238,10 +279,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_MailFolders() { scopes := sel.Excludes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeMailFolder.String()], join(f1, f2)) - assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeMailFolder: join(f1, f2), + ExchangeMail: AnyTgt, + }, + ) } func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() { @@ -258,10 +304,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() { scopes := sel.Includes require.Len(t, scopes, 1) - scope := scopes[0] - assert.Equal(t, scope[ExchangeUser.String()], user) - assert.Equal(t, scope[ExchangeMailFolder.String()], join(f1, f2)) - assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) + scopeMustHave( + t, + ExchangeScope(scopes[0]), + map[categorizer]string{ + ExchangeUser: user, + ExchangeMailFolder: join(f1, f2), + ExchangeMail: AnyTgt, + }, + ) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeMailFolder) } @@ -277,23 +328,45 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Users() { sel.Exclude(sel.Users([]string{u1, u2})) scopes := sel.Excludes - require.Len(t, scopes, 6) + require.Len(t, scopes, 3) - for _, scope := range scopes { - assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) + for _, sc := range scopes { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ExchangeUser: join(u1, u2)}, + ) - if scope[scopeKeyCategory] == ExchangeContactFolder.String() { - assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) - assert.Equal(t, scope[ExchangeContactFolder.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeContactFolder.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeContact: AnyTgt, + ExchangeContactFolder: AnyTgt, + }, + ) } - if scope[scopeKeyCategory] == ExchangeEvent.String() { - assert.Equal(t, scope[ExchangeEvent.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeEvent.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeEvent: AnyTgt, + }, + ) } - if scope[scopeKeyCategory] == ExchangeMailFolder.String() { - assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) - assert.Equal(t, scope[ExchangeMailFolder.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeMailFolder.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeMail: AnyTgt, + ExchangeMailFolder: AnyTgt, + }, + ) } } } @@ -309,23 +382,45 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Users() { sel.Include(sel.Users([]string{u1, u2})) scopes := sel.Includes - require.Len(t, scopes, 6) + require.Len(t, scopes, 3) - for _, scope := range scopes { - assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) + for _, sc := range scopes { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ExchangeUser: join(u1, u2)}, + ) - if scope[scopeKeyCategory] == ExchangeContactFolder.String() { - assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) - assert.Equal(t, scope[ExchangeContactFolder.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeContactFolder.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeContact: AnyTgt, + ExchangeContactFolder: AnyTgt, + }, + ) } - if scope[scopeKeyCategory] == ExchangeEvent.String() { - assert.Equal(t, scope[ExchangeEvent.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeEvent.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeEvent: AnyTgt, + }, + ) } - if scope[scopeKeyCategory] == ExchangeMailFolder.String() { - assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) - assert.Equal(t, scope[ExchangeMailFolder.String()], AnyTgt) + if sc[scopeKeyCategory].Matches(ExchangeMailFolder.String()) { + scopeMustHave( + t, + ExchangeScope(sc), + map[categorizer]string{ + ExchangeMail: AnyTgt, + ExchangeMailFolder: AnyTgt, + }, + ) } } } @@ -470,7 +565,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_Category() { for _, test := range table { suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) { eb := NewExchangeBackup() - eb.Includes = []scope{{scopeKeyCategory: test.is.String()}} + eb.Includes = []scope{ + {scopeKeyCategory: filters.NewIdentity(test.is.String())}, + } scope := eb.Scopes()[0] test.check(t, test.expect, scope.Category()) }) @@ -502,7 +599,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_IncludesCategory() { for _, test := range table { suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) { eb := NewExchangeBackup() - eb.Includes = []scope{{scopeKeyCategory: test.is.String()}} + eb.Includes = []scope{ + {scopeKeyCategory: filters.NewIdentity(test.is.String())}, + } scope := eb.Scopes()[0] test.check(t, scope.IncludesCategory(test.expect)) }) @@ -984,7 +1083,7 @@ func (suite *ExchangeSelectorSuite) TestContains() { suite.T().Run(test.name, func(t *testing.T) { var result bool for _, scope := range test.scopes { - if scope.Contains(ExchangeMail, target) { + if scope.Matches(ExchangeMail, target) { result = true break } diff --git a/src/pkg/selectors/helpers_test.go b/src/pkg/selectors/helpers_test.go index eb22e4b26..05fac065f 100644 --- a/src/pkg/selectors/helpers_test.go +++ b/src/pkg/selectors/helpers_test.go @@ -1,6 +1,13 @@ package selectors -import "github.com/alcionai/corso/pkg/backup/details" +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/filters" +) // --------------------------------------------------------------------------- // categorizers @@ -58,7 +65,7 @@ type mockScope scope var _ scoper = &mockScope{} func (ms mockScope) categorizer() categorizer { - switch ms[scopeKeyCategory] { + switch ms[scopeKeyCategory].Target { case rootCatStub.String(): return rootCatStub case leafCatStub.String(): @@ -73,7 +80,7 @@ func (ms mockScope) matchesEntry( pathValues map[categorizer]string, entry details.DetailsEntry, ) bool { - return ms[shouldMatch] == "true" + return ms[shouldMatch].Target == "true" } func (ms mockScope) setDefaults() {} @@ -91,12 +98,12 @@ func stubScope(match string) mockScope { } return mockScope{ - rootCatStub.String(): AnyTgt, - scopeKeyCategory: rootCatStub.String(), - scopeKeyGranularity: Item, - scopeKeyResource: stubResource, - scopeKeyDataType: rootCatStub.String(), - shouldMatch: sm, + rootCatStub.String(): passAny, + scopeKeyCategory: filters.NewIdentity(rootCatStub.String()), + scopeKeyGranularity: filters.NewIdentity(Item), + scopeKeyResource: filters.NewIdentity(stubResource), + scopeKeyDataType: filters.NewIdentity(rootCatStub.String()), + shouldMatch: filters.NewIdentity(sm), } } @@ -124,3 +131,12 @@ func setScopesToDefault[T scopeT](ts []T) []T { return ts } + +// calls assert.Equal(t, getCatValue(sc, k)[0], v) on each k:v pair in the map +func scopeMustHave[T scopeT](t *testing.T, sc T, m map[categorizer]string) { + for k, v := range m { + t.Run(k.String(), func(t *testing.T) { + assert.Equal(t, getCatValue(sc, k), split(v)) + }) + } +} diff --git a/src/pkg/selectors/onedrive.go b/src/pkg/selectors/onedrive.go index 89442e4d1..a4391c09a 100644 --- a/src/pkg/selectors/onedrive.go +++ b/src/pkg/selectors/onedrive.go @@ -111,12 +111,9 @@ func (s *oneDrive) Filter(scopes ...[]OneDriveScope) { // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *oneDrive) Users(users []string) []OneDriveScope { - users = normalize(users) scopes := []OneDriveScope{} - for _, u := range users { - scopes = append(scopes, makeScope[OneDriveScope](u, Group, OneDriveUser, users)) - } + scopes = append(scopes, makeScope[OneDriveScope](Group, OneDriveUser, users, users)) return scopes } @@ -226,7 +223,7 @@ var _ scoper = &OneDriveScope{} // Category describes the type of the data in scope. func (s OneDriveScope) Category() oneDriveCategory { - return oneDriveCategory(s[scopeKeyCategory]) + return oneDriveCategory(getCategory(s)) } // categorizer type is a generic wrapper around Category. @@ -238,13 +235,13 @@ func (s OneDriveScope) categorizer() categorizer { // FilterCategory returns the category enum of the scope filter. // If the scope is not a filter type, returns OneDriveUnknownCategory. func (s OneDriveScope) FilterCategory() oneDriveCategory { - return oneDriveCategory(s[scopeKeyInfoFilter]) + return oneDriveCategory(getFilterCategory(s)) } // Granularity describes the granularity (directory || item) // of the data in scope. func (s OneDriveScope) Granularity() string { - return s[scopeKeyGranularity] + return getGranularity(s) } // IncludeCategory checks whether the scope includes a @@ -255,10 +252,10 @@ func (s OneDriveScope) IncludesCategory(cat oneDriveCategory) bool { return categoryMatches(s.Category(), cat) } -// Contains returns true if the category is included in the scope's -// data type, and the target string is included in the scope. -func (s OneDriveScope) Contains(cat oneDriveCategory, target string) bool { - return contains(s, cat, target) +// Matches returns true if the category is included in the scope's +// data type, and the target string matches that category's comparator. +func (s OneDriveScope) Matches(cat oneDriveCategory, target string) bool { + return matches(s, cat, target) } // returns true if the category is included in the scope's data type, diff --git a/src/pkg/selectors/onedrive_test.go b/src/pkg/selectors/onedrive_test.go index 79cd9d072..f5b2f92c7 100644 --- a/src/pkg/selectors/onedrive_test.go +++ b/src/pkg/selectors/onedrive_test.go @@ -87,7 +87,7 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Users() { userScopes := sel.Users([]string{u1, u2}) for _, scope := range userScopes { // Scope value is either u1 or u2 - assert.Contains(t, []string{u1, u2}, scope[OneDriveUser.String()]) + assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()].Target) } // Initialize the selector Include, Exclude, Filter @@ -105,10 +105,10 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Users() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - require.Equal(t, 2, len(test.scopesToCheck)) + require.Len(t, test.scopesToCheck, 1) for _, scope := range test.scopesToCheck { // Scope value is u1,u2 - assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()]) + assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()].Target) } }) } @@ -125,10 +125,14 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Include_Users() { sel.Include(sel.Users([]string{u1, u2})) scopes := sel.Includes - require.Len(t, scopes, 2) + require.Len(t, scopes, 1) - for _, scope := range scopes { - assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()]) + for _, sc := range scopes { + scopeMustHave( + t, + OneDriveScope(sc), + map[categorizer]string{OneDriveUser: join(u1, u2)}, + ) } } @@ -143,9 +147,13 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Exclude_Users() { sel.Exclude(sel.Users([]string{u1, u2})) scopes := sel.Excludes - require.Len(t, scopes, 2) + require.Len(t, scopes, 1) - for _, scope := range scopes { - assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()]) + for _, sc := range scopes { + scopeMustHave( + t, + OneDriveScope(sc), + map[categorizer]string{OneDriveUser: join(u1, u2)}, + ) } } diff --git a/src/pkg/selectors/scopes.go b/src/pkg/selectors/scopes.go index ab2f7f1e3..c69f36a96 100644 --- a/src/pkg/selectors/scopes.go +++ b/src/pkg/selectors/scopes.go @@ -3,8 +3,8 @@ package selectors import ( "strings" - "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/filters" ) // --------------------------------------------------------------------------- @@ -75,7 +75,7 @@ type ( // (human readable), or whether the scope is a filter-type or an inclusion-/exclusion-type. // Metadata values can be used in either logical processing of scopes, and/or for presentation // to end users. - scope map[string]string + scope map[string]filters.Filter // scoper describes the minimum necessary interface that a soundly built scope should // comply with. @@ -109,24 +109,24 @@ type ( } // scopeT is the generic type interface of a scoper. scopeT interface { - ~map[string]string + ~map[string]filters.Filter scoper } ) // makeScope produces a well formatted, typed scope that ensures all base values are populated. func makeScope[T scopeT]( - resource, granularity string, + granularity string, cat categorizer, - vs []string, + resources, vs []string, ) T { s := T{ - scopeKeyCategory: cat.String(), - scopeKeyDataType: cat.leafCat().String(), - scopeKeyGranularity: granularity, - scopeKeyResource: resource, - cat.String(): join(vs...), - cat.rootCat().String(): resource, + scopeKeyCategory: filters.NewIdentity(cat.String()), + scopeKeyDataType: filters.NewIdentity(cat.leafCat().String()), + scopeKeyGranularity: filters.NewIdentity(granularity), + scopeKeyResource: filters.NewIdentity(join(resources...)), + cat.String(): filterize(vs...), + cat.rootCat().String(): filterize(resources...), } return s @@ -137,14 +137,15 @@ func makeScope[T scopeT]( func makeFilterScope[T scopeT]( cat, filterCat categorizer, vs []string, + f func([]string) filters.Filter, ) T { return T{ - scopeKeyCategory: cat.String(), - scopeKeyDataType: cat.leafCat().String(), - scopeKeyGranularity: Filter, - scopeKeyInfoFilter: filterCat.String(), - scopeKeyResource: Filter, - filterCat.String(): join(vs...), + scopeKeyCategory: filters.NewIdentity(cat.String()), + scopeKeyDataType: filters.NewIdentity(cat.leafCat().String()), + scopeKeyGranularity: filters.NewIdentity(Filter), + scopeKeyInfoFilter: filters.NewIdentity(filterCat.String()), + scopeKeyResource: filters.NewIdentity(Filter), + filterCat.String(): f(clean(vs)), } } @@ -152,9 +153,9 @@ func makeFilterScope[T scopeT]( // scope funcs // --------------------------------------------------------------------------- -// contains returns true if the category is included in the scope's +// matches returns true if the category is included in the scope's // data type, and the target string is included in the scope. -func contains[T scopeT, C categoryT](s T, cat C, target string) bool { +func matches[T scopeT, C categoryT](s T, cat C, target string) bool { if !typeAndCategoryMatches(cat, s.categorizer()) { return false } @@ -163,20 +164,23 @@ func contains[T scopeT, C categoryT](s T, cat C, target string) bool { return false } - compare := s[cat.String()] - if len(compare) == 0 { - return false - } + return s[cat.String()].Matches(target) +} - if compare == NoneTgt { - return false - } +// getCategory returns the scope's category value. +// if s is a filter-type scope, returns the filter category. +func getCategory[T scopeT](s T) string { + return s[scopeKeyCategory].Target +} - if compare == AnyTgt { - return true - } +// getFilterCategory returns the scope's infoFilter category value. +func getFilterCategory[T scopeT](s T) string { + return s[scopeKeyInfoFilter].Target +} - return strings.Contains(compare, target) +// getGranularity returns the scope's granularity value. +func getGranularity[T scopeT](s T) string { + return s[scopeKeyGranularity].Target } // getCatValue takes the value of s[cat], split it by the standard @@ -188,20 +192,20 @@ func getCatValue[T scopeT](s T, cat categorizer) []string { return None() } - return split(v) + return split(v.Target) } // set sets a value by category to the scope. Only intended for internal // use, not for exporting to callers. -func set[T scopeT](s T, cat categorizer, v string) T { - s[cat.String()] = v +func set[T scopeT](s T, cat categorizer, v []string) T { + s[cat.String()] = filterize(v...) return s } // granularity describes the granularity (directory || item) // of the data in scope. func granularity[T scopeT](s T) string { - return s[scopeKeyGranularity] + return s[scopeKeyGranularity].Target } // returns true if the category is included in the scope's category type, @@ -211,7 +215,7 @@ func isAnyTarget[T scopeT, C categoryT](s T, cat C) bool { return false } - return s[cat.String()] == AnyTgt + return s[cat.String()].Target == AnyTgt } // reduce filters the entries in the details to only those that match the @@ -227,9 +231,9 @@ func reduce[T scopeT, C categoryT]( } // aggregate each scope type by category for easier isolation in future processing. - excludes := scopesByCategory[T](s.Excludes, dataCategories) - filters := scopesByCategory[T](s.Filters, dataCategories) - includes := scopesByCategory[T](s.Includes, dataCategories) + excls := scopesByCategory[T](s.Excludes, dataCategories) + filts := scopesByCategory[T](s.Filters, dataCategories) + incls := scopesByCategory[T](s.Includes, dataCategories) ents := []details.DetailsEntry{} @@ -247,9 +251,9 @@ func reduce[T scopeT, C categoryT]( dc, dc.pathValues(path), ent, - excludes[dc], - filters[dc], - includes[dc], + excls[dc], + filts[dc], + incls[dc], ) if passed { ents = append(ents, ent) @@ -381,25 +385,32 @@ func matchesPathValues[T scopeT, C categoryT]( cat C, pathValues map[categorizer]string, ) bool { + // if scope specifies a filter category, + // path checking is automatically skipped. + if len(getFilterCategory(sc)) > 0 { + return false + } + for _, c := range cat.pathKeys() { - target := getCatValue(sc, c) + scopeVals := getCatValue(sc, c) // the scope must define the targets to match on - if len(target) == 0 { + if len(scopeVals) == 0 { return false } // None() fails all matches - if target[0] == NoneTgt { + if scopeVals[0] == NoneTgt { return false } // the path must contain a value to match against - pv, ok := pathValues[c] + pathVal, ok := pathValues[c] if !ok { return false } // all parts of the scope must match cc := c.(C) if !isAnyTarget(sc, cc) { - if !common.ContainsString(target, pv) { + f := filters.NewContains(false, cc, join(scopeVals...)) + if !f.Matches(pathVal) { return false } } diff --git a/src/pkg/selectors/scopes_test.go b/src/pkg/selectors/scopes_test.go index 665be3a99..39bad9bc2 100644 --- a/src/pkg/selectors/scopes_test.go +++ b/src/pkg/selectors/scopes_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/alcionai/corso/pkg/backup/details" + "github.com/alcionai/corso/pkg/filters" ) // --------------------------------------------------------------------------- @@ -42,7 +43,7 @@ func (suite *SelectorScopesSuite) TestContains() { name: "none", scope: func() mockScope { stub := stubScope("") - stub[rootCatStub.String()] = NoneTgt + stub[rootCatStub.String()] = failAny return stub }, check: rootCatStub.String(), @@ -52,7 +53,7 @@ func (suite *SelectorScopesSuite) TestContains() { name: "blank value", scope: func() mockScope { stub := stubScope("") - stub[rootCatStub.String()] = "" + stub[rootCatStub.String()] = filters.NewEquals(false, nil, "") return stub }, check: rootCatStub.String(), @@ -62,7 +63,7 @@ func (suite *SelectorScopesSuite) TestContains() { name: "blank target", scope: func() mockScope { stub := stubScope("") - stub[rootCatStub.String()] = "fnords" + stub[rootCatStub.String()] = filterize("fnords") return stub }, check: "", @@ -72,7 +73,7 @@ func (suite *SelectorScopesSuite) TestContains() { name: "matching target", scope: func() mockScope { stub := stubScope("") - stub[rootCatStub.String()] = rootCatStub.String() + stub[rootCatStub.String()] = filterize(rootCatStub.String()) return stub }, check: rootCatStub.String(), @@ -82,7 +83,7 @@ func (suite *SelectorScopesSuite) TestContains() { name: "non-matching target", scope: func() mockScope { stub := stubScope("") - stub[rootCatStub.String()] = rootCatStub.String() + stub[rootCatStub.String()] = filterize(rootCatStub.String()) return stub }, check: "smarf", @@ -93,7 +94,7 @@ func (suite *SelectorScopesSuite) TestContains() { suite.T().Run(test.name, func(t *testing.T) { test.expect( t, - contains(test.scope(), rootCatStub, test.check)) + matches(test.scope(), rootCatStub, test.check)) }) } } @@ -101,8 +102,10 @@ func (suite *SelectorScopesSuite) TestContains() { func (suite *SelectorScopesSuite) TestGetCatValue() { t := suite.T() stub := stubScope("") - stub[rootCatStub.String()] = rootCatStub.String() - assert.Equal(t, []string{rootCatStub.String()}, getCatValue(stub, rootCatStub)) + stub[rootCatStub.String()] = filterize(rootCatStub.String()) + assert.Equal(t, + []string{rootCatStub.String()}, + getCatValue(stub, rootCatStub)) assert.Equal(t, None(), getCatValue(stub, leafCatStub)) } @@ -250,7 +253,7 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() { t := suite.T() s1 := stubScope("") s2 := stubScope("") - s2[scopeKeyCategory] = unknownCatStub.String() + s2[scopeKeyCategory] = filterize(unknownCatStub.String()) result := scopesByCategory[mockScope]( []scope{scope(s1), scope(s2)}, map[pathType]mockCategorizer{ @@ -297,22 +300,158 @@ func toMockScope(sc []scope) []mockScope { } func (suite *SelectorScopesSuite) TestMatchesPathValues() { - t := suite.T() cat := rootCatStub - sc := stubScope("") - sc[rootCatStub.String()] = rootCatStub.String() - sc[leafCatStub.String()] = leafCatStub.String() pvs := stubPathValues() - assert.True(t, matchesPathValues(sc, cat, pvs), "matching values") - // "any" seems like it should pass, but this is the path value, - // not the scope value, so unless the scope is also "any", it fails. - pvs[rootCatStub] = AnyTgt - pvs[leafCatStub] = AnyTgt - assert.False(t, matchesPathValues(sc, cat, pvs), "any") - pvs[rootCatStub] = NoneTgt - pvs[leafCatStub] = NoneTgt - assert.False(t, matchesPathValues(sc, cat, pvs), "none") - pvs[rootCatStub] = "foo" - pvs[leafCatStub] = "bar" - assert.False(t, matchesPathValues(sc, cat, pvs), "mismatched values") + + table := []struct { + name string + rootVal string + leafVal string + expect assert.BoolAssertionFunc + }{ + { + name: "matching values", + rootVal: rootCatStub.String(), + leafVal: leafCatStub.String(), + expect: assert.True, + }, + { + name: "any", + rootVal: AnyTgt, + leafVal: AnyTgt, + expect: assert.True, + }, + { + name: "none", + rootVal: NoneTgt, + leafVal: NoneTgt, + expect: assert.False, + }, + { + name: "mismatched values", + rootVal: "fnords", + leafVal: "smarf", + expect: assert.False, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sc := stubScope("") + sc[rootCatStub.String()] = filterize(test.rootVal) + sc[leafCatStub.String()] = filterize(test.leafVal) + + test.expect(t, matchesPathValues(sc, cat, pvs)) + }) + } +} + +func (suite *SelectorScopesSuite) TestAddToSet() { + t := suite.T() + set := []string{} + + set = addToSet(set, []string{}) + assert.Len(t, set, 0) + + set = addToSet(set, []string{"a"}) + assert.Len(t, set, 1) + assert.Equal(t, set[0], "a") + + set = addToSet(set, []string{"a"}) + assert.Len(t, set, 1) + + set = addToSet(set, []string{"a", "b"}) + assert.Len(t, set, 2) + assert.Equal(t, set[0], "a") + assert.Equal(t, set[1], "b") + + set = addToSet(set, []string{"c", "d"}) + assert.Len(t, set, 4) + assert.Equal(t, set[0], "a") + assert.Equal(t, set[1], "b") + assert.Equal(t, set[2], "c") + assert.Equal(t, set[3], "d") +} + +func (suite *SelectorScopesSuite) TestClean() { + table := []struct { + name string + input []string + expect []string + }{ + { + name: "nil", + input: nil, + expect: None(), + }, + { + name: "has anyTgt", + input: []string{"a", AnyTgt}, + expect: Any(), + }, + { + name: "has noneTgt", + input: []string{"a", NoneTgt}, + expect: None(), + }, + { + name: "has anyTgt and noneTgt, any first", + input: []string{"a", AnyTgt, NoneTgt}, + expect: Any(), + }, + { + name: "has noneTgt and anyTgt, none first", + input: []string{"a", NoneTgt, AnyTgt}, + expect: None(), + }, + { + name: "already clean", + input: []string{"a", "b"}, + expect: []string{"a", "b"}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + result := clean(test.input) + assert.Equal(t, result, test.expect) + }) + } +} + +func (suite *SelectorScopesSuite) TestWrapFilter() { + table := []struct { + name string + filter filterFunc + input []string + comparator int + target string + }{ + { + name: "any", + filter: filters.NewContains, + input: Any(), + comparator: int(filters.Pass), + target: AnyTgt, + }, + { + name: "none", + filter: filters.NewIn, + input: None(), + comparator: int(filters.Fail), + target: NoneTgt, + }, + { + name: "something", + filter: filters.NewEquals, + input: []string{"userid"}, + comparator: int(filters.Equal), + target: "userid", + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ff := wrapFilter(test.filter)(test.input) + assert.Equal(t, int(ff.Comparator), test.comparator) + assert.Equal(t, ff.Target, test.target) + }) + } } diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index 8768f7b95..d8660d211 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/pkg/errors" + + "github.com/alcionai/corso/pkg/filters" ) type service int @@ -53,6 +55,11 @@ const ( delimiter = "," ) +var ( + passAny = filters.NewPass() + failAny = filters.NewFail() +) + // All is the resource name that gets output when the resource is AnyTgt. // It is not used aside from printing resources. const All = "All" @@ -149,9 +156,8 @@ func discreteScopes[T scopeT, C categoryT]( discreteIDs []string, ) []T { sl := []T{} - jdid := join(discreteIDs...) - if len(jdid) == 0 { + if len(discreteIDs) == 0 { return scopes[T](s) } @@ -164,7 +170,7 @@ func discreteScopes[T scopeT, C categoryT]( w[k] = v } - set(w, rootCat, jdid) + set(w, rootCat, discreteIDs) t = w } @@ -246,31 +252,41 @@ func toResourceTypeMap(ms []scope) map[string][]string { for _, m := range ms { res := m[scopeKeyResource] - if res == AnyTgt { - res = All + k := res.Target + + if res.Target == AnyTgt { + k = All } - r[res] = addToSet(r[res], m[scopeKeyDataType]) + r[k] = addToSet(r[k], m[scopeKeyDataType].Targets()) } return r } -// returns [v] if set is empty, -// returns self if set contains v, -// appends v to self, otherwise. -func addToSet(set []string, v string) []string { +// returns v if set is empty, +// unions v with set, otherwise. +func addToSet(set []string, v []string) []string { if len(set) == 0 { - return []string{v} + return v } - for _, s := range set { - if s == v { - return set + for _, vv := range v { + var matched bool + + for _, s := range set { + if vv == s { + matched = true + break + } + } + + if !matched { + set = append(set, vv) } } - return append(set, v) + return set } // --------------------------------------------------------------------------- @@ -305,8 +321,8 @@ func split(s string) []string { // if the slice contains None, returns [None] // if the slice contains Any and None, returns the first // if the slice is empty, returns [None] -// otherwise returns the input unchanged -func normalize(s []string) []string { +// otherwise returns the input +func clean(s []string) []string { if len(s) == 0 { return None() } @@ -323,3 +339,53 @@ func normalize(s []string) []string { return s } + +// filterize turns the slice into a filter. +// if the input is Any(), returns a passAny filter. +// if the input is None(), returns a failAny filter. +// if the input is len(1), returns an Equals filter. +// otherwise returns a Contains filter. +func filterize(s ...string) filters.Filter { + s = clean(s) + + if len(s) == 1 { + if s[0] == AnyTgt { + return passAny + } + + if s[0] == NoneTgt { + return failAny + } + + return filters.NewEquals(false, "", s[0]) + } + + return filters.NewContains(false, "", join(s...)) +} + +type filterFunc func(bool, any, string) filters.Filter + +// wrapFilter produces a func that filterizes the input by: +// - cleans the input string +// - normalizes the cleaned input (returns anyFail if empty, allFail if *) +// - joins the string +// - and generates a filter with the joined input. +func wrapFilter(ff filterFunc) func([]string) filters.Filter { + return func(s []string) filters.Filter { + s = clean(s) + + if len(s) == 1 { + if s[0] == AnyTgt { + return passAny + } + + if s[0] == NoneTgt { + return failAny + } + } + + ss := join(s...) + + return ff(false, nil, ss) + } +} diff --git a/src/pkg/selectors/selectors_test.go b/src/pkg/selectors/selectors_test.go index b9a6710ba..7475a5790 100644 --- a/src/pkg/selectors/selectors_test.go +++ b/src/pkg/selectors/selectors_test.go @@ -57,8 +57,8 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() { sel.Includes = []scope{ scope(stubScope("")), - {scopeKeyResource: "smarf", scopeKeyDataType: unknownCatStub.String()}, - {scopeKeyResource: "smurf", scopeKeyDataType: unknownCatStub.String()}, + {scopeKeyResource: filterize("smarf"), scopeKeyDataType: filterize(unknownCatStub.String())}, + {scopeKeyResource: filterize("smurf"), scopeKeyDataType: filterize(unknownCatStub.String())}, } p = sel.Printable() res = p.Resources() @@ -94,8 +94,8 @@ func (suite *SelectorSuite) TestToResourceTypeMap() { input: []scope{ scope(stubScope("")), { - scopeKeyResource: "smarf", - scopeKeyDataType: unknownCatStub.String(), + scopeKeyResource: filterize("smarf"), + scopeKeyDataType: filterize(unknownCatStub.String()), }, }, expect: map[string][]string{ @@ -108,8 +108,8 @@ func (suite *SelectorSuite) TestToResourceTypeMap() { input: []scope{ scope(stubScope("")), { - scopeKeyResource: stubResource, - scopeKeyDataType: "other", + scopeKeyResource: filterize(stubResource), + scopeKeyDataType: filterize("other"), }, }, expect: map[string][]string{ @@ -130,10 +130,10 @@ func (suite *SelectorSuite) TestContains() { key := rootCatStub target := "fnords" does := stubScope("") - does[key.String()] = target + does[key.String()] = filterize(target) doesNot := stubScope("") - doesNot[key.String()] = "smarf" + doesNot[key.String()] = filterize("smarf") - assert.True(t, contains(does, key, target), "does contain") - assert.False(t, contains(doesNot, key, target), "does not contain") + assert.True(t, matches(does, key, target), "does contain") + assert.False(t, matches(doesNot, key, target), "does not contain") }