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.
This commit is contained in:
Keepers 2022-08-30 15:29:24 -06:00 committed by GitHub
parent e3abc281d6
commit 6f04321a60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 743 additions and 322 deletions

View File

@ -199,13 +199,13 @@ func (suite *ExchangeSuite) TestExchangeBackupCreateSelectors() {
name: "many users, events", name: "many users, events",
user: []string{"fnord", "smarf"}, user: []string{"fnord", "smarf"},
data: []string{dataEvents}, data: []string{dataEvents},
expectIncludeLen: 2, expectIncludeLen: 1,
}, },
{ {
name: "many users, events + contacts", name: "many users, events + contacts",
user: []string{"fnord", "smarf"}, user: []string{"fnord", "smarf"},
data: []string{dataEvents, dataContacts}, data: []string{dataEvents, dataContacts},
expectIncludeLen: 4, expectIncludeLen: 2,
}, },
} }
for _, test := range table { for _, test := range table {
@ -327,7 +327,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() {
{ {
name: "multiple users", name: "multiple users",
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 6, expectIncludeLen: 3,
}, },
{ {
name: "any users, any data", name: "any users, any data",
@ -457,7 +457,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() {
name: "many users, events", name: "many users, events",
events: []string{"foo", "bar"}, events: []string{"foo", "bar"},
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 2, expectIncludeLen: 1,
}, },
{ {
name: "many users, events + contacts", name: "many users, events + contacts",
@ -465,7 +465,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() {
contactFolders: []string{"foo", "bar"}, contactFolders: []string{"foo", "bar"},
events: []string{"foo", "bar"}, events: []string{"foo", "bar"},
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 6, expectIncludeLen: 2,
}, },
} }
for _, test := range table { for _, test := range table {

View File

@ -162,7 +162,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() {
{ {
name: "multiple users", name: "multiple users",
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 6, expectIncludeLen: 3,
}, },
{ {
name: "any users, any data", name: "any users, any data",
@ -292,7 +292,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() {
name: "many users, events", name: "many users, events",
events: []string{"foo", "bar"}, events: []string{"foo", "bar"},
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 2, expectIncludeLen: 1,
}, },
{ {
name: "many users, events + contacts", name: "many users, events + contacts",
@ -300,7 +300,7 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() {
contactFolders: []string{"foo", "bar"}, contactFolders: []string{"foo", "bar"},
events: []string{"foo", "bar"}, events: []string{"foo", "bar"},
users: []string{"fnord", "smarf"}, users: []string{"fnord", "smarf"},
expectIncludeLen: 6, expectIncludeLen: 2,
}, },
} }
for _, test := range table { for _, test := range table {

View File

@ -266,7 +266,7 @@ func IterateFilterFolderDirectoriesForCollections(
return true return true
} }
if !scope.Contains(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { if !scope.Matches(selectors.ExchangeMailFolder, *folder.GetDisplayName()) {
return true return true
} }

View File

@ -145,6 +145,7 @@ func (pb Builder) String() string {
return join(escaped) return join(escaped)
} }
//nolint:unused
func (pb Builder) join(start, end int) string { func (pb Builder) join(start, end int) string {
return join(pb.elements[start:end]) return join(pb.elements[start:end])
} }

View File

@ -1,6 +1,8 @@
package filters package filters
import "strings" import (
"strings"
)
type comparator int type comparator int
@ -14,10 +16,16 @@ const (
Less Less
// a < b < c // a < b < c
Between Between
// "foo" contains "f" // "foo,bar,baz" contains "foo"
Contains Contains
// "f" is found in "foo" // "foo" is found in "foo,bar,baz"
In In
// always passes
Pass
// always fails
Fail
// passthrough for the target
Identity
) )
const delimiter = "," const delimiter = ","
@ -44,48 +52,72 @@ type Filter struct {
Negate bool `json:"negate"` // when true, negate the comparator result Negate bool `json:"negate"` // when true, negate the comparator result
} }
// ----------------------------------------------------------------------------------------------------
// Constructors
// ----------------------------------------------------------------------------------------------------
// NewEquals creates a filter which Matches(v) is true if // NewEquals creates a filter which Matches(v) is true if
// target == v // target == v
func NewEquals(negate bool, category any, target string) Filter { 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 // NewGreater creates a filter which Matches(v) is true if
// target > v // target > v
func NewGreater(negate bool, category any, target string) Filter { 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 // NewLess creates a filter which Matches(v) is true if
// target < v // target < v
func NewLess(negate bool, category any, target string) Filter { 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 // NewBetween creates a filter which Matches(v) is true if
// lesser < v && v < greater // lesser < v && v < greater
func NewBetween(negate bool, category any, lesser, greater string) Filter { 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 // NewContains creates a filter which Matches(v) is true if
// super.Contains(v) // super.Contains(v)
func NewContains(negate bool, category any, super string) Filter { 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 // NewIn creates a filter which Matches(v) is true if
// v.Contains(substr) // v.Contains(substr)
func NewIn(negate bool, category any, substr string) Filter { 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 // Checks whether the filter matches the input
func (f Filter) Matches(input string) bool { func (f Filter) Matches(input string) bool {
var cmp func(string, string) bool var cmp func(string, string) bool
switch f.Comparator { switch f.Comparator {
case Equal: case Equal, Identity:
cmp = equals cmp = equals
case Greater: case Greater:
cmp = greater cmp = greater
@ -97,9 +129,13 @@ func (f Filter) Matches(input string) bool {
cmp = contains cmp = contains
case In: case In:
cmp = 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 { if f.Negate {
result = !result result = !result
} }
@ -140,3 +176,39 @@ func contains(target, input string) bool {
func in(target, input string) bool { func in(target, input string) bool {
return strings.Contains(input, target) 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
}

View File

@ -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() { func (suite *FiltersSuite) TestIn() {
makeFilt := filters.NewIn makeFilt := filters.NewIn
f := makeFilt(false, "", "murf") 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")
})
}
}

View File

@ -1,10 +1,9 @@
package selectors package selectors
import ( import (
"strings"
"github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/pkg/backup/details" "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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope { func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope {
users = normalize(users)
folders = normalize(folders)
contacts = normalize(contacts)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(
for _, f := range folders { scopes,
scopes = append( makeScope[ExchangeScope](Item, ExchangeContact, users, contacts).
scopes, set(ExchangeContactFolder, folders),
makeScope[ExchangeScope](u, Item, ExchangeContact, contacts).set(ExchangeContactFolder, f), )
)
}
}
return scopes 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) ContactFolders(users, folders []string) []ExchangeScope { func (s *exchange) ContactFolders(users, folders []string) []ExchangeScope {
users = normalize(users)
folders = normalize(folders)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(
scopes = append( scopes,
scopes, makeScope[ExchangeScope](Group, ExchangeContactFolder, users, folders),
makeScope[ExchangeScope](u, Group, ExchangeContactFolder, folders), )
)
}
return scopes 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Events(users, events []string) []ExchangeScope { func (s *exchange) Events(users, events []string) []ExchangeScope {
users = normalize(users)
events = normalize(events)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(
scopes = append( scopes,
scopes, makeScope[ExchangeScope](Item, ExchangeEvent, users, events),
makeScope[ExchangeScope](u, Item, ExchangeEvent, events), )
)
}
return scopes 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope { func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope {
users = normalize(users)
folders = normalize(folders)
mails = normalize(mails)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(
for _, f := range folders { scopes,
scopes = append( makeScope[ExchangeScope](Item, ExchangeMail, users, mails).
scopes, set(ExchangeMailFolder, folders),
makeScope[ExchangeScope](u, Item, ExchangeMail, mails).set(ExchangeMailFolder, f), )
)
}
}
return scopes 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) MailFolders(users, folders []string) []ExchangeScope { func (s *exchange) MailFolders(users, folders []string) []ExchangeScope {
users = normalize(users)
folders = normalize(folders)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(
scopes = append( scopes,
scopes, makeScope[ExchangeScope](Group, ExchangeMailFolder, users, folders),
makeScope[ExchangeScope](u, Group, ExchangeMailFolder, folders), )
)
}
return scopes 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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *exchange) Users(users []string) []ExchangeScope { func (s *exchange) Users(users []string) []ExchangeScope {
users = normalize(users)
scopes := []ExchangeScope{} scopes := []ExchangeScope{}
for _, u := range users { scopes = append(scopes,
scopes = append(scopes, makeScope[ExchangeScope](u, Group, ExchangeContactFolder, Any())) makeScope[ExchangeScope](Group, ExchangeContactFolder, users, Any()),
scopes = append(scopes, makeScope[ExchangeScope](u, Item, ExchangeEvent, Any())) makeScope[ExchangeScope](Item, ExchangeEvent, users, Any()),
scopes = append(scopes, makeScope[ExchangeScope](u, Group, ExchangeMailFolder, Any())) makeScope[ExchangeScope](Group, ExchangeMailFolder, users, Any()),
} )
return scopes 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. // If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope { func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope {
return []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. // If the input is empty or selectors.None, the scope will always fail comparisons.
func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScope { func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScope {
return []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] // If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope { func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope {
return []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] // If any slice is empty, it defaults to [selectors.None]
func (sr *ExchangeRestore) MailSubject(subjectSubstrings []string) []ExchangeScope { func (sr *ExchangeRestore) MailSubject(subjectSubstrings []string) []ExchangeScope {
return []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 current
} }
return dest return dest.Target
} }
// Sets the destination value of the provided category. Returns an error // 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() cs := cat.String()
if curr, ok := d[cs]; ok { if curr, ok := d[cs]; ok {
return existingDestinationErr(cs, curr) return existingDestinationErr(cs, curr.Target)
} }
d[cs] = dest d[cs] = filterize(dest)
return nil return nil
} }
@ -410,6 +400,8 @@ func (ec exchangeCategory) String() string {
// leafCat returns the leaf category of the receiver. // leafCat returns the leaf category of the receiver.
// If the receiver category has multiple leaves (ex: User) or no leaves, // If the receiver category has multiple leaves (ex: User) or no leaves,
// (ex: Unknown), the receiver itself is returned. // (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: ExchangeContactFolder.leafCat() => ExchangeContact
// Ex: ExchangeEvent.leafCat() => ExchangeEvent // Ex: ExchangeEvent.leafCat() => ExchangeEvent
// Ex: ExchangeUser.leafCat() => ExchangeUser // Ex: ExchangeUser.leafCat() => ExchangeUser
@ -417,7 +409,8 @@ func (ec exchangeCategory) leafCat() categorizer {
switch ec { switch ec {
case ExchangeContact, ExchangeContactFolder: case ExchangeContact, ExchangeContactFolder:
return ExchangeContact return ExchangeContact
case ExchangeMail, ExchangeMailFolder: case ExchangeMail, ExchangeMailFolder, ExchangeFilterMailReceivedAfter,
ExchangeFilterMailReceivedBefore, ExchangeFilterMailSender, ExchangeFilterMailSubject:
return ExchangeMail return ExchangeMail
} }
@ -504,7 +497,7 @@ var _ scoper = &ExchangeScope{}
// Category describes the type of the data in scope. // Category describes the type of the data in scope.
func (s ExchangeScope) Category() exchangeCategory { func (s ExchangeScope) Category() exchangeCategory {
return exchangeCategory(s[scopeKeyCategory]) return exchangeCategory(getCategory(s))
} }
// categorizer type is a generic wrapper around Category. // categorizer type is a generic wrapper around Category.
@ -513,22 +506,22 @@ func (s ExchangeScope) categorizer() categorizer {
return s.Category() return s.Category()
} }
// 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. // data type, and the target string matches that category's comparator.
func (s ExchangeScope) Contains(cat exchangeCategory, target string) bool { func (s ExchangeScope) Matches(cat exchangeCategory, target string) bool {
return contains(s, cat, target) return matches(s, cat, target)
} }
// FilterCategory returns the category enum of the scope filter. // FilterCategory returns the category enum of the scope filter.
// If the scope is not a filter type, returns ExchangeUnknownCategory. // If the scope is not a filter type, returns ExchangeUnknownCategory.
func (s ExchangeScope) FilterCategory() exchangeCategory { func (s ExchangeScope) FilterCategory() exchangeCategory {
return exchangeCategory(s[scopeKeyInfoFilter]) return exchangeCategory(getFilterCategory(s))
} }
// Granularity describes the granularity (directory || item) // Granularity describes the granularity (directory || item)
// of the data in scope. // of the data in scope.
func (s ExchangeScope) Granularity() string { func (s ExchangeScope) Granularity() string {
return s[scopeKeyGranularity] return getGranularity(s)
} }
// IncludeCategory checks whether the scope includes a certain category of data. // 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. // 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) return set(s, cat, v)
} }
@ -561,15 +554,15 @@ func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope {
func (s ExchangeScope) setDefaults() { func (s ExchangeScope) setDefaults() {
switch s.Category() { switch s.Category() {
case ExchangeContactFolder: case ExchangeContactFolder:
s[ExchangeContact.String()] = AnyTgt s[ExchangeContact.String()] = passAny
case ExchangeMailFolder: case ExchangeMailFolder:
s[ExchangeMail.String()] = AnyTgt s[ExchangeMail.String()] = passAny
case ExchangeUser: case ExchangeUser:
s[ExchangeContactFolder.String()] = AnyTgt s[ExchangeContactFolder.String()] = passAny
s[ExchangeContact.String()] = AnyTgt s[ExchangeContact.String()] = passAny
s[ExchangeEvent.String()] = AnyTgt s[ExchangeEvent.String()] = passAny
s[ExchangeMailFolder.String()] = AnyTgt s[ExchangeMailFolder.String()] = passAny
s[ExchangeMail.String()] = AnyTgt s[ExchangeMail.String()] = passAny
} }
} }
@ -609,43 +602,19 @@ func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
return false return false
} }
// the scope must define targets to match on
filterCat := s.FilterCategory() filterCat := s.FilterCategory()
targets := s.Get(filterCat) i := ""
if len(targets) == 0 { switch filterCat {
return false 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 s.Matches(filterCat, i)
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
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/backup/details"
"github.com/alcionai/corso/pkg/filters"
) )
type ExchangeSelectorSuite struct { type ExchangeSelectorSuite struct {
@ -69,10 +70,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Contacts() {
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeContactFolder.String()], folder) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeContact.String()], join(c1, c2)) map[categorizer]string{
ExchangeUser: user,
ExchangeContactFolder: folder,
ExchangeContact: join(c1, c2),
},
)
} }
func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() { func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() {
@ -90,10 +96,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() {
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeContactFolder.String()], folder) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeContact.String()], join(c1, c2)) map[categorizer]string{
ExchangeUser: user,
ExchangeContactFolder: folder,
ExchangeContact: join(c1, c2),
},
)
assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContact) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContact)
} }
@ -112,10 +123,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_ContactFolders(
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeContactFolder.String()], join(f1, f2)) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) map[categorizer]string{
ExchangeUser: user,
ExchangeContactFolder: join(f1, f2),
ExchangeContact: AnyTgt,
},
)
} }
func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders() { func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders() {
@ -132,10 +148,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders(
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeContactFolder.String()], join(f1, f2)) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) map[categorizer]string{
ExchangeUser: user,
ExchangeContactFolder: join(f1, f2),
ExchangeContact: AnyTgt,
},
)
assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContactFolder) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeContactFolder)
} }
@ -154,9 +175,14 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Events() {
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeEvent.String()], join(e1, e2)) ExchangeScope(scopes[0]),
map[categorizer]string{
ExchangeUser: user,
ExchangeEvent: join(e1, e2),
},
)
} }
func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() { func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() {
@ -173,9 +199,14 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() {
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeEvent.String()], join(e1, e2)) ExchangeScope(scopes[0]),
map[categorizer]string{
ExchangeUser: user,
ExchangeEvent: join(e1, e2),
},
)
assert.Equal(t, sel.Scopes()[0].Category(), ExchangeEvent) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeEvent)
} }
@ -195,10 +226,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Mails() {
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeMailFolder.String()], folder) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeMail.String()], join(m1, m2)) map[categorizer]string{
ExchangeUser: user,
ExchangeMailFolder: folder,
ExchangeMail: join(m1, m2),
},
)
} }
func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() { func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() {
@ -216,10 +252,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() {
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeMailFolder.String()], folder) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeMail.String()], join(m1, m2)) map[categorizer]string{
ExchangeUser: user,
ExchangeMailFolder: folder,
ExchangeMail: join(m1, m2),
},
)
assert.Equal(t, sel.Scopes()[0].Category(), ExchangeMail) assert.Equal(t, sel.Scopes()[0].Category(), ExchangeMail)
} }
@ -238,10 +279,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_MailFolders() {
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeMailFolder.String()], join(f1, f2)) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) map[categorizer]string{
ExchangeUser: user,
ExchangeMailFolder: join(f1, f2),
ExchangeMail: AnyTgt,
},
)
} }
func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() { func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() {
@ -258,10 +304,15 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() {
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 1) require.Len(t, scopes, 1)
scope := scopes[0] scopeMustHave(
assert.Equal(t, scope[ExchangeUser.String()], user) t,
assert.Equal(t, scope[ExchangeMailFolder.String()], join(f1, f2)) ExchangeScope(scopes[0]),
assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) map[categorizer]string{
ExchangeUser: user,
ExchangeMailFolder: join(f1, f2),
ExchangeMail: AnyTgt,
},
)
assert.Equal(t, sel.Scopes()[0].Category(), ExchangeMailFolder) 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})) sel.Exclude(sel.Users([]string{u1, u2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 6) require.Len(t, scopes, 3)
for _, scope := range scopes { for _, sc := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) scopeMustHave(
t,
ExchangeScope(sc),
map[categorizer]string{ExchangeUser: join(u1, u2)},
)
if scope[scopeKeyCategory] == ExchangeContactFolder.String() { if sc[scopeKeyCategory].Matches(ExchangeContactFolder.String()) {
assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) scopeMustHave(
assert.Equal(t, scope[ExchangeContactFolder.String()], AnyTgt) t,
ExchangeScope(sc),
map[categorizer]string{
ExchangeContact: AnyTgt,
ExchangeContactFolder: AnyTgt,
},
)
} }
if scope[scopeKeyCategory] == ExchangeEvent.String() { if sc[scopeKeyCategory].Matches(ExchangeEvent.String()) {
assert.Equal(t, scope[ExchangeEvent.String()], AnyTgt) scopeMustHave(
t,
ExchangeScope(sc),
map[categorizer]string{
ExchangeEvent: AnyTgt,
},
)
} }
if scope[scopeKeyCategory] == ExchangeMailFolder.String() { if sc[scopeKeyCategory].Matches(ExchangeMailFolder.String()) {
assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) scopeMustHave(
assert.Equal(t, scope[ExchangeMailFolder.String()], AnyTgt) 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})) sel.Include(sel.Users([]string{u1, u2}))
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 6) require.Len(t, scopes, 3)
for _, scope := range scopes { for _, sc := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) scopeMustHave(
t,
ExchangeScope(sc),
map[categorizer]string{ExchangeUser: join(u1, u2)},
)
if scope[scopeKeyCategory] == ExchangeContactFolder.String() { if sc[scopeKeyCategory].Matches(ExchangeContactFolder.String()) {
assert.Equal(t, scope[ExchangeContact.String()], AnyTgt) scopeMustHave(
assert.Equal(t, scope[ExchangeContactFolder.String()], AnyTgt) t,
ExchangeScope(sc),
map[categorizer]string{
ExchangeContact: AnyTgt,
ExchangeContactFolder: AnyTgt,
},
)
} }
if scope[scopeKeyCategory] == ExchangeEvent.String() { if sc[scopeKeyCategory].Matches(ExchangeEvent.String()) {
assert.Equal(t, scope[ExchangeEvent.String()], AnyTgt) scopeMustHave(
t,
ExchangeScope(sc),
map[categorizer]string{
ExchangeEvent: AnyTgt,
},
)
} }
if scope[scopeKeyCategory] == ExchangeMailFolder.String() { if sc[scopeKeyCategory].Matches(ExchangeMailFolder.String()) {
assert.Equal(t, scope[ExchangeMail.String()], AnyTgt) scopeMustHave(
assert.Equal(t, scope[ExchangeMailFolder.String()], AnyTgt) t,
ExchangeScope(sc),
map[categorizer]string{
ExchangeMail: AnyTgt,
ExchangeMailFolder: AnyTgt,
},
)
} }
} }
} }
@ -470,7 +565,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_Category() {
for _, test := range table { for _, test := range table {
suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) { suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) {
eb := NewExchangeBackup() eb := NewExchangeBackup()
eb.Includes = []scope{{scopeKeyCategory: test.is.String()}} eb.Includes = []scope{
{scopeKeyCategory: filters.NewIdentity(test.is.String())},
}
scope := eb.Scopes()[0] scope := eb.Scopes()[0]
test.check(t, test.expect, scope.Category()) test.check(t, test.expect, scope.Category())
}) })
@ -502,7 +599,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_IncludesCategory() {
for _, test := range table { for _, test := range table {
suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) { suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) {
eb := NewExchangeBackup() eb := NewExchangeBackup()
eb.Includes = []scope{{scopeKeyCategory: test.is.String()}} eb.Includes = []scope{
{scopeKeyCategory: filters.NewIdentity(test.is.String())},
}
scope := eb.Scopes()[0] scope := eb.Scopes()[0]
test.check(t, scope.IncludesCategory(test.expect)) test.check(t, scope.IncludesCategory(test.expect))
}) })
@ -984,7 +1083,7 @@ func (suite *ExchangeSelectorSuite) TestContains() {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
var result bool var result bool
for _, scope := range test.scopes { for _, scope := range test.scopes {
if scope.Contains(ExchangeMail, target) { if scope.Matches(ExchangeMail, target) {
result = true result = true
break break
} }

View File

@ -1,6 +1,13 @@
package selectors 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 // categorizers
@ -58,7 +65,7 @@ type mockScope scope
var _ scoper = &mockScope{} var _ scoper = &mockScope{}
func (ms mockScope) categorizer() categorizer { func (ms mockScope) categorizer() categorizer {
switch ms[scopeKeyCategory] { switch ms[scopeKeyCategory].Target {
case rootCatStub.String(): case rootCatStub.String():
return rootCatStub return rootCatStub
case leafCatStub.String(): case leafCatStub.String():
@ -73,7 +80,7 @@ func (ms mockScope) matchesEntry(
pathValues map[categorizer]string, pathValues map[categorizer]string,
entry details.DetailsEntry, entry details.DetailsEntry,
) bool { ) bool {
return ms[shouldMatch] == "true" return ms[shouldMatch].Target == "true"
} }
func (ms mockScope) setDefaults() {} func (ms mockScope) setDefaults() {}
@ -91,12 +98,12 @@ func stubScope(match string) mockScope {
} }
return mockScope{ return mockScope{
rootCatStub.String(): AnyTgt, rootCatStub.String(): passAny,
scopeKeyCategory: rootCatStub.String(), scopeKeyCategory: filters.NewIdentity(rootCatStub.String()),
scopeKeyGranularity: Item, scopeKeyGranularity: filters.NewIdentity(Item),
scopeKeyResource: stubResource, scopeKeyResource: filters.NewIdentity(stubResource),
scopeKeyDataType: rootCatStub.String(), scopeKeyDataType: filters.NewIdentity(rootCatStub.String()),
shouldMatch: sm, shouldMatch: filters.NewIdentity(sm),
} }
} }
@ -124,3 +131,12 @@ func setScopesToDefault[T scopeT](ts []T) []T {
return ts 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))
})
}
}

View File

@ -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 contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
func (s *oneDrive) Users(users []string) []OneDriveScope { func (s *oneDrive) Users(users []string) []OneDriveScope {
users = normalize(users)
scopes := []OneDriveScope{} scopes := []OneDriveScope{}
for _, u := range users { scopes = append(scopes, makeScope[OneDriveScope](Group, OneDriveUser, users, users))
scopes = append(scopes, makeScope[OneDriveScope](u, Group, OneDriveUser, users))
}
return scopes return scopes
} }
@ -226,7 +223,7 @@ var _ scoper = &OneDriveScope{}
// Category describes the type of the data in scope. // Category describes the type of the data in scope.
func (s OneDriveScope) Category() oneDriveCategory { func (s OneDriveScope) Category() oneDriveCategory {
return oneDriveCategory(s[scopeKeyCategory]) return oneDriveCategory(getCategory(s))
} }
// categorizer type is a generic wrapper around Category. // 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. // FilterCategory returns the category enum of the scope filter.
// If the scope is not a filter type, returns OneDriveUnknownCategory. // If the scope is not a filter type, returns OneDriveUnknownCategory.
func (s OneDriveScope) FilterCategory() oneDriveCategory { func (s OneDriveScope) FilterCategory() oneDriveCategory {
return oneDriveCategory(s[scopeKeyInfoFilter]) return oneDriveCategory(getFilterCategory(s))
} }
// Granularity describes the granularity (directory || item) // Granularity describes the granularity (directory || item)
// of the data in scope. // of the data in scope.
func (s OneDriveScope) Granularity() string { func (s OneDriveScope) Granularity() string {
return s[scopeKeyGranularity] return getGranularity(s)
} }
// IncludeCategory checks whether the scope includes a // IncludeCategory checks whether the scope includes a
@ -255,10 +252,10 @@ func (s OneDriveScope) IncludesCategory(cat oneDriveCategory) bool {
return categoryMatches(s.Category(), cat) return categoryMatches(s.Category(), cat)
} }
// 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. // data type, and the target string matches that category's comparator.
func (s OneDriveScope) Contains(cat oneDriveCategory, target string) bool { func (s OneDriveScope) Matches(cat oneDriveCategory, target string) bool {
return contains(s, cat, target) return matches(s, cat, target)
} }
// returns true if the category is included in the scope's data type, // returns true if the category is included in the scope's data type,

View File

@ -87,7 +87,7 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Users() {
userScopes := sel.Users([]string{u1, u2}) userScopes := sel.Users([]string{u1, u2})
for _, scope := range userScopes { for _, scope := range userScopes {
// Scope value is either u1 or u2 // 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 // Initialize the selector Include, Exclude, Filter
@ -105,10 +105,10 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Users() {
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { 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 { for _, scope := range test.scopesToCheck {
// Scope value is u1,u2 // 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})) sel.Include(sel.Users([]string{u1, u2}))
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 2) require.Len(t, scopes, 1)
for _, scope := range scopes { for _, sc := range scopes {
assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()]) 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})) sel.Exclude(sel.Users([]string{u1, u2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 2) require.Len(t, scopes, 1)
for _, scope := range scopes { for _, sc := range scopes {
assert.Contains(t, join(u1, u2), scope[OneDriveUser.String()]) scopeMustHave(
t,
OneDriveScope(sc),
map[categorizer]string{OneDriveUser: join(u1, u2)},
)
} }
} }

View File

@ -3,8 +3,8 @@ package selectors
import ( import (
"strings" "strings"
"github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/pkg/backup/details" "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. // (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 // Metadata values can be used in either logical processing of scopes, and/or for presentation
// to end users. // to end users.
scope map[string]string scope map[string]filters.Filter
// scoper describes the minimum necessary interface that a soundly built scope should // scoper describes the minimum necessary interface that a soundly built scope should
// comply with. // comply with.
@ -109,24 +109,24 @@ type (
} }
// scopeT is the generic type interface of a scoper. // scopeT is the generic type interface of a scoper.
scopeT interface { scopeT interface {
~map[string]string ~map[string]filters.Filter
scoper scoper
} }
) )
// makeScope produces a well formatted, typed scope that ensures all base values are populated. // makeScope produces a well formatted, typed scope that ensures all base values are populated.
func makeScope[T scopeT]( func makeScope[T scopeT](
resource, granularity string, granularity string,
cat categorizer, cat categorizer,
vs []string, resources, vs []string,
) T { ) T {
s := T{ s := T{
scopeKeyCategory: cat.String(), scopeKeyCategory: filters.NewIdentity(cat.String()),
scopeKeyDataType: cat.leafCat().String(), scopeKeyDataType: filters.NewIdentity(cat.leafCat().String()),
scopeKeyGranularity: granularity, scopeKeyGranularity: filters.NewIdentity(granularity),
scopeKeyResource: resource, scopeKeyResource: filters.NewIdentity(join(resources...)),
cat.String(): join(vs...), cat.String(): filterize(vs...),
cat.rootCat().String(): resource, cat.rootCat().String(): filterize(resources...),
} }
return s return s
@ -137,14 +137,15 @@ func makeScope[T scopeT](
func makeFilterScope[T scopeT]( func makeFilterScope[T scopeT](
cat, filterCat categorizer, cat, filterCat categorizer,
vs []string, vs []string,
f func([]string) filters.Filter,
) T { ) T {
return T{ return T{
scopeKeyCategory: cat.String(), scopeKeyCategory: filters.NewIdentity(cat.String()),
scopeKeyDataType: cat.leafCat().String(), scopeKeyDataType: filters.NewIdentity(cat.leafCat().String()),
scopeKeyGranularity: Filter, scopeKeyGranularity: filters.NewIdentity(Filter),
scopeKeyInfoFilter: filterCat.String(), scopeKeyInfoFilter: filters.NewIdentity(filterCat.String()),
scopeKeyResource: Filter, scopeKeyResource: filters.NewIdentity(Filter),
filterCat.String(): join(vs...), filterCat.String(): f(clean(vs)),
} }
} }
@ -152,9 +153,9 @@ func makeFilterScope[T scopeT](
// scope funcs // 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. // 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()) { if !typeAndCategoryMatches(cat, s.categorizer()) {
return false return false
} }
@ -163,20 +164,23 @@ func contains[T scopeT, C categoryT](s T, cat C, target string) bool {
return false return false
} }
compare := s[cat.String()] return s[cat.String()].Matches(target)
if len(compare) == 0 { }
return false
}
if compare == NoneTgt { // getCategory returns the scope's category value.
return false // 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 { // getFilterCategory returns the scope's infoFilter category value.
return true 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 // 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 None()
} }
return split(v) return split(v.Target)
} }
// set sets a value by category to the scope. Only intended for internal // set sets a value by category to the scope. Only intended for internal
// use, not for exporting to callers. // use, not for exporting to callers.
func set[T scopeT](s T, cat categorizer, v string) T { func set[T scopeT](s T, cat categorizer, v []string) T {
s[cat.String()] = v s[cat.String()] = filterize(v...)
return s return s
} }
// granularity describes the granularity (directory || item) // granularity describes the granularity (directory || item)
// of the data in scope. // of the data in scope.
func granularity[T scopeT](s T) string { 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, // 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 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 // 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. // aggregate each scope type by category for easier isolation in future processing.
excludes := scopesByCategory[T](s.Excludes, dataCategories) excls := scopesByCategory[T](s.Excludes, dataCategories)
filters := scopesByCategory[T](s.Filters, dataCategories) filts := scopesByCategory[T](s.Filters, dataCategories)
includes := scopesByCategory[T](s.Includes, dataCategories) incls := scopesByCategory[T](s.Includes, dataCategories)
ents := []details.DetailsEntry{} ents := []details.DetailsEntry{}
@ -247,9 +251,9 @@ func reduce[T scopeT, C categoryT](
dc, dc,
dc.pathValues(path), dc.pathValues(path),
ent, ent,
excludes[dc], excls[dc],
filters[dc], filts[dc],
includes[dc], incls[dc],
) )
if passed { if passed {
ents = append(ents, ent) ents = append(ents, ent)
@ -381,25 +385,32 @@ func matchesPathValues[T scopeT, C categoryT](
cat C, cat C,
pathValues map[categorizer]string, pathValues map[categorizer]string,
) bool { ) bool {
// if scope specifies a filter category,
// path checking is automatically skipped.
if len(getFilterCategory(sc)) > 0 {
return false
}
for _, c := range cat.pathKeys() { for _, c := range cat.pathKeys() {
target := getCatValue(sc, c) scopeVals := getCatValue(sc, c)
// the scope must define the targets to match on // the scope must define the targets to match on
if len(target) == 0 { if len(scopeVals) == 0 {
return false return false
} }
// None() fails all matches // None() fails all matches
if target[0] == NoneTgt { if scopeVals[0] == NoneTgt {
return false return false
} }
// the path must contain a value to match against // the path must contain a value to match against
pv, ok := pathValues[c] pathVal, ok := pathValues[c]
if !ok { if !ok {
return false return false
} }
// all parts of the scope must match // all parts of the scope must match
cc := c.(C) cc := c.(C)
if !isAnyTarget(sc, cc) { if !isAnyTarget(sc, cc) {
if !common.ContainsString(target, pv) { f := filters.NewContains(false, cc, join(scopeVals...))
if !f.Matches(pathVal) {
return false return false
} }
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/backup/details"
"github.com/alcionai/corso/pkg/filters"
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -42,7 +43,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "none", name: "none",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = NoneTgt stub[rootCatStub.String()] = failAny
return stub return stub
}, },
check: rootCatStub.String(), check: rootCatStub.String(),
@ -52,7 +53,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "blank value", name: "blank value",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = "" stub[rootCatStub.String()] = filters.NewEquals(false, nil, "")
return stub return stub
}, },
check: rootCatStub.String(), check: rootCatStub.String(),
@ -62,7 +63,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "blank target", name: "blank target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = "fnords" stub[rootCatStub.String()] = filterize("fnords")
return stub return stub
}, },
check: "", check: "",
@ -72,7 +73,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "matching target", name: "matching target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = rootCatStub.String() stub[rootCatStub.String()] = filterize(rootCatStub.String())
return stub return stub
}, },
check: rootCatStub.String(), check: rootCatStub.String(),
@ -82,7 +83,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "non-matching target", name: "non-matching target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = rootCatStub.String() stub[rootCatStub.String()] = filterize(rootCatStub.String())
return stub return stub
}, },
check: "smarf", check: "smarf",
@ -93,7 +94,7 @@ func (suite *SelectorScopesSuite) TestContains() {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
test.expect( test.expect(
t, 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() { func (suite *SelectorScopesSuite) TestGetCatValue() {
t := suite.T() t := suite.T()
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = rootCatStub.String() stub[rootCatStub.String()] = filterize(rootCatStub.String())
assert.Equal(t, []string{rootCatStub.String()}, getCatValue(stub, rootCatStub)) assert.Equal(t,
[]string{rootCatStub.String()},
getCatValue(stub, rootCatStub))
assert.Equal(t, None(), getCatValue(stub, leafCatStub)) assert.Equal(t, None(), getCatValue(stub, leafCatStub))
} }
@ -250,7 +253,7 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
t := suite.T() t := suite.T()
s1 := stubScope("") s1 := stubScope("")
s2 := stubScope("") s2 := stubScope("")
s2[scopeKeyCategory] = unknownCatStub.String() s2[scopeKeyCategory] = filterize(unknownCatStub.String())
result := scopesByCategory[mockScope]( result := scopesByCategory[mockScope](
[]scope{scope(s1), scope(s2)}, []scope{scope(s1), scope(s2)},
map[pathType]mockCategorizer{ map[pathType]mockCategorizer{
@ -297,22 +300,158 @@ func toMockScope(sc []scope) []mockScope {
} }
func (suite *SelectorScopesSuite) TestMatchesPathValues() { func (suite *SelectorScopesSuite) TestMatchesPathValues() {
t := suite.T()
cat := rootCatStub cat := rootCatStub
sc := stubScope("")
sc[rootCatStub.String()] = rootCatStub.String()
sc[leafCatStub.String()] = leafCatStub.String()
pvs := stubPathValues() pvs := stubPathValues()
assert.True(t, matchesPathValues(sc, cat, pvs), "matching values")
// "any" seems like it should pass, but this is the path value, table := []struct {
// not the scope value, so unless the scope is also "any", it fails. name string
pvs[rootCatStub] = AnyTgt rootVal string
pvs[leafCatStub] = AnyTgt leafVal string
assert.False(t, matchesPathValues(sc, cat, pvs), "any") expect assert.BoolAssertionFunc
pvs[rootCatStub] = NoneTgt }{
pvs[leafCatStub] = NoneTgt {
assert.False(t, matchesPathValues(sc, cat, pvs), "none") name: "matching values",
pvs[rootCatStub] = "foo" rootVal: rootCatStub.String(),
pvs[leafCatStub] = "bar" leafVal: leafCatStub.String(),
assert.False(t, matchesPathValues(sc, cat, pvs), "mismatched values") 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)
})
}
} }

View File

@ -6,6 +6,8 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/pkg/filters"
) )
type service int type service int
@ -53,6 +55,11 @@ const (
delimiter = "," delimiter = ","
) )
var (
passAny = filters.NewPass()
failAny = filters.NewFail()
)
// All is the resource name that gets output when the resource is AnyTgt. // All is the resource name that gets output when the resource is AnyTgt.
// It is not used aside from printing resources. // It is not used aside from printing resources.
const All = "All" const All = "All"
@ -149,9 +156,8 @@ func discreteScopes[T scopeT, C categoryT](
discreteIDs []string, discreteIDs []string,
) []T { ) []T {
sl := []T{} sl := []T{}
jdid := join(discreteIDs...)
if len(jdid) == 0 { if len(discreteIDs) == 0 {
return scopes[T](s) return scopes[T](s)
} }
@ -164,7 +170,7 @@ func discreteScopes[T scopeT, C categoryT](
w[k] = v w[k] = v
} }
set(w, rootCat, jdid) set(w, rootCat, discreteIDs)
t = w t = w
} }
@ -246,31 +252,41 @@ func toResourceTypeMap(ms []scope) map[string][]string {
for _, m := range ms { for _, m := range ms {
res := m[scopeKeyResource] res := m[scopeKeyResource]
if res == AnyTgt { k := res.Target
res = All
if res.Target == AnyTgt {
k = All
} }
r[res] = addToSet(r[res], m[scopeKeyDataType]) r[k] = addToSet(r[k], m[scopeKeyDataType].Targets())
} }
return r return r
} }
// returns [v] if set is empty, // returns v if set is empty,
// returns self if set contains v, // unions v with set, otherwise.
// appends v to self, otherwise. func addToSet(set []string, v []string) []string {
func addToSet(set []string, v string) []string {
if len(set) == 0 { if len(set) == 0 {
return []string{v} return v
} }
for _, s := range set { for _, vv := range v {
if s == v { var matched bool
return set
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 None, returns [None]
// if the slice contains Any and None, returns the first // if the slice contains Any and None, returns the first
// if the slice is empty, returns [None] // if the slice is empty, returns [None]
// otherwise returns the input unchanged // otherwise returns the input
func normalize(s []string) []string { func clean(s []string) []string {
if len(s) == 0 { if len(s) == 0 {
return None() return None()
} }
@ -323,3 +339,53 @@ func normalize(s []string) []string {
return s 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)
}
}

View File

@ -57,8 +57,8 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
sel.Includes = []scope{ sel.Includes = []scope{
scope(stubScope("")), scope(stubScope("")),
{scopeKeyResource: "smarf", scopeKeyDataType: unknownCatStub.String()}, {scopeKeyResource: filterize("smarf"), scopeKeyDataType: filterize(unknownCatStub.String())},
{scopeKeyResource: "smurf", scopeKeyDataType: unknownCatStub.String()}, {scopeKeyResource: filterize("smurf"), scopeKeyDataType: filterize(unknownCatStub.String())},
} }
p = sel.Printable() p = sel.Printable()
res = p.Resources() res = p.Resources()
@ -94,8 +94,8 @@ func (suite *SelectorSuite) TestToResourceTypeMap() {
input: []scope{ input: []scope{
scope(stubScope("")), scope(stubScope("")),
{ {
scopeKeyResource: "smarf", scopeKeyResource: filterize("smarf"),
scopeKeyDataType: unknownCatStub.String(), scopeKeyDataType: filterize(unknownCatStub.String()),
}, },
}, },
expect: map[string][]string{ expect: map[string][]string{
@ -108,8 +108,8 @@ func (suite *SelectorSuite) TestToResourceTypeMap() {
input: []scope{ input: []scope{
scope(stubScope("")), scope(stubScope("")),
{ {
scopeKeyResource: stubResource, scopeKeyResource: filterize(stubResource),
scopeKeyDataType: "other", scopeKeyDataType: filterize("other"),
}, },
}, },
expect: map[string][]string{ expect: map[string][]string{
@ -130,10 +130,10 @@ func (suite *SelectorSuite) TestContains() {
key := rootCatStub key := rootCatStub
target := "fnords" target := "fnords"
does := stubScope("") does := stubScope("")
does[key.String()] = target does[key.String()] = filterize(target)
doesNot := stubScope("") doesNot := stubScope("")
doesNot[key.String()] = "smarf" doesNot[key.String()] = filterize("smarf")
assert.True(t, contains(does, key, target), "does contain") assert.True(t, matches(does, key, target), "does contain")
assert.False(t, contains(doesNot, key, target), "does not contain") assert.False(t, matches(doesNot, key, target), "does not contain")
} }