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:
parent
7d057dd2ac
commit
12dbfce6d6
@ -480,7 +480,7 @@ func (ec exchangeCategory) includesType(cat categorizer) bool {
|
||||
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
|
||||
func (ec exchangeCategory) pathValues(path []string) map[categorizer]string {
|
||||
m := map[categorizer]string{}
|
||||
if len(path) < 4 {
|
||||
if len(path) < 2 {
|
||||
return m
|
||||
}
|
||||
m[ExchangeUser] = path[1]
|
||||
@ -528,7 +528,7 @@ func (ec exchangeCategory) pathKeys() []categorizer {
|
||||
type ExchangeScope scope
|
||||
|
||||
// interface compliance checks
|
||||
// var _ scoper = &ExchangeScope{}
|
||||
var _ scoper = &ExchangeScope{}
|
||||
|
||||
// Category describes the type of the data in scope.
|
||||
func (s ExchangeScope) Category() exchangeCategory {
|
||||
@ -541,8 +541,15 @@ func (s ExchangeScope) categorizer() categorizer {
|
||||
return s.Category()
|
||||
}
|
||||
|
||||
// Filer describes the specific filter, and its target values.
|
||||
func (s ExchangeScope) Filter() exchangeCategory {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
@ -552,20 +559,13 @@ func (s ExchangeScope) Granularity() string {
|
||||
return s[scopeKeyGranularity]
|
||||
}
|
||||
|
||||
// IncludeCategory checks whether the scope includes a
|
||||
// certain category of data.
|
||||
// IncludeCategory checks whether the scope includes a certain category of data.
|
||||
// Ex: to check if the scope includes mail data:
|
||||
// s.IncludesCategory(selector.ExchangeMail)
|
||||
func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool {
|
||||
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,
|
||||
// and the value is set to Any().
|
||||
func (s ExchangeScope) IsAny(cat exchangeCategory) bool {
|
||||
@ -606,31 +606,39 @@ func (s ExchangeScope) setDefaults() {
|
||||
// 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.
|
||||
func (s ExchangeScope) matchesEntry(
|
||||
cat categorizer,
|
||||
pathValues map[categorizer]string,
|
||||
entry details.DetailsEntry,
|
||||
) bool {
|
||||
return false
|
||||
// TODO: uncomment when reducer is added.
|
||||
// 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)
|
||||
// matchesPathValues can be handled generically, thanks to SCIENCE.
|
||||
return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange)
|
||||
}
|
||||
|
||||
// matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo
|
||||
// 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
|
||||
if info == nil {
|
||||
return false
|
||||
}
|
||||
// the scope must define targets to match on
|
||||
filterCat := s.Filter()
|
||||
filterCat := s.FilterCategory()
|
||||
targets := s.Get(filterCat)
|
||||
if len(targets) == 0 {
|
||||
return false
|
||||
@ -664,152 +672,3 @@ func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeI
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Contacts() {
|
||||
|
||||
sel.Exclude(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 1, len(scopes))
|
||||
require.Len(t, scopes, 1)
|
||||
|
||||
scope := scopes[0]
|
||||
assert.Equal(t, scope[ExchangeUser.String()], user)
|
||||
@ -277,7 +277,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Users() {
|
||||
|
||||
sel.Exclude(sel.Users([]string{u1, u2}))
|
||||
scopes := sel.Excludes
|
||||
require.Equal(t, 6, len(scopes))
|
||||
require.Len(t, scopes, 6)
|
||||
|
||||
for _, scope := range scopes {
|
||||
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}))
|
||||
scopes := sel.Includes
|
||||
require.Equal(t, 6, len(scopes))
|
||||
require.Len(t, scopes, 6)
|
||||
|
||||
for _, scope := range scopes {
|
||||
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) {
|
||||
scopes := setScopesToDefault(test.scope)
|
||||
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)
|
||||
var aMatch bool
|
||||
for _, scope := range scopes {
|
||||
if scope.matchesPath(ExchangeMail, path) {
|
||||
pv := ExchangeMail.pathValues(path)
|
||||
if matchesPathValues(scope, ExchangeMail, pv) {
|
||||
aMatch = true
|
||||
break
|
||||
}
|
||||
@ -781,7 +782,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
|
||||
func (suite *ExchangeSourceSuite) TestScopesByCategory() {
|
||||
var (
|
||||
es = NewExchangeRestore()
|
||||
users = es.Users(Any())
|
||||
@ -804,6 +805,11 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
|
||||
}
|
||||
return mss
|
||||
}
|
||||
cats := map[pathType]exchangeCategory{
|
||||
exchangeContactPath: ExchangeContact,
|
||||
exchangeEventPath: ExchangeEvent,
|
||||
exchangeMailPath: ExchangeMail,
|
||||
}
|
||||
table := []struct {
|
||||
name string
|
||||
scopes input
|
||||
@ -817,16 +823,16 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.T().Run(test.name, func(t *testing.T) {
|
||||
result := exchangeScopesByCategory(test.scopes)
|
||||
assert.Equal(t, test.expect.contact, len(result[ExchangeContact.String()]))
|
||||
assert.Equal(t, test.expect.event, len(result[ExchangeEvent.String()]))
|
||||
assert.Equal(t, test.expect.mail, len(result[ExchangeMail.String()]))
|
||||
result := scopesByCategory[ExchangeScope](test.scopes, cats)
|
||||
assert.Len(t, result[ExchangeContact], test.expect.contact)
|
||||
assert.Len(t, result[ExchangeEvent], test.expect.event)
|
||||
assert.Len(t, result[ExchangeMail], test.expect.mail)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
|
||||
var exchangeInfo *details.ExchangeInfo
|
||||
func (suite *ExchangeSourceSuite) TestPasses() {
|
||||
deets := details.DetailsEntry{}
|
||||
const (
|
||||
mid = "mailID"
|
||||
cat = ExchangeMail
|
||||
@ -867,7 +873,14 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
|
||||
}
|
||||
for _, test := range table {
|
||||
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
|
||||
}{
|
||||
{ExchangeCategoryUnknown, nil, map[categorizer]string{}},
|
||||
{ExchangeCategoryUnknown, []string{"a", "b", "c"}, map[categorizer]string{}},
|
||||
{ExchangeCategoryUnknown, []string{"a"}, map[categorizer]string{}},
|
||||
{ExchangeContact, contactPath, contactMap},
|
||||
{ExchangeEvent, eventPath, eventMap},
|
||||
{ExchangeMail, mailPath, mailMap},
|
||||
|
||||
@ -45,13 +45,12 @@ func (sc mockCategorizer) pathKeys() []categorizer {
|
||||
return []categorizer{rootCatStub, leafCatStub}
|
||||
}
|
||||
|
||||
// TODO: Uncomment when reducer func is added
|
||||
// func stubPathValues() map[categorizer]string {
|
||||
// return map[categorizer]string{
|
||||
// rootCatStub: rootCatStub.String(),
|
||||
// leafCatStub: leafCatStub.String(),
|
||||
// }
|
||||
// }
|
||||
func stubPathValues() map[categorizer]string {
|
||||
return map[categorizer]string{
|
||||
rootCatStub: rootCatStub.String(),
|
||||
leafCatStub: leafCatStub.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// scopers
|
||||
|
||||
@ -3,6 +3,7 @@ package selectors
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/corso/internal/common"
|
||||
"github.com/alcionai/corso/pkg/backup/details"
|
||||
)
|
||||
|
||||
@ -40,12 +41,11 @@ type (
|
||||
// so that the two can be compared.
|
||||
pathKeys() []categorizer
|
||||
}
|
||||
// TODO: Uncomment when reducer func is added
|
||||
// categoryT is the generic type interface of a categorizer
|
||||
// categoryT interface {
|
||||
// ~int
|
||||
// categorizer
|
||||
// }
|
||||
categoryT interface {
|
||||
~int
|
||||
categorizer
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
@ -157,3 +157,188 @@ func isAnyTarget[T scopeT](s T, cat categorizer) bool {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -4,7 +4,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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.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")
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
|
||||
p := sel.Printable()
|
||||
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{
|
||||
scope(stubScope("")),
|
||||
@ -68,7 +68,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
|
||||
p.Includes = nil
|
||||
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
|
||||
res = p.Resources()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user