refactor scope filtering (#555)

Scope filtering is currently hardcoded to the exchange
use case.  In order for future work to rely on boilerplate
rather than re-writing the full filtering logic on each new
type, as much of that code as is possible has been moved
into a generic toolset.
This commit is contained in:
Keepers 2022-08-22 13:08:14 -06:00 committed by GitHub
parent 7d057dd2ac
commit 12dbfce6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 460 additions and 208 deletions

View File

@ -480,7 +480,7 @@ func (ec exchangeCategory) includesType(cat categorizer) bool {
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} // => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
func (ec exchangeCategory) pathValues(path []string) map[categorizer]string { func (ec exchangeCategory) pathValues(path []string) map[categorizer]string {
m := map[categorizer]string{} m := map[categorizer]string{}
if len(path) < 4 { if len(path) < 2 {
return m return m
} }
m[ExchangeUser] = path[1] m[ExchangeUser] = path[1]
@ -528,7 +528,7 @@ func (ec exchangeCategory) pathKeys() []categorizer {
type ExchangeScope scope type ExchangeScope scope
// interface compliance checks // interface compliance checks
// var _ scoper = &ExchangeScope{} 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 {
@ -541,8 +541,15 @@ func (s ExchangeScope) categorizer() categorizer {
return s.Category() return s.Category()
} }
// Filer describes the specific filter, and its target values. // Contains returns true if the category is included in the scope's
func (s ExchangeScope) Filter() exchangeCategory { // 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)
}
// 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 exchangeCatAtoI(s[scopeKeyInfoFilter]) return exchangeCatAtoI(s[scopeKeyInfoFilter])
} }
@ -552,20 +559,13 @@ func (s ExchangeScope) Granularity() string {
return s[scopeKeyGranularity] return s[scopeKeyGranularity]
} }
// IncludeCategory checks whether the scope includes a // IncludeCategory checks whether the scope includes a certain category of data.
// certain category of data.
// Ex: to check if the scope includes mail data: // Ex: to check if the scope includes mail data:
// s.IncludesCategory(selector.ExchangeMail) // s.IncludesCategory(selector.ExchangeMail)
func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool { func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool {
return s.Category().isType(cat) return s.Category().isType(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 ExchangeScope) Contains(cat exchangeCategory, target string) bool {
return contains(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,
// and the value is set to Any(). // and the value is set to Any().
func (s ExchangeScope) IsAny(cat exchangeCategory) bool { func (s ExchangeScope) IsAny(cat exchangeCategory) bool {
@ -606,31 +606,39 @@ func (s ExchangeScope) setDefaults() {
// Backup Details Filtering // Backup Details Filtering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Reduce filters the entries in a details struct to only those that match the
// inclusions, filters, and exclusions in the selector.
func (s exchange) Reduce(deets *details.Details) *details.Details {
return reduce[ExchangeScope](
deets,
s.Selector,
map[pathType]exchangeCategory{
exchangeContactPath: ExchangeContact,
exchangeEventPath: ExchangeEvent,
exchangeMailPath: ExchangeMail,
},
)
}
// matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details. // matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details.
func (s ExchangeScope) matchesEntry( func (s ExchangeScope) matchesEntry(
cat categorizer, cat categorizer,
pathValues map[categorizer]string, pathValues map[categorizer]string,
entry details.DetailsEntry, entry details.DetailsEntry,
) bool { ) bool {
return false // matchesPathValues can be handled generically, thanks to SCIENCE.
// TODO: uncomment when reducer is added. return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange)
// return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange)
}
// matches returns true if either the path or the info matches the scope details.
func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool {
return s.matchesPath(cat, path) || s.matchesInfo(cat, info)
} }
// matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo // matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo
// returns true if the scope and info match for the provided category. // returns true if the scope and info match for the provided category.
func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeInfo) bool { func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
// we need values to match against // we need values to match against
if info == nil { if info == nil {
return false return false
} }
// the scope must define targets to match on // the scope must define targets to match on
filterCat := s.Filter() filterCat := s.FilterCategory()
targets := s.Get(filterCat) targets := s.Get(filterCat)
if len(targets) == 0 { if len(targets) == 0 {
return false return false
@ -664,152 +672,3 @@ func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeI
} }
return false return false
} }
// matchesPath handles the standard behavior when comparing a scope and a path
// returns true if the scope and path match for the provided category.
func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool {
pathIDs := cat.pathValues(path)
for _, c := range categoryPathSet[cat] {
target := s.Get(c.(exchangeCategory))
// the scope must define the targets to match on
if len(target) == 0 {
return false
}
// None() fails all matches
if target[0] == NoneTgt {
return false
}
// the path must contain a value to match against
id, ok := pathIDs[c]
if !ok {
return false
}
// all parts of the scope must match
isAny := target[0] == AnyTgt
if !isAny {
if !common.ContainsString(target, id) {
return false
}
}
}
return true
}
// ---------------------------------------------------------------------------
// Restore Point Filtering
// ---------------------------------------------------------------------------
// Reduce reduces the entries in a backupDetails struct to only
// those that match the inclusions, filters, and exclusions in the selector.
func (sr *ExchangeRestore) Reduce(deets *details.Details) *details.Details {
if deets == nil {
return nil
}
entExcs := exchangeScopesByCategory(sr.Excludes)
entFilt := exchangeScopesByCategory(sr.Filters)
entIncs := exchangeScopesByCategory(sr.Includes)
ents := []details.DetailsEntry{}
for _, ent := range deets.Entries {
// todo: use Path pkg for this
path := strings.Split(ent.RepoRef, "/")
// not all paths will be len=3. Most should be longer.
// This just protects us from panicing four lines later.
if len(path) < 3 {
continue
}
var cat exchangeCategory
switch path[2] {
case "contact":
cat = ExchangeContact
case "event":
cat = ExchangeEvent
case "mail":
cat = ExchangeMail
}
matched := matchExchangeEntry(
cat,
path,
ent.Exchange,
entExcs[cat.String()],
entFilt[cat.String()],
entIncs[cat.String()])
if matched {
ents = append(ents, ent)
}
}
deets.Entries = ents
return deets
}
// groups each scope by its category of data (contact, event, or mail).
// user-level scopes will duplicate to all three categories.
func exchangeScopesByCategory(scopes []scope) map[string][]ExchangeScope {
m := map[string][]ExchangeScope{
ExchangeContact.String(): {},
ExchangeEvent.String(): {},
ExchangeMail.String(): {},
}
for _, msc := range scopes {
sc := ExchangeScope(msc)
if sc.IncludesCategory(ExchangeContact) {
m[ExchangeContact.String()] = append(m[ExchangeContact.String()], sc)
}
if sc.IncludesCategory(ExchangeEvent) {
m[ExchangeEvent.String()] = append(m[ExchangeEvent.String()], sc)
}
if sc.IncludesCategory(ExchangeMail) {
m[ExchangeMail.String()] = append(m[ExchangeMail.String()], sc)
}
}
return m
}
// compare each path to the included and excluded exchange scopes. Returns true
// if the path is included, passes filters, and not excluded.
func matchExchangeEntry(
cat exchangeCategory,
path []string,
info *details.ExchangeInfo,
excs, filts, incs []ExchangeScope,
) bool {
// a passing match requires either a filter or an inclusion
if len(incs)+len(filts) == 0 {
return false
}
// skip this check if 0 inclusions were populated
// since filters act as the inclusion check in that case
if len(incs) > 0 {
// at least one inclusion must apply.
var included bool
for _, inc := range incs {
if inc.matches(cat, path, info) {
included = true
break
}
}
if !included {
return false
}
}
// all filters must pass
for _, filt := range filts {
if !filt.matches(cat, path, info) {
return false
}
}
// any matching exclusion means failure
for _, exc := range excs {
if exc.matches(cat, path, info) {
return false
}
}
return true
}

View File

@ -67,7 +67,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Contacts() {
sel.Exclude(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2})) sel.Exclude(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -88,7 +88,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Contacts() {
sel.Include(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2})) sel.Include(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -110,7 +110,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_ContactFolders()
sel.Exclude(sel.ContactFolders([]string{user}, []string{f1, f2})) sel.Exclude(sel.ContactFolders([]string{user}, []string{f1, f2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -130,7 +130,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_ContactFolders()
sel.Include(sel.ContactFolders([]string{user}, []string{f1, f2})) sel.Include(sel.ContactFolders([]string{user}, []string{f1, f2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -152,7 +152,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Events() {
sel.Exclude(sel.Events([]string{user}, []string{e1, e2})) sel.Exclude(sel.Events([]string{user}, []string{e1, e2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -171,7 +171,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Events() {
sel.Include(sel.Events([]string{user}, []string{e1, e2})) sel.Include(sel.Events([]string{user}, []string{e1, e2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -193,7 +193,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Mails() {
sel.Exclude(sel.Mails([]string{user}, []string{folder}, []string{m1, m2})) sel.Exclude(sel.Mails([]string{user}, []string{folder}, []string{m1, m2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -214,7 +214,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Mails() {
sel.Include(sel.Mails([]string{user}, []string{folder}, []string{m1, m2})) sel.Include(sel.Mails([]string{user}, []string{folder}, []string{m1, m2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -236,7 +236,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_MailFolders() {
sel.Exclude(sel.MailFolders([]string{user}, []string{f1, f2})) sel.Exclude(sel.MailFolders([]string{user}, []string{f1, f2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -256,7 +256,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_MailFolders() {
sel.Include(sel.MailFolders([]string{user}, []string{f1, f2})) sel.Include(sel.MailFolders([]string{user}, []string{f1, f2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 1, len(scopes)) require.Len(t, scopes, 1)
scope := scopes[0] scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user) assert.Equal(t, scope[ExchangeUser.String()], user)
@ -277,7 +277,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Users() {
sel.Exclude(sel.Users([]string{u1, u2})) sel.Exclude(sel.Users([]string{u1, u2}))
scopes := sel.Excludes scopes := sel.Excludes
require.Equal(t, 6, len(scopes)) require.Len(t, scopes, 6)
for _, scope := range scopes { for _, scope := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()])
@ -306,7 +306,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Users() {
sel.Include(sel.Users([]string{u1, u2})) sel.Include(sel.Users([]string{u1, u2}))
scopes := sel.Includes scopes := sel.Includes
require.Equal(t, 6, len(scopes)) require.Len(t, scopes, 6)
for _, scope := range scopes { for _, scope := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()]) assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()])
@ -531,7 +531,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesInfo() {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
scopes := setScopesToDefault(test.scope) scopes := setScopesToDefault(test.scope)
for _, scope := range scopes { for _, scope := range scopes {
test.expect(t, scope.matchesInfo(scope.Category(), info)) test.expect(t, scope.matchesInfo(info))
} }
}) })
} }
@ -574,7 +574,8 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesPath() {
scopes := setScopesToDefault(test.scope) scopes := setScopesToDefault(test.scope)
var aMatch bool var aMatch bool
for _, scope := range scopes { for _, scope := range scopes {
if scope.matchesPath(ExchangeMail, path) { pv := ExchangeMail.pathValues(path)
if matchesPathValues(scope, ExchangeMail, pv) {
aMatch = true aMatch = true
break break
} }
@ -781,7 +782,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() {
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() { func (suite *ExchangeSourceSuite) TestScopesByCategory() {
var ( var (
es = NewExchangeRestore() es = NewExchangeRestore()
users = es.Users(Any()) users = es.Users(Any())
@ -804,6 +805,11 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
} }
return mss return mss
} }
cats := map[pathType]exchangeCategory{
exchangeContactPath: ExchangeContact,
exchangeEventPath: ExchangeEvent,
exchangeMailPath: ExchangeMail,
}
table := []struct { table := []struct {
name string name string
scopes input scopes input
@ -817,16 +823,16 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
} }
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) {
result := exchangeScopesByCategory(test.scopes) result := scopesByCategory[ExchangeScope](test.scopes, cats)
assert.Equal(t, test.expect.contact, len(result[ExchangeContact.String()])) assert.Len(t, result[ExchangeContact], test.expect.contact)
assert.Equal(t, test.expect.event, len(result[ExchangeEvent.String()])) assert.Len(t, result[ExchangeEvent], test.expect.event)
assert.Equal(t, test.expect.mail, len(result[ExchangeMail.String()])) assert.Len(t, result[ExchangeMail], test.expect.mail)
}) })
} }
} }
func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { func (suite *ExchangeSourceSuite) TestPasses() {
var exchangeInfo *details.ExchangeInfo deets := details.DetailsEntry{}
const ( const (
mid = "mailID" mid = "mailID"
cat = ExchangeMail cat = ExchangeMail
@ -867,7 +873,14 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
} }
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) {
test.expect(t, matchExchangeEntry(cat, path, exchangeInfo, test.excludes, test.filters, test.includes)) result := passes(
cat,
cat.pathValues(path),
deets,
test.excludes,
test.filters,
test.includes)
test.expect(t, result)
}) })
} }
} }
@ -1049,7 +1062,7 @@ func (suite *ExchangeSourceSuite) TestExchangeCategory_PathValues() {
expect map[categorizer]string expect map[categorizer]string
}{ }{
{ExchangeCategoryUnknown, nil, map[categorizer]string{}}, {ExchangeCategoryUnknown, nil, map[categorizer]string{}},
{ExchangeCategoryUnknown, []string{"a", "b", "c"}, map[categorizer]string{}}, {ExchangeCategoryUnknown, []string{"a"}, map[categorizer]string{}},
{ExchangeContact, contactPath, contactMap}, {ExchangeContact, contactPath, contactMap},
{ExchangeEvent, eventPath, eventMap}, {ExchangeEvent, eventPath, eventMap},
{ExchangeMail, mailPath, mailMap}, {ExchangeMail, mailPath, mailMap},

View File

@ -45,13 +45,12 @@ func (sc mockCategorizer) pathKeys() []categorizer {
return []categorizer{rootCatStub, leafCatStub} return []categorizer{rootCatStub, leafCatStub}
} }
// TODO: Uncomment when reducer func is added func stubPathValues() map[categorizer]string {
// func stubPathValues() map[categorizer]string { return map[categorizer]string{
// return map[categorizer]string{ rootCatStub: rootCatStub.String(),
// rootCatStub: rootCatStub.String(), leafCatStub: leafCatStub.String(),
// leafCatStub: leafCatStub.String(), }
// } }
// }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// scopers // scopers

View File

@ -3,6 +3,7 @@ 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"
) )
@ -40,12 +41,11 @@ type (
// so that the two can be compared. // so that the two can be compared.
pathKeys() []categorizer pathKeys() []categorizer
} }
// TODO: Uncomment when reducer func is added
// categoryT is the generic type interface of a categorizer // categoryT is the generic type interface of a categorizer
// categoryT interface { categoryT interface {
// ~int ~int
// categorizer categorizer
// } }
) )
type ( type (
@ -157,3 +157,188 @@ func isAnyTarget[T scopeT](s T, cat categorizer) bool {
} }
return s[cat.String()] == AnyTgt return s[cat.String()] == AnyTgt
} }
// reduce filters the entries in the details to only those that match the
// inclusions, filters, and exclusions in the selector.
//
func reduce[T scopeT, C categoryT](
deets *details.Details,
s Selector,
dataCategories map[pathType]C,
) *details.Details {
if deets == nil {
return nil
}
// 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)
ents := []details.DetailsEntry{}
// for each entry, compare that entry against the scopes of the same data type
for _, ent := range deets.Entries {
// todo: use Path pkg for this
path := strings.Split(ent.RepoRef, "/")
dc, ok := dataCategories[pathTypeIn(path)]
if !ok {
continue
}
passed := passes(
dc,
dc.pathValues(path),
ent,
excludes[dc],
filters[dc],
includes[dc],
)
if passed {
ents = append(ents, ent)
}
}
reduced := &details.Details{DetailsModel: deets.DetailsModel}
reduced.Entries = ents
return reduced
}
// TODO: this is a hack. We don't want these values declared here- it will get
// unwieldy to have all of them for all services. They should be declared in
// paths, since that's where service- and data-type-specific assertions are owned.
type pathType int
const (
unknownPathType pathType = iota
exchangeEventPath
exchangeContactPath
exchangeMailPath
)
// return the service data type of the path.
// TODO: this is a hack. We don't want this identification to occur in this
// package. It should get handled in paths, since that's where service- and
// data-type-specific assertions are owned.
// Ideally, we'd use something like path.DataType() instead of this func.
func pathTypeIn(path []string) pathType {
// not all paths will be len=3. Most should be longer.
// This just protects us from panicing below.
if len(path) < 3 {
return unknownPathType
}
switch path[2] {
case "mail":
return exchangeMailPath
case "contact":
return exchangeContactPath
case "event":
return exchangeEventPath
}
return unknownPathType
}
// groups each scope by its category of data (specified by the service-selector).
// ex: a slice containing the scopes [mail1, mail2, event1]
// would produce a map like { mail: [1, 2], event: [1] }
// so long as "mail" and "event" are contained in cats.
func scopesByCategory[T scopeT, C categoryT](
scopes []scope,
cats map[pathType]C,
) map[C][]T {
m := map[C][]T{}
for _, cat := range cats {
m[cat] = []T{}
}
for _, sc := range scopes {
for _, cat := range cats {
t := T(sc)
if t.categorizer().includesType(cat) {
m[cat] = append(m[cat], t)
}
}
}
return m
}
// passes compares each path to the included and excluded exchange scopes. Returns true
// if the path is included, passes filters, and not excluded.
func passes[T scopeT](
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
excs, filts, incs []T,
) bool {
// a passing match requires either a filter or an inclusion
if len(incs)+len(filts) == 0 {
return false
}
// skip this check if 0 inclusions were populated
// since filters act as the inclusion check in that case
if len(incs) > 0 {
// at least one inclusion must apply.
var included bool
for _, inc := range incs {
if inc.matchesEntry(cat, pathValues, entry) {
included = true
break
}
}
if !included {
return false
}
}
// all filters must pass
for _, filt := range filts {
if !filt.matchesEntry(cat, pathValues, entry) {
return false
}
}
// any matching exclusion means failure
for _, exc := range excs {
if exc.matchesEntry(cat, pathValues, entry) {
return false
}
}
return true
}
// matchesPathValues will check whether the pathValues have matching entries
// in the scope. The keys of the values to match against are identified by
// the categorizer.
// Standard expectations apply: None() or missing values always fail, Any()
// always succeeds.
func matchesPathValues[T scopeT](
sc T,
cat categorizer,
pathValues map[categorizer]string,
) bool {
for _, c := range cat.pathKeys() {
target := getCatValue(sc, c)
// the scope must define the targets to match on
if len(target) == 0 {
return false
}
// None() fails all matches
if target[0] == NoneTgt {
return false
}
// the path must contain a value to match against
pv, ok := pathValues[c]
if !ok {
return false
}
// all parts of the scope must match
if !isAnyTarget(sc, c) {
if !common.ContainsString(target, pv) {
return false
}
}
}
return true
}

View File

@ -4,7 +4,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/pkg/backup/details"
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -115,3 +118,196 @@ func (suite *SelectorScopesSuite) TestIsAnyTarget() {
assert.True(t, isAnyTarget(stub, rootCatStub)) assert.True(t, isAnyTarget(stub, rootCatStub))
assert.False(t, isAnyTarget(stub, leafCatStub)) assert.False(t, isAnyTarget(stub, leafCatStub))
} }
var reduceTestTable = []struct {
name string
sel func() Selector
expectLen int
expectPasses assert.BoolAssertionFunc
}{
{
name: "include all",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
sel.Excludes = nil
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "include none",
sel: func() Selector {
sel := stubSelector()
sel.Includes[0] = scope(stubScope("none"))
sel.Filters = nil
sel.Excludes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "filter and include all",
sel: func() Selector {
sel := stubSelector()
sel.Excludes = nil
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "include all filter none",
sel: func() Selector {
sel := stubSelector()
sel.Filters[0] = scope(stubScope("none"))
sel.Excludes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "include all exclude all",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "include all exclude none",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
sel.Excludes[0] = scope(stubScope("none"))
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "filter all exclude all",
sel: func() Selector {
sel := stubSelector()
sel.Includes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "filter all exclude none",
sel: func() Selector {
sel := stubSelector()
sel.Includes = nil
sel.Excludes[0] = scope(stubScope("none"))
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
}
func (suite *SelectorScopesSuite) TestReduce() {
deets := func() details.Details {
return details.Details{
DetailsModel: details.DetailsModel{
Entries: []details.DetailsEntry{
{RepoRef: rootCatStub.String() + "/stub/" + leafCatStub.String()},
},
},
}
}
dataCats := map[pathType]mockCategorizer{
unknownPathType: rootCatStub,
}
for _, test := range reduceTestTable {
suite.T().Run(test.name, func(t *testing.T) {
ds := deets()
result := reduce[mockScope](&ds, test.sel(), dataCats)
require.NotNil(t, result)
assert.Len(t, result.Entries, test.expectLen)
})
}
}
func (suite *SelectorScopesSuite) TestPathTypeIn() {
t := suite.T()
assert.Equal(t, unknownPathType, pathTypeIn([]string{}), "empty")
assert.Equal(t, exchangeMailPath, pathTypeIn([]string{"", "", "mail"}), "mail")
assert.Equal(t, exchangeContactPath, pathTypeIn([]string{"", "", "contact"}), "contact")
assert.Equal(t, exchangeEventPath, pathTypeIn([]string{"", "", "event"}), "event")
assert.Equal(t, unknownPathType, pathTypeIn([]string{"", "", "fnords"}), "bogus")
}
func (suite *SelectorScopesSuite) TestScopesByCategory() {
t := suite.T()
s1 := stubScope("")
s2 := stubScope("")
s2[scopeKeyCategory] = unknownCatStub.String()
result := scopesByCategory[mockScope](
[]scope{scope(s1), scope(s2)},
map[pathType]mockCategorizer{
unknownPathType: rootCatStub,
})
assert.Len(t, result, 1)
assert.Len(t, result[rootCatStub], 1)
assert.Empty(t, result[leafCatStub])
}
func (suite *SelectorScopesSuite) TestPasses() {
cat := rootCatStub
pathVals := map[categorizer]string{}
entry := details.DetailsEntry{}
for _, test := range reduceTestTable {
suite.T().Run(test.name, func(t *testing.T) {
sel := test.sel()
excl := toMockScope(sel.Excludes)
filt := toMockScope(sel.Filters)
incl := toMockScope(sel.Includes)
result := passes(
cat,
pathVals,
entry,
excl, filt, incl)
test.expectPasses(t, result)
})
}
}
func toMockScope(sc []scope) []mockScope {
if len(sc) == 0 {
return nil
}
ms := []mockScope{}
for _, s := range sc {
ms = append(ms, mockScope(s))
}
return ms
}
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")
}

View File

@ -53,7 +53,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
p := sel.Printable() p := sel.Printable()
res := p.Resources() res := p.Resources()
assert.Equal(t, stubResource, res, "resource should state only the user") assert.Equal(t, stubResource, res, "resource should state only the stub")
sel.Includes = []scope{ sel.Includes = []scope{
scope(stubScope("")), scope(stubScope("")),
@ -68,7 +68,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
p.Includes = nil p.Includes = nil
res = p.Resources() res = p.Resources()
assert.Equal(t, stubResource, res, "resource on filters should state only the user") assert.Equal(t, stubResource, res, "resource on filters should state only the stub")
p.Filters = nil p.Filters = nil
res = p.Resources() res = p.Resources()