diff --git a/src/cli/backup/sharepoint_test.go b/src/cli/backup/sharepoint_test.go index 648d3e8c4..8fadd064e 100644 --- a/src/cli/backup/sharepoint_test.go +++ b/src/cli/backup/sharepoint_test.go @@ -163,12 +163,11 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() { ) table := []struct { - name string - site []string - weburl []string - data []string - expect []string - expectScopesLen int + name string + site []string + weburl []string + data []string + expect []string }{ { name: "no sites or urls", @@ -181,63 +180,54 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() { expect: selectors.None(), }, { - name: "site wildcard", - site: []string{flags.Wildcard}, - expect: bothIDs, - expectScopesLen: 2, + name: "site wildcard", + site: []string{flags.Wildcard}, + expect: bothIDs, }, { - name: "url wildcard", - weburl: []string{flags.Wildcard}, - expect: bothIDs, - expectScopesLen: 2, + name: "url wildcard", + weburl: []string{flags.Wildcard}, + expect: bothIDs, }, { - name: "sites", - site: []string{id1, id2}, - expect: []string{id1, id2}, - expectScopesLen: 2, + name: "sites", + site: []string{id1, id2}, + expect: []string{id1, id2}, }, { - name: "urls", - weburl: []string{url1, url2}, - expect: []string{url1, url2}, - expectScopesLen: 2, + name: "urls", + weburl: []string{url1, url2}, + expect: []string{url1, url2}, }, { - name: "mix sites and urls", - site: []string{id1}, - weburl: []string{url2}, - expect: []string{id1, url2}, - expectScopesLen: 2, + name: "mix sites and urls", + site: []string{id1}, + weburl: []string{url2}, + expect: []string{id1, url2}, }, { - name: "duplicate sites and urls", - site: []string{id1, id2}, - weburl: []string{url1, url2}, - expect: []string{id1, id2, url1, url2}, - expectScopesLen: 2, + name: "duplicate sites and urls", + site: []string{id1, id2}, + weburl: []string{url1, url2}, + expect: []string{id1, id2, url1, url2}, }, { - name: "unnecessary site wildcard", - site: []string{id1, flags.Wildcard}, - weburl: []string{url1, url2}, - expect: bothIDs, - expectScopesLen: 2, + name: "unnecessary site wildcard", + site: []string{id1, flags.Wildcard}, + weburl: []string{url1, url2}, + expect: bothIDs, }, { - name: "unnecessary url wildcard", - site: []string{id1}, - weburl: []string{url1, flags.Wildcard}, - expect: bothIDs, - expectScopesLen: 2, + name: "unnecessary url wildcard", + site: []string{id1}, + weburl: []string{url1, flags.Wildcard}, + expect: bothIDs, }, { - name: "Pages", - site: bothIDs, - data: []string{dataPages}, - expect: bothIDs, - expectScopesLen: 1, + name: "Pages", + site: bothIDs, + data: []string{dataPages}, + expect: bothIDs, }, } for _, test := range table { @@ -249,7 +239,7 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() { sel, err := sharePointBackupCreateSelectors(ctx, ins, test.site, test.weburl, test.data) require.NoError(t, err, clues.ToCore(err)) - assert.ElementsMatch(t, test.expect, sel.DiscreteResourceOwners()) + assert.ElementsMatch(t, test.expect, sel.ResourceOwners.Targets) }) } } diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 008134559..245909161 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -69,7 +69,7 @@ func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) { } func (s ExchangeBackup) SplitByResourceOwner(users []string) []ExchangeBackup { - sels := splitByResourceOwner[ExchangeScope](s.Selector, users, ExchangeUser) + sels := splitByProtectedResource[ExchangeScope](s.Selector, users, ExchangeUser) ss := make([]ExchangeBackup, 0, len(sels)) for _, sel := range sels { @@ -103,7 +103,7 @@ func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) { } func (sr ExchangeRestore) SplitByResourceOwner(users []string) []ExchangeRestore { - sels := splitByResourceOwner[ExchangeScope](sr.Selector, users, ExchangeUser) + sels := splitByProtectedResource[ExchangeScope](sr.Selector, users, ExchangeUser) ss := make([]ExchangeRestore, 0, len(sels)) for _, sel := range sels { diff --git a/src/pkg/selectors/groups.go b/src/pkg/selectors/groups.go index 8051275d9..cc4a7ebfd 100644 --- a/src/pkg/selectors/groups.go +++ b/src/pkg/selectors/groups.go @@ -66,7 +66,7 @@ func (s Selector) ToGroupsBackup() (*GroupsBackup, error) { } func (s GroupsBackup) SplitByResourceOwner(resources []string) []GroupsBackup { - sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup) + sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup) ss := make([]GroupsBackup, 0, len(sels)) for _, sel := range sels { @@ -100,7 +100,7 @@ func (s Selector) ToGroupsRestore() (*GroupsRestore, error) { } func (s GroupsRestore) SplitByResourceOwner(resources []string) []GroupsRestore { - sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup) + sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup) ss := make([]GroupsRestore, 0, len(sels)) for _, sel := range sels { diff --git a/src/pkg/selectors/onedrive.go b/src/pkg/selectors/onedrive.go index 18fa0fca3..057634215 100644 --- a/src/pkg/selectors/onedrive.go +++ b/src/pkg/selectors/onedrive.go @@ -68,7 +68,7 @@ func (s Selector) ToOneDriveBackup() (*OneDriveBackup, error) { } func (s OneDriveBackup) SplitByResourceOwner(users []string) []OneDriveBackup { - sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser) + sels := splitByProtectedResource[OneDriveScope](s.Selector, users, OneDriveUser) ss := make([]OneDriveBackup, 0, len(sels)) for _, sel := range sels { @@ -102,7 +102,7 @@ func (s Selector) ToOneDriveRestore() (*OneDriveRestore, error) { } func (s OneDriveRestore) SplitByResourceOwner(users []string) []OneDriveRestore { - sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser) + sels := splitByProtectedResource[OneDriveScope](s.Selector, users, OneDriveUser) ss := make([]OneDriveRestore, 0, len(sels)) for _, sel := range sels { diff --git a/src/pkg/selectors/onedrive_test.go b/src/pkg/selectors/onedrive_test.go index aeb2f19cd..be8553bcd 100644 --- a/src/pkg/selectors/onedrive_test.go +++ b/src/pkg/selectors/onedrive_test.go @@ -43,16 +43,12 @@ func (suite *OneDriveSelectorSuite) TestToOneDriveBackup() { } func (suite *OneDriveSelectorSuite) TestOneDriveSelector_AllData() { - t := suite.T() - var ( users = []string{"u1", "u2"} sel = NewOneDriveBackup(users) allScopes = sel.AllData() ) - assert.ElementsMatch(t, users, sel.DiscreteResourceOwners()) - // Initialize the selector Include, Exclude, Filter sel.Exclude(allScopes) sel.Include(allScopes) diff --git a/src/pkg/selectors/scopes.go b/src/pkg/selectors/scopes.go index aebd0f156..5a453bf4f 100644 --- a/src/pkg/selectors/scopes.go +++ b/src/pkg/selectors/scopes.go @@ -161,6 +161,267 @@ type ( } ) +// appendScopes iterates through each scope in the list of scope slices, +// calling setDefaults() to ensure it is completely populated, and appends +// those scopes to the `to` slice. +func appendScopes[T scopeT](to []scope, scopes ...[]T) []scope { + if len(to) == 0 { + to = []scope{} + } + + for _, scopeSl := range scopes { + for _, s := range scopeSl { + s.setDefaults() + to = append(to, scope(s)) + } + } + + return to +} + +// scopes retrieves the list of scopes in the selector. +func scopes[T scopeT](s Selector) []T { + scopes := []T{} + + for _, v := range s.Includes { + scopes = append(scopes, T(v)) + } + + return scopes +} + +// --------------------------------------------------------------------------- +// scope config & constructors +// --------------------------------------------------------------------------- + +// constructs the default item-scope comparator options according +// to the selector configuration. +// - if cfg.OnlyMatchItemNames == false, then comparison assumes item IDs, +// which are case sensitive, resulting in StrictEqualsMatch +func defaultItemOptions(cfg Config) []option { + opts := []option{} + + if !cfg.OnlyMatchItemNames { + opts = append(opts, StrictEqualMatch()) + } + + return opts +} + +type scopeConfig struct { + usePathFilter bool + usePrefixFilter bool + useSuffixFilter bool + useEqualsFilter bool + useStrictEqualsFilter bool +} + +type option func(*scopeConfig) + +func (sc *scopeConfig) populate(opts ...option) { + for _, opt := range opts { + opt(sc) + } +} + +// PrefixMatch ensures the selector uses a Prefix comparator, instead +// of contains or equals. Will not override a default Any() or None() +// comparator. +func PrefixMatch() option { + return func(sc *scopeConfig) { + sc.usePrefixFilter = true + } +} + +// SuffixMatch ensures the selector uses a Suffix comparator, instead +// of contains or equals. Will not override a default Any() or None() +// comparator. +func SuffixMatch() option { + return func(sc *scopeConfig) { + sc.useSuffixFilter = true + } +} + +// StrictEqualsMatch ensures the selector uses a StrictEquals comparator, instead +// of contains. Will not override a default Any() or None() comparator. +func StrictEqualMatch() option { + return func(sc *scopeConfig) { + sc.useStrictEqualsFilter = true + } +} + +// ExactMatch ensures the selector uses an Equals comparator, instead +// of contains. Will not override a default Any() or None() comparator. +func ExactMatch() option { + return func(sc *scopeConfig) { + sc.useEqualsFilter = true + } +} + +// pathComparator is an internal-facing option. It is assumed that scope +// constructors will provide the pathComparator option whenever a folder- +// level scope (ie, a scope that compares path hierarchies) is created. +func pathComparator() option { + return func(sc *scopeConfig) { + sc.usePathFilter = true + } +} + +func badCastErr(cast, is service) error { + return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is))) +} + +// if the provided slice contains Any, returns [Any] +// if the slice contains None, returns [None] +// if the slice contains Any and None, returns the first +// if the slice is empty, returns [None] +// otherwise returns the input +func clean(s []string) []string { + if len(s) == 0 { + return None() + } + + for _, e := range s { + if e == AnyTgt { + return Any() + } + + if e == NoneTgt { + return None() + } + } + + return s +} + +type filterFunc func([]string) filters.Filter + +// 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 scopeConfig specifies a filter, use that filter. +// if the input is len(1), returns an Equals filter. +// otherwise returns a Contains filter. +func filterFor(sc scopeConfig, targets ...string) filters.Filter { + return filterize(sc, nil, targets...) +} + +// 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 scopeConfig specifies a filter, use that filter. +// if defaultFilter is non-nil, returns that filter. +// if the input is len(1), returns an Equals filter. +// otherwise returns a Contains filter. +func filterize( + sc scopeConfig, + defaultFilter filterFunc, + targets ...string, +) filters.Filter { + targets = clean(targets) + + if len(targets) == 0 || targets[0] == NoneTgt { + return failAny + } + + if targets[0] == AnyTgt { + return passAny + } + + if sc.usePathFilter { + if sc.useEqualsFilter { + return filters.PathEquals(targets) + } + + if sc.usePrefixFilter { + return filters.PathPrefix(targets) + } + + if sc.useSuffixFilter { + return filters.PathSuffix(targets) + } + + return filters.PathContains(targets) + } + + if sc.usePrefixFilter { + return filters.Prefix(targets) + } + + if sc.useSuffixFilter { + return filters.Suffix(targets) + } + + if sc.useStrictEqualsFilter { + return filters.StrictEqual(targets) + } + + if defaultFilter != nil { + return defaultFilter(targets) + } + + return filters.Equal(targets) +} + +// pathFilterFactory returns the appropriate path filter +// (contains, prefix, or suffix) for the provided options. +// If multiple options are flagged, Prefix takes priority. +// If no options are provided, returns PathContains. +func pathFilterFactory(opts ...option) filterFunc { + sc := &scopeConfig{} + sc.populate(opts...) + + var ff filterFunc + + switch true { + case sc.usePrefixFilter: + ff = filters.PathPrefix + case sc.useSuffixFilter: + ff = filters.PathSuffix + case sc.useEqualsFilter: + ff = filters.PathEquals + default: + ff = filters.PathContains + } + + return wrapSliceFilter(ff) +} + +func wrapSliceFilter(ff filterFunc) filterFunc { + return func(s []string) filters.Filter { + s = clean(s) + + if f, ok := isAnyOrNone(s); ok { + return f + } + + return ff(s) + } +} + +// returns (, true) if s is len==1 and s[0] is +// anyTgt or noneTgt, implying that the caller should use +// the returned filter. On (, false), the caller +// can ignore the returned filter. +// a special case exists for len(s)==0, interpreted as +// "noneTgt" +func isAnyOrNone(s []string) (filters.Filter, bool) { + switch len(s) { + case 0: + return failAny, true + + case 1: + switch s[0] { + case AnyTgt: + return passAny, true + case NoneTgt: + return failAny, true + } + } + + return failAny, false +} + // makeScope produces a well formatted, typed scope that ensures all base values are populated. func makeScope[T scopeT]( cat categorizer, @@ -239,95 +500,9 @@ func marshalScope(mss map[string]string) string { } // --------------------------------------------------------------------------- -// scope funcs +// reducer & filtering // --------------------------------------------------------------------------- -// matches returns true if the category is included in the scope's -// data type, and the input string passes the scope's filter for -// that category. -func matches[T scopeT, C categoryT](s T, cat C, inpt string) bool { - if !typeAndCategoryMatches(cat, s.categorizer()) { - return false - } - - if len(inpt) == 0 { - return false - } - - return s[cat.String()].Compare(inpt) -} - -// matchesAny returns true if the category is included in the scope's -// data type, and any one of the input strings passes the scope's filter. -func matchesAny[T scopeT, C categoryT](s T, cat C, inpts []string) bool { - if !typeAndCategoryMatches(cat, s.categorizer()) { - return false - } - - if len(inpts) == 0 { - return false - } - - return s[cat.String()].CompareAny(inpts...) -} - -// getCategory returns the scope's category value. -// if s is an info-type scope, returns the info category. -func getCategory[T scopeT](s T) string { - return s[scopeKeyCategory].Identity -} - -// getInfoCategory returns the scope's infoFilter category value. -func getInfoCategory[T scopeT](s T) string { - return s[scopeKeyInfoCategory].Identity -} - -// getCatValue takes the value of s[cat] and returns the slice. -// If s[cat] is nil, returns None(). -func getCatValue[T scopeT](s T, cat categorizer) []string { - filt, ok := s[cat.String()] - if !ok { - return None() - } - - if len(filt.Targets) > 0 { - return filt.Targets - } - - return filt.Targets -} - -// set sets a value by category to the scope. Only intended for internal -// use, not for exporting to callers. -func set[T scopeT](s T, cat categorizer, v []string, opts ...option) T { - sc := &scopeConfig{} - sc.populate(opts...) - - s[cat.String()] = filterFor(*sc, v...) - - return s -} - -// returns true if the category is included in the scope's category type, -// and the value is set to None(). -func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool { - if !typeAndCategoryMatches(cat, s.categorizer()) { - return false - } - - return s[cat.String()].Comparator == filters.Fails -} - -// returns true if the category is included in the scope's category type, -// and the value is set to Any(). -func isAnyTarget[T scopeT, C categoryT](s T, cat C) bool { - if !typeAndCategoryMatches(cat, s.categorizer()) { - return false - } - - return s[cat.String()].Comparator == filters.Passes -} - // 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]( @@ -542,6 +717,92 @@ func matchesPathValues[T scopeT, C categoryT]( // helper funcs // --------------------------------------------------------------------------- +// matches returns true if the category is included in the scope's +// data type, and the input string passes the scope's filter for +// that category. +func matches[T scopeT, C categoryT](s T, cat C, inpt string) bool { + if !typeAndCategoryMatches(cat, s.categorizer()) { + return false + } + + if len(inpt) == 0 { + return false + } + + return s[cat.String()].Compare(inpt) +} + +// matchesAny returns true if the category is included in the scope's +// data type, and any one of the input strings passes the scope's filter. +func matchesAny[T scopeT, C categoryT](s T, cat C, inpts []string) bool { + if !typeAndCategoryMatches(cat, s.categorizer()) { + return false + } + + if len(inpts) == 0 { + return false + } + + return s[cat.String()].CompareAny(inpts...) +} + +// getCategory returns the scope's category value. +// if s is an info-type scope, returns the info category. +func getCategory[T scopeT](s T) string { + return s[scopeKeyCategory].Identity +} + +// getInfoCategory returns the scope's infoFilter category value. +func getInfoCategory[T scopeT](s T) string { + return s[scopeKeyInfoCategory].Identity +} + +// getCatValue takes the value of s[cat] and returns the slice. +// If s[cat] is nil, returns None(). +func getCatValue[T scopeT](s T, cat categorizer) []string { + filt, ok := s[cat.String()] + if !ok { + return None() + } + + if len(filt.Targets) > 0 { + return filt.Targets + } + + return filt.Targets +} + +// set sets a value by category to the scope. Only intended for internal +// use, not for exporting to callers. +func set[T scopeT](s T, cat categorizer, v []string, opts ...option) T { + sc := &scopeConfig{} + sc.populate(opts...) + + s[cat.String()] = filterFor(*sc, v...) + + return s +} + +// returns true if the category is included in the scope's category type, +// and the value is set to None(). +func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool { + if !typeAndCategoryMatches(cat, s.categorizer()) { + return false + } + + return s[cat.String()].Comparator == filters.Fails +} + +// returns true if the category is included in the scope's category type, +// and the value is set to Any(). +func isAnyTarget[T scopeT, C categoryT](s T, cat C) bool { + if !typeAndCategoryMatches(cat, s.categorizer()) { + return false + } + + return s[cat.String()].Comparator == filters.Passes +} + // categoryMatches returns true if: // - neither type is 'unknown' // - either type is the root type diff --git a/src/pkg/selectors/selectors.go b/src/pkg/selectors/selectors.go index 1707f877f..474ab60f5 100644 --- a/src/pkg/selectors/selectors.go +++ b/src/pkg/selectors/selectors.go @@ -159,11 +159,25 @@ func (s *Selector) Configure(cfg Config) { s.Cfg = cfg } -// DiscreteResourceOwners returns the list of individual resourceOwners used -// in the selector. -// TODO(rkeepers): remove in favor of split and s.DiscreteOwner -func (s Selector) DiscreteResourceOwners() []string { - return s.ResourceOwners.Targets +// --------------------------------------------------------------------------- +// protected resources & idname provider compliance +// --------------------------------------------------------------------------- + +var _ idname.Provider = &Selector{} + +// ID returns s.discreteOwner, which is assumed to be a stable ID. +func (s Selector) ID() string { + return s.DiscreteOwner +} + +// Name returns s.discreteOwnerName. If that value is empty, it returns +// s.DiscreteOwner instead. +func (s Selector) Name() string { + if len(s.DiscreteOwnerName) == 0 { + return s.DiscreteOwner + } + + return s.DiscreteOwnerName } // SetDiscreteOwnerIDName ensures the selector has the correct discrete owner @@ -193,32 +207,17 @@ func (s Selector) SetDiscreteOwnerIDName(id, name string) Selector { return r } -// ID returns s.discreteOwner, which is assumed to be a stable ID. -func (s Selector) ID() string { - return s.DiscreteOwner -} - -// Name returns s.discreteOwnerName. If that value is empty, it returns -// s.DiscreteOwner instead. -func (s Selector) Name() string { - if len(s.DiscreteOwnerName) == 0 { - return s.DiscreteOwner - } - - return s.DiscreteOwnerName -} - -// isAnyResourceOwner returns true if the selector includes all resource owners. -func isAnyResourceOwner(s Selector) bool { +// isAnyProtectedResource returns true if the selector includes all resource owners. +func isAnyProtectedResource(s Selector) bool { return s.ResourceOwners.Comparator == filters.Passes } -// isNoneResourceOwner returns true if the selector includes no resource owners. -func isNoneResourceOwner(s Selector) bool { +// isNoneProtectedResource returns true if the selector includes no resource owners. +func isNoneProtectedResource(s Selector) bool { return s.ResourceOwners.Comparator == filters.Fails } -// SplitByResourceOwner makes one shallow clone for each resourceOwner in the +// splitByProtectedResource makes one shallow clone for each resourceOwner in the // selector, specifying a new DiscreteOwner for each one. // If the original selector already specified a discrete slice of resource owners, // only those owners are used in the result. @@ -230,14 +229,14 @@ func isNoneResourceOwner(s Selector) bool { // // temporarily, clones all scopes in each selector and replaces the owners with // the discrete owner. -func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string, rootCat C) []Selector { - if isNoneResourceOwner(s) { +func splitByProtectedResource[T scopeT, C categoryT](s Selector, allOwners []string, rootCat C) []Selector { + if isNoneProtectedResource(s) { return []Selector{} } targets := allOwners - if !isAnyResourceOwner(s) { + if !isAnyProtectedResource(s) { targets = s.ResourceOwners.Targets } @@ -252,35 +251,6 @@ func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string, return ss } -// appendScopes iterates through each scope in the list of scope slices, -// calling setDefaults() to ensure it is completely populated, and appends -// those scopes to the `to` slice. -func appendScopes[T scopeT](to []scope, scopes ...[]T) []scope { - if len(to) == 0 { - to = []scope{} - } - - for _, scopeSl := range scopes { - for _, s := range scopeSl { - s.setDefaults() - to = append(to, scope(s)) - } - } - - return to -} - -// scopes retrieves the list of scopes in the selector. -func scopes[T scopeT](s Selector) []T { - scopes := []T{} - - for _, v := range s.Includes { - scopes = append(scopes, T(v)) - } - - return scopes -} - // Returns the path.ServiceType matching the selector service. func (s Selector) PathService() path.ServiceType { return serviceToPathType[s.Service] @@ -331,6 +301,9 @@ func selectorAsIface[T any](s Selector) (T, error) { case ServiceSharePoint: a, err = func() (any, error) { return s.ToSharePointRestore() }() t = a.(T) + case ServiceGroups: + a, err = func() (any, error) { return s.ToGroupsRestore() }() + t = a.(T) default: err = clues.Stack(ErrorUnrecognizedService, clues.New(s.Service.String())) } @@ -420,28 +393,6 @@ func (ls loggableSelector) marshal() string { // helpers // --------------------------------------------------------------------------- -// produces the discrete set of resource owners in the slice of scopes. -// Any and None values are discarded. -func resourceOwnersIn(s []scope, rootCat string) []string { - rm := map[string]struct{}{} - - for _, sc := range s { - for _, v := range sc[rootCat].Targets { - rm[v] = struct{}{} - } - } - - rs := []string{} - - for k := range rm { - if k != AnyTgt && k != NoneTgt { - rs = append(rs, k) - } - } - - return rs -} - // produces the discrete set of path categories in the slice of scopes. func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType { m := map[path.CategoryType]struct{}{} @@ -459,235 +410,3 @@ func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType { return maps.Keys(m) } - -// --------------------------------------------------------------------------- -// scope constructors -// --------------------------------------------------------------------------- - -// constructs the default item-scope comparator options according -// to the selector configuration. -// - if cfg.OnlyMatchItemNames == false, then comparison assumes item IDs, -// which are case sensitive, resulting in StrictEqualsMatch -func defaultItemOptions(cfg Config) []option { - opts := []option{} - - if !cfg.OnlyMatchItemNames { - opts = append(opts, StrictEqualMatch()) - } - - return opts -} - -type scopeConfig struct { - usePathFilter bool - usePrefixFilter bool - useSuffixFilter bool - useEqualsFilter bool - useStrictEqualsFilter bool -} - -type option func(*scopeConfig) - -func (sc *scopeConfig) populate(opts ...option) { - for _, opt := range opts { - opt(sc) - } -} - -// PrefixMatch ensures the selector uses a Prefix comparator, instead -// of contains or equals. Will not override a default Any() or None() -// comparator. -func PrefixMatch() option { - return func(sc *scopeConfig) { - sc.usePrefixFilter = true - } -} - -// SuffixMatch ensures the selector uses a Suffix comparator, instead -// of contains or equals. Will not override a default Any() or None() -// comparator. -func SuffixMatch() option { - return func(sc *scopeConfig) { - sc.useSuffixFilter = true - } -} - -// StrictEqualsMatch ensures the selector uses a StrictEquals comparator, instead -// of contains. Will not override a default Any() or None() comparator. -func StrictEqualMatch() option { - return func(sc *scopeConfig) { - sc.useStrictEqualsFilter = true - } -} - -// ExactMatch ensures the selector uses an Equals comparator, instead -// of contains. Will not override a default Any() or None() comparator. -func ExactMatch() option { - return func(sc *scopeConfig) { - sc.useEqualsFilter = true - } -} - -// pathComparator is an internal-facing option. It is assumed that scope -// constructors will provide the pathComparator option whenever a folder- -// level scope (ie, a scope that compares path hierarchies) is created. -func pathComparator() option { - return func(sc *scopeConfig) { - sc.usePathFilter = true - } -} - -func badCastErr(cast, is service) error { - return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is))) -} - -// if the provided slice contains Any, returns [Any] -// if the slice contains None, returns [None] -// if the slice contains Any and None, returns the first -// if the slice is empty, returns [None] -// otherwise returns the input -func clean(s []string) []string { - if len(s) == 0 { - return None() - } - - for _, e := range s { - if e == AnyTgt { - return Any() - } - - if e == NoneTgt { - return None() - } - } - - return s -} - -type filterFunc func([]string) filters.Filter - -// 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 scopeConfig specifies a filter, use that filter. -// if the input is len(1), returns an Equals filter. -// otherwise returns a Contains filter. -func filterFor(sc scopeConfig, targets ...string) filters.Filter { - return filterize(sc, nil, targets...) -} - -// 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 scopeConfig specifies a filter, use that filter. -// if defaultFilter is non-nil, returns that filter. -// if the input is len(1), returns an Equals filter. -// otherwise returns a Contains filter. -func filterize( - sc scopeConfig, - defaultFilter filterFunc, - targets ...string, -) filters.Filter { - targets = clean(targets) - - if len(targets) == 0 || targets[0] == NoneTgt { - return failAny - } - - if targets[0] == AnyTgt { - return passAny - } - - if sc.usePathFilter { - if sc.useEqualsFilter { - return filters.PathEquals(targets) - } - - if sc.usePrefixFilter { - return filters.PathPrefix(targets) - } - - if sc.useSuffixFilter { - return filters.PathSuffix(targets) - } - - return filters.PathContains(targets) - } - - if sc.usePrefixFilter { - return filters.Prefix(targets) - } - - if sc.useSuffixFilter { - return filters.Suffix(targets) - } - - if sc.useStrictEqualsFilter { - return filters.StrictEqual(targets) - } - - if defaultFilter != nil { - return defaultFilter(targets) - } - - return filters.Equal(targets) -} - -// pathFilterFactory returns the appropriate path filter -// (contains, prefix, or suffix) for the provided options. -// If multiple options are flagged, Prefix takes priority. -// If no options are provided, returns PathContains. -func pathFilterFactory(opts ...option) filterFunc { - sc := &scopeConfig{} - sc.populate(opts...) - - var ff filterFunc - - switch true { - case sc.usePrefixFilter: - ff = filters.PathPrefix - case sc.useSuffixFilter: - ff = filters.PathSuffix - case sc.useEqualsFilter: - ff = filters.PathEquals - default: - ff = filters.PathContains - } - - return wrapSliceFilter(ff) -} - -func wrapSliceFilter(ff filterFunc) filterFunc { - return func(s []string) filters.Filter { - s = clean(s) - - if f, ok := isAnyOrNone(s); ok { - return f - } - - return ff(s) - } -} - -// returns (, true) if s is len==1 and s[0] is -// anyTgt or noneTgt, implying that the caller should use -// the returned filter. On (, false), the caller -// can ignore the returned filter. -// a special case exists for len(s)==0, interpreted as -// "noneTgt" -func isAnyOrNone(s []string) (filters.Filter, bool) { - switch len(s) { - case 0: - return failAny, true - - case 1: - switch s[0] { - case AnyTgt: - return passAny, true - case NoneTgt: - return failAny, true - } - } - - return failAny, false -} diff --git a/src/pkg/selectors/selectors_test.go b/src/pkg/selectors/selectors_test.go index 3931adfec..30d20c3c9 100644 --- a/src/pkg/selectors/selectors_test.go +++ b/src/pkg/selectors/selectors_test.go @@ -44,56 +44,6 @@ func (suite *SelectorSuite) TestBadCastErr() { assert.Error(suite.T(), err, clues.ToCore(err)) } -func (suite *SelectorSuite) TestResourceOwnersIn() { - rootCat := rootCatStub.String() - - table := []struct { - name string - input []scope - expect []string - }{ - { - name: "nil", - input: nil, - expect: []string{}, - }, - { - name: "empty", - input: []scope{}, - expect: []string{}, - }, - { - name: "single", - input: []scope{{rootCat: filters.Identity("foo")}}, - expect: []string{"foo"}, - }, - { - name: "multiple scopes", - input: []scope{ - {rootCat: filters.Identity("foo,bar")}, - {rootCat: filters.Identity("baz")}, - }, - expect: []string{"foo,bar", "baz"}, - }, - { - name: "multiple scopes with duplicates", - input: []scope{ - {rootCat: filters.Identity("foo")}, - {rootCat: filters.Identity("foo")}, - }, - expect: []string{"foo"}, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - result := resourceOwnersIn(test.input, rootCat) - assert.ElementsMatch(t, test.expect, result) - }) - } -} - func (suite *SelectorSuite) TestPathCategoriesIn() { leafCat := leafCatStub.String() f := filters.Identity(leafCat) @@ -144,20 +94,20 @@ func (suite *SelectorSuite) TestContains() { func (suite *SelectorSuite) TestIsAnyResourceOwner() { t := suite.T() - assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{"foo"}))) - assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{}))) - assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, nil))) - assert.True(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{AnyTgt}))) - assert.True(t, isAnyResourceOwner(newSelector(ServiceUnknown, Any()))) + assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{"foo"}))) + assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{}))) + assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, nil))) + assert.True(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{AnyTgt}))) + assert.True(t, isAnyProtectedResource(newSelector(ServiceUnknown, Any()))) } func (suite *SelectorSuite) TestIsNoneResourceOwner() { t := suite.T() - assert.False(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{"foo"}))) - assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{}))) - assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, nil))) - assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{NoneTgt}))) - assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, None()))) + assert.False(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{"foo"}))) + assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{}))) + assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, nil))) + assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{NoneTgt}))) + assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, None()))) } func (suite *SelectorSuite) TestSplitByResourceOnwer() { @@ -224,7 +174,7 @@ func (suite *SelectorSuite) TestSplitByResourceOnwer() { t := suite.T() s := newSelector(ServiceUnknown, test.input) - result := splitByResourceOwner[mockScope](s, allOwners, rootCatStub) + result := splitByProtectedResource[mockScope](s, allOwners, rootCatStub) assert.Len(t, result, test.expectLen) diff --git a/src/pkg/selectors/sharepoint.go b/src/pkg/selectors/sharepoint.go index a408f6339..31ad200c0 100644 --- a/src/pkg/selectors/sharepoint.go +++ b/src/pkg/selectors/sharepoint.go @@ -68,7 +68,7 @@ func (s Selector) ToSharePointBackup() (*SharePointBackup, error) { } func (s SharePointBackup) SplitByResourceOwner(sites []string) []SharePointBackup { - sels := splitByResourceOwner[SharePointScope](s.Selector, sites, SharePointSite) + sels := splitByProtectedResource[SharePointScope](s.Selector, sites, SharePointSite) ss := make([]SharePointBackup, 0, len(sels)) for _, sel := range sels { @@ -102,7 +102,7 @@ func (s Selector) ToSharePointRestore() (*SharePointRestore, error) { } func (s SharePointRestore) SplitByResourceOwner(sites []string) []SharePointRestore { - sels := splitByResourceOwner[SharePointScope](s.Selector, sites, SharePointSite) + sels := splitByProtectedResource[SharePointScope](s.Selector, sites, SharePointSite) ss := make([]SharePointRestore, 0, len(sels)) for _, sel := range sels { diff --git a/src/pkg/selectors/sharepoint_test.go b/src/pkg/selectors/sharepoint_test.go index a8003951e..1609783f0 100644 --- a/src/pkg/selectors/sharepoint_test.go +++ b/src/pkg/selectors/sharepoint_test.go @@ -44,66 +44,6 @@ func (suite *SharePointSelectorSuite) TestToSharePointBackup() { assert.NotZero(t, ob.Scopes()) } -func (suite *SharePointSelectorSuite) TestSharePointSelector_AllData() { - t := suite.T() - - sites := []string{"s1", "s2"} - - sel := NewSharePointBackup(sites) - siteScopes := sel.AllData() - - assert.ElementsMatch(t, sites, sel.DiscreteResourceOwners()) - - // Initialize the selector Include, Exclude, Filter - sel.Exclude(siteScopes) - sel.Include(siteScopes) - sel.Filter(siteScopes) - - table := []struct { - name string - scopesToCheck []scope - }{ - {"Include Scopes", sel.Includes}, - {"Exclude Scopes", sel.Excludes}, - {"info scopes", sel.Filters}, - } - for _, test := range table { - require.Len(t, test.scopesToCheck, 3) - - for _, scope := range test.scopesToCheck { - var ( - spsc = SharePointScope(scope) - cat = spsc.Category() - ) - - suite.Run(test.name+"-"+cat.String(), func() { - t := suite.T() - - switch cat { - case SharePointLibraryItem: - scopeMustHave( - t, - spsc, - map[categorizer][]string{ - SharePointLibraryItem: Any(), - SharePointLibraryFolder: Any(), - }, - ) - case SharePointListItem: - scopeMustHave( - t, - spsc, - map[categorizer][]string{ - SharePointListItem: Any(), - SharePointList: Any(), - }, - ) - } - }) - } - } -} - func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() { t := suite.T()