selectors cleanup (#3997)

some code movement and removal in selectors and scopes before adding scope-to-reason.  Removed code
was only being used in testing.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #3993

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-08-09 19:11:33 -06:00 committed by GitHub
parent 1fdaa29b3f
commit 67e38faf5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 435 additions and 579 deletions

View File

@ -168,7 +168,6 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
weburl []string
data []string
expect []string
expectScopesLen int
}{
{
name: "no sites or urls",
@ -184,60 +183,51 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
name: "site wildcard",
site: []string{flags.Wildcard},
expect: bothIDs,
expectScopesLen: 2,
},
{
name: "url wildcard",
weburl: []string{flags.Wildcard},
expect: bothIDs,
expectScopesLen: 2,
},
{
name: "sites",
site: []string{id1, id2},
expect: []string{id1, id2},
expectScopesLen: 2,
},
{
name: "urls",
weburl: []string{url1, url2},
expect: []string{url1, url2},
expectScopesLen: 2,
},
{
name: "mix sites and urls",
site: []string{id1},
weburl: []string{url2},
expect: []string{id1, url2},
expectScopesLen: 2,
},
{
name: "duplicate sites and urls",
site: []string{id1, id2},
weburl: []string{url1, url2},
expect: []string{id1, id2, url1, url2},
expectScopesLen: 2,
},
{
name: "unnecessary site wildcard",
site: []string{id1, flags.Wildcard},
weburl: []string{url1, url2},
expect: bothIDs,
expectScopesLen: 2,
},
{
name: "unnecessary url wildcard",
site: []string{id1},
weburl: []string{url1, flags.Wildcard},
expect: bothIDs,
expectScopesLen: 2,
},
{
name: "Pages",
site: bothIDs,
data: []string{dataPages},
expect: bothIDs,
expectScopesLen: 1,
},
}
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)
})
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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 (<filter>, true) if s is len==1 and s[0] is
// anyTgt or noneTgt, implying that the caller should use
// the returned filter. On (<filter>, 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

View File

@ -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 (<filter>, true) if s is len==1 and s[0] is
// anyTgt or noneTgt, implying that the caller should use
// the returned filter. On (<filter>, 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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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()