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:
parent
1fdaa29b3f
commit
67e38faf5e
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user