diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 7886224fe..c4c283f9c 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -179,7 +179,7 @@ func makeExchangeScope(granularity string, cat exchangeCategory, vs []string) Ex func makeExchangeUserScope(user, granularity string, cat exchangeCategory, vs []string) ExchangeScope { es := makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user) es[scopeKeyResource] = user - es[scopeKeyDataType] = cat.dataType() + es[scopeKeyDataType] = cat.leafType().String() return es } @@ -301,7 +301,7 @@ func makeExchangeFilterScope(cat, filterCat exchangeCategory, vs []string) Excha scopeKeyCategory: cat.String(), scopeKeyInfoFilter: filterCat.String(), scopeKeyResource: Filter, - scopeKeyDataType: cat.dataType(), + scopeKeyDataType: cat.leafType().String(), filterCat.String(): join(vs...), } } @@ -383,17 +383,15 @@ func (d ExchangeDestination) Set(cat exchangeCategory, dest string) error { } // --------------------------------------------------------------------------- -// Scopes +// Categories // --------------------------------------------------------------------------- -type ( - // ExchangeScope specifies the data available - // when interfacing with the Exchange service. - ExchangeScope scope - // exchangeCategory enumerates the type of the lowest level - // of data () in a scope. - exchangeCategory int -) +// exchangeCategory enumerates the type of the lowest level +// of data specified by the scope. +type exchangeCategory int + +// interface compliance checks +var _ categorizer = ExchangeCategoryUnknown //go:generate stringer -type=exchangeCategory const ( @@ -441,35 +439,139 @@ func exchangeCatAtoI(s string) exchangeCategory { } } -// exchangeDataType returns the human-readable name of the core data type. -// Ex: ExchangeContactFolder.dataType() => ExchangeContact.String() -// Ex: ExchangeEvent.dataType() => ExchangeEvent.String(). -func (ec exchangeCategory) dataType() string { - switch ec { - case ExchangeContact, ExchangeContactFolder: - return ExchangeContact.String() - case ExchangeMail, ExchangeMailFolder: - return ExchangeMail.String() - } - return ec.String() +// exchangePathSet describes the category type keys used in Exchange paths. +// The order of each slice is important, and should match the order in which +// these types appear in the canonical Path for each type. +var categoryPathSet = map[categorizer][]categorizer{ + ExchangeContact: {ExchangeUser, ExchangeContactFolder, ExchangeContact}, + ExchangeEvent: {ExchangeUser, ExchangeEvent}, + ExchangeMail: {ExchangeUser, ExchangeMailFolder, ExchangeMail}, + ExchangeUser: {ExchangeUser}, // the root category must be represented } -// Granularity describes the granularity (directory || item) -// of the data in scope -func (s ExchangeScope) Granularity() string { - return s[scopeKeyGranularity] +// leafType returns the leaf category of the receiver. +// If the receiver category has multiple leaves (ex: User) or no leaves, +// (ex: Unknown), the receiver itself is returned. +// Ex: ExchangeContactFolder.leafType() => ExchangeContact +// Ex: ExchangeEvent.leafType() => ExchangeEvent +// Ex: ExchangeUser.leafType() => ExchangeUser +func (ec exchangeCategory) leafType() exchangeCategory { + switch ec { + case ExchangeContact, ExchangeContactFolder: + return ExchangeContact + case ExchangeMail, ExchangeMailFolder: + return ExchangeMail + } + return ec } +// isType checks if either the receiver is a supertype of the parameter, +// or if the parameter is a supertype of the receiver. +// if either value is an unknown types, the comparison is always false. +// if either value is the root type (user), the comparison is always true. +func (ec exchangeCategory) isType(cat exchangeCategory) bool { + if cat == ExchangeCategoryUnknown || ec == ExchangeCategoryUnknown { + return false + } + if cat == ExchangeUser || ec == ExchangeUser { + return true + } + return ec.leafType() == cat.leafType() +} + +// includesType returns true if it matches the isType check for +// the receiver's service category. +func (ec exchangeCategory) includesType(cat categorizer) bool { + c, ok := cat.(exchangeCategory) + if !ok { + return false + } + return ec.isType(c) +} + +// transforms a path to a map of identified properties. +// TODO: this should use service-specific funcs in the Paths pkg. Instead of +// peeking at the path directly, the caller should compare against values like +// path.UserID() and path.Folders(). +// +// Malformed (ie, short len) paths will return incomplete results. +// Example: +// [tenantID, userID, "mail", mailFolder, mailID] +// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} +func (ec exchangeCategory) pathValues(path []string) map[categorizer]string { + m := map[categorizer]string{} + if len(path) < 4 { + return m + } + m[ExchangeUser] = path[1] + /* + TODO/Notice: + Mail and Contacts contain folder structures, identified + in this code as being at index 3. This assumes a single + folder, while in reality users can express subfolder + hierarchies of arbirary depth. Subfolder handling is coming + at a later time. + */ + switch ec { + case ExchangeContact: + if len(path) < 5 { + return m + } + m[ExchangeContactFolder] = path[3] + m[ExchangeContact] = path[4] + case ExchangeEvent: + if len(path) < 4 { + return m + } + m[ExchangeEvent] = path[3] + case ExchangeMail: + if len(path) < 5 { + return m + } + m[ExchangeMailFolder] = path[3] + m[ExchangeMail] = path[4] + } + return m +} + +// pathKeys returns a map of the path types, keyed by their leaf type, +func (ec exchangeCategory) pathKeys() []categorizer { + return categoryPathSet[ec.leafType()] +} + +// --------------------------------------------------------------------------- +// Scopes +// --------------------------------------------------------------------------- + +// ExchangeScope specifies the data available +// when interfacing with the Exchange service. +type ExchangeScope scope + +// interface compliance checks +// var _ scoper = &ExchangeScope{} + // Category describes the type of the data in scope. func (s ExchangeScope) Category() exchangeCategory { return exchangeCatAtoI(s[scopeKeyCategory]) } +// categorizer type is a generic wrapper around Category. +// Primarily used by scopes.go to for abstract comparisons. +func (s ExchangeScope) categorizer() categorizer { + return s.Category() +} + // Filer describes the specific filter, and its target values. func (s ExchangeScope) Filter() exchangeCategory { return exchangeCatAtoI(s[scopeKeyInfoFilter]) } +// Granularity describes the granularity (directory || item) +// of the data in scope +func (s ExchangeScope) Granularity() string { + return s[scopeKeyGranularity] +} + // IncludeCategory checks whether the scope includes a // certain category of data. // Ex: to check if the scope includes mail data: @@ -528,13 +630,6 @@ func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope { return s } -var categoryPathSet = map[exchangeCategory][]exchangeCategory{ - ExchangeContact: {ExchangeUser, ExchangeContactFolder, ExchangeContact}, - ExchangeEvent: {ExchangeUser, ExchangeEvent}, - ExchangeMail: {ExchangeUser, ExchangeMailFolder, ExchangeMail}, - ExchangeUser: {ExchangeUser}, -} - // 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) @@ -586,9 +681,9 @@ func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeI // 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 := exchangeIDPath(cat, path) + pathIDs := cat.pathValues(path) for _, c := range categoryPathSet[cat] { - target := s.Get(c) + target := s.Get(c.(exchangeCategory)) // the scope must define the targets to match on if len(target) == 0 { return false @@ -617,47 +712,6 @@ func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool { // Restore Point Filtering // --------------------------------------------------------------------------- -// transforms a path to a map of identified properties. -// Malformed (ie, short len) paths will return incomplete results. -// Example: -// [tenantID, userID, "mail", mailFolder, mailID] -// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} -func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]string { - m := map[exchangeCategory]string{} - if len(path) == 0 { - return m - } - m[ExchangeUser] = path[1] - /* - TODO/Notice: - Mail and Contacts contain folder structures, identified - in this code as being at index 3. This assumes a single - folder, while in reality users can express subfolder - hierarchies of arbirary depth. Subfolder handling is coming - at a later time. - */ - switch cat { - case ExchangeContact: - if len(path) < 5 { - return m - } - m[ExchangeContactFolder] = path[3] - m[ExchangeContact] = path[4] - case ExchangeEvent: - if len(path) < 4 { - return m - } - m[ExchangeEvent] = path[3] - case ExchangeMail: - if len(path) < 5 { - return m - } - m[ExchangeMailFolder] = path[3] - m[ExchangeMail] = path[4] - } - return m -} - // 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 { diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index ef27791ac..dd7534b3e 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -942,3 +942,144 @@ func (suite *ExchangeSourceSuite) TestIsAny() { }) } } + +func (suite *ExchangeSourceSuite) TestExchangeCategory_LeafType() { + table := []struct { + cat exchangeCategory + expect exchangeCategory + }{ + {exchangeCategory(-1), exchangeCategory(-1)}, + {ExchangeCategoryUnknown, ExchangeCategoryUnknown}, + {ExchangeUser, ExchangeUser}, + {ExchangeMailFolder, ExchangeMail}, + {ExchangeMail, ExchangeMail}, + {ExchangeContactFolder, ExchangeContact}, + {ExchangeEvent, ExchangeEvent}, + } + for _, test := range table { + suite.T().Run(test.cat.String(), func(t *testing.T) { + assert.Equal(t, test.expect, test.cat.leafType(), test.cat.String()) + }) + } +} + +func (suite *ExchangeSourceSuite) TestExchangeCategory_IsType() { + table := []struct { + cat exchangeCategory + input exchangeCategory + expect assert.BoolAssertionFunc + }{ + // technically this should be false, but we're not reducing fabricated + // exchange category values to unknown. Since the type is unexported, + // that transformation would be unnecessary. + {exchangeCategory(-1), exchangeCategory(-1), assert.True}, + {ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.False}, + {ExchangeUser, ExchangeCategoryUnknown, assert.False}, + {ExchangeCategoryUnknown, ExchangeUser, assert.False}, + {ExchangeUser, ExchangeUser, assert.True}, + {ExchangeMailFolder, ExchangeMail, assert.True}, + {ExchangeMailFolder, ExchangeContact, assert.False}, + {ExchangeContactFolder, ExchangeMail, assert.False}, + {ExchangeMail, ExchangeMail, assert.True}, + {ExchangeContactFolder, ExchangeContact, assert.True}, + {ExchangeContactFolder, ExchangeEvent, assert.False}, + {ExchangeEvent, ExchangeContact, assert.False}, + {ExchangeEvent, ExchangeEvent, assert.True}, + } + for _, test := range table { + suite.T().Run(test.cat.String(), func(t *testing.T) { + test.expect(t, test.cat.isType(test.input)) + }) + } +} + +func (suite *ExchangeSourceSuite) TestExchangeCategory_IncludesType() { + table := []struct { + cat categorizer + input categorizer + expect assert.BoolAssertionFunc + }{ + // technically this should be false, but we're not reducing fabricated + // exchange category values to unknown. Since the type is unexported, + // that transformation would be unnecessary. + {exchangeCategory(-1), exchangeCategory(-1), assert.True}, + {ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.False}, + {ExchangeUser, ExchangeCategoryUnknown, assert.False}, + {ExchangeCategoryUnknown, ExchangeUser, assert.False}, + {ExchangeUser, ExchangeUser, assert.True}, + {ExchangeMailFolder, ExchangeMail, assert.True}, + {ExchangeMailFolder, ExchangeContact, assert.False}, + {ExchangeContactFolder, ExchangeMail, assert.False}, + {ExchangeMail, ExchangeMail, assert.True}, + {ExchangeContactFolder, ExchangeContact, assert.True}, + {ExchangeContactFolder, ExchangeEvent, assert.False}, + {ExchangeEvent, ExchangeContact, assert.False}, + {ExchangeEvent, ExchangeEvent, assert.True}, + {ExchangeUser, rootCatStub, assert.False}, + {rootCatStub, ExchangeUser, assert.False}, + } + for _, test := range table { + suite.T().Run(test.cat.String(), func(t *testing.T) { + test.expect(t, test.cat.includesType(test.input)) + }) + } +} + +func (suite *ExchangeSourceSuite) TestExchangeCategory_PathValues() { + contactPath := []string{"ten", "user", "contact", "cfolder", "contactitem"} + contactMap := map[categorizer]string{ + ExchangeUser: contactPath[1], + ExchangeContactFolder: contactPath[3], + ExchangeContact: contactPath[4], + } + eventPath := []string{"ten", "user", "event", "eventitem"} + eventMap := map[categorizer]string{ + ExchangeUser: eventPath[1], + ExchangeEvent: eventPath[3], + } + mailPath := []string{"ten", "user", "mail", "mfolder", "mailitem"} + mailMap := map[categorizer]string{ + ExchangeUser: contactPath[1], + ExchangeMailFolder: mailPath[3], + ExchangeMail: mailPath[4], + } + table := []struct { + cat exchangeCategory + path []string + expect map[categorizer]string + }{ + {ExchangeCategoryUnknown, nil, map[categorizer]string{}}, + {ExchangeCategoryUnknown, []string{"a", "b", "c"}, map[categorizer]string{}}, + {ExchangeContact, contactPath, contactMap}, + {ExchangeEvent, eventPath, eventMap}, + {ExchangeMail, mailPath, mailMap}, + } + for _, test := range table { + suite.T().Run(test.cat.String(), func(t *testing.T) { + assert.Equal(t, test.cat.pathValues(test.path), test.expect) + }) + } +} + +func (suite *ExchangeSourceSuite) TestExchangeCategory_PathKeys() { + contact := []categorizer{ExchangeUser, ExchangeContactFolder, ExchangeContact} + event := []categorizer{ExchangeUser, ExchangeEvent} + mail := []categorizer{ExchangeUser, ExchangeMailFolder, ExchangeMail} + user := []categorizer{ExchangeUser} + var empty []categorizer + table := []struct { + cat exchangeCategory + expect []categorizer + }{ + {ExchangeCategoryUnknown, empty}, + {ExchangeContact, contact}, + {ExchangeEvent, event}, + {ExchangeMail, mail}, + {ExchangeUser, user}, + } + for _, test := range table { + suite.T().Run(test.cat.String(), func(t *testing.T) { + assert.Equal(t, test.cat.pathKeys(), test.expect) + }) + } +}