Add sharepoint selector boilerplate (#1464)

## Description

Adds the boilerplate for sharepoint selectors
to the selectors package.  Many categories are
given filler names (folders, items) and are
expected to change through development as we
pin down the design for those structures.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1462

## Test Plan

- [x]  Unit test
This commit is contained in:
Keepers 2022-11-08 17:00:15 -07:00 committed by GitHub
parent 20795d3b56
commit 9ac9f8bbe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 785 additions and 3 deletions

View File

@ -17,15 +17,17 @@ type service int
//go:generate stringer -type=service -linecomment
const (
ServiceUnknown service = iota // Unknown Service
ServiceExchange // Exchange
ServiceOneDrive // OneDrive
ServiceUnknown service = iota // Unknown Service
ServiceExchange // Exchange
ServiceOneDrive // OneDrive
ServiceSharePoint // SharePoint
)
var serviceToPathType = map[service]path.ServiceType{
ServiceUnknown: path.UnknownService,
ServiceExchange: path.ExchangeService,
ServiceOneDrive: path.OneDriveService,
// ServiceSharePoint: path.SharePointService, TODO: add sharepoint to path
}
var (
@ -192,6 +194,8 @@ func (s Selector) Reduce(ctx context.Context, deets *details.Details) (*details.
r, err = s.ToExchangeRestore()
case ServiceOneDrive:
r, err = s.ToOneDriveRestore()
case ServiceSharePoint:
r, err = s.ToSharePointRestore()
default:
return nil, errors.New("service not supported: " + s.Service.String())
}
@ -231,6 +235,14 @@ func (s Selector) ToPrintable() Printable {
return Printable{}
}
return r.Printable()
case ServiceSharePoint:
r, err := s.ToSharePointBackup()
if err != nil {
return Printable{}
}
return r.Printable()
}

View File

@ -0,0 +1,434 @@
package selectors
import (
"context"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------------------------------------------------------------------
// Selectors
// ---------------------------------------------------------------------------
type (
// sharePoint provides an api for selecting
// data scopes applicable to the SharePoint service.
sharePoint struct {
Selector
}
// SharePointBackup provides an api for selecting
// data scopes applicable to the SharePoint service,
// plus backup-specific methods.
SharePointBackup struct {
sharePoint
}
// SharePointRestorep provides an api for selecting
// data scopes applicable to the SharePoint service,
// plus restore-specific methods.
SharePointRestore struct {
sharePoint
}
)
var _ Reducer = &SharePointRestore{}
// NewSharePointBackup produces a new Selector with the service set to ServiceSharePoint.
func NewSharePointBackup() *SharePointBackup {
src := SharePointBackup{
sharePoint{
newSelector(ServiceSharePoint),
},
}
return &src
}
// ToSharePointBackup transforms the generic selector into an SharePointBackup.
// Errors if the service defined by the selector is not ServiceSharePoint.
func (s Selector) ToSharePointBackup() (*SharePointBackup, error) {
if s.Service != ServiceSharePoint {
return nil, badCastErr(ServiceSharePoint, s.Service)
}
src := SharePointBackup{sharePoint{s}}
return &src, nil
}
// NewSharePointRestore produces a new Selector with the service set to ServiceSharePoint.
func NewSharePointRestore() *SharePointRestore {
src := SharePointRestore{
sharePoint{
newSelector(ServiceSharePoint),
},
}
return &src
}
// ToSharePointRestore transforms the generic selector into an SharePointRestore.
// Errors if the service defined by the selector is not ServiceSharePoint.
func (s Selector) ToSharePointRestore() (*SharePointRestore, error) {
if s.Service != ServiceSharePoint {
return nil, badCastErr(ServiceSharePoint, s.Service)
}
src := SharePointRestore{sharePoint{s}}
return &src, nil
}
// Printable creates the minimized display of a selector, formatted for human readability.
func (s sharePoint) Printable() Printable {
return toPrintable[SharePointScope](s.Selector)
}
// -------------------
// Scope Factories
// Include appends the provided scopes to the selector's inclusion set.
// Data is included if it matches ANY inclusion.
// The inclusion set is later filtered (all included data must pass ALL
// filters) and excluded (all included data must not match ANY exclusion).
// Data is included if it matches ANY inclusion (of the same data category).
//
// All parts of the scope must match for data to be exclucded.
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
// a scope value. No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: Site(u1) automatically cascades to all folders and files owned
// by s1.
func (s *sharePoint) Include(scopes ...[]SharePointScope) {
s.Includes = appendScopes(s.Includes, scopes...)
}
// Exclude appends the provided scopes to the selector's exclusion set.
// Every Exclusion scope applies globally, affecting all inclusion scopes.
// Data is excluded if it matches ANY exclusion.
//
// All parts of the scope must match for data to be exclucded.
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
// a scope value. No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: Site(u1) automatically cascades to all folders and files owned
// by s1.
func (s *sharePoint) Exclude(scopes ...[]SharePointScope) {
s.Excludes = appendScopes(s.Excludes, scopes...)
}
// Filter appends the provided scopes to the selector's filters set.
// A selector with >0 filters and 0 inclusions will include any data
// that passes all filters.
// A selector with >0 filters and >0 inclusions will reduce the
// inclusion set to only the data that passes all filters.
// Data is retained if it passes ALL filters.
//
// All parts of the scope must match for data to be exclucded.
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
// a scope value. No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: Site(u1) automatically cascades to all folders and files owned
// by s1.
func (s *sharePoint) Filter(scopes ...[]SharePointScope) {
s.Filters = appendScopes(s.Filters, scopes...)
}
// Scopes retrieves the list of sharePointScopes in the selector.
func (s *sharePoint) Scopes() []SharePointScope {
return scopes[SharePointScope](s.Selector)
}
// DiscreteScopes retrieves the list of sharePointScopes in the selector.
// If any Include scope's Site category is set to Any, replaces that
// scope's value with the list of siteIDs instead.
func (s *sharePoint) DiscreteScopes(siteIDs []string) []SharePointScope {
return discreteScopes[SharePointScope](s.Selector, SharePointSite, siteIDs)
}
// -------------------
// Scope Factories
// Produces one or more SharePoint site scopes.
// One scope is created per site entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *sharePoint) Sites(sites []string) []SharePointScope {
scopes := []SharePointScope{}
scopes = append(scopes, makeScope[SharePointScope](SharePointFolder, sites, Any()))
return scopes
}
// Folders produces one or more SharePoint folder scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *sharePoint) Folders(sites, folders []string, opts ...option) []SharePointScope {
var (
scopes = []SharePointScope{}
os = append([]option{pathType()}, opts...)
)
scopes = append(
scopes,
makeScope[SharePointScope](SharePointFolder, sites, folders, os...),
)
return scopes
}
// Items produces one or more SharePoint item scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the folder scopes.
func (s *sharePoint) Items(sites, folders, items []string, opts ...option) []SharePointScope {
scopes := []SharePointScope{}
scopes = append(
scopes,
makeScope[SharePointScope](SharePointItem, sites, items).
set(SharePointFolder, folders, opts...),
)
return scopes
}
// -------------------
// Filter Factories
// WebURL produces a SharePoint item webURL filter scope.
// Matches any item where the webURL contains the substring.
// If the input equals selectors.Any, the scope will match all times.
// If the input is empty or selectors.None, the scope will always fail comparisons.
func (s *sharePoint) WebURL(substring string) []SharePointScope {
return []SharePointScope{
makeFilterScope[SharePointScope](
SharePointItem,
SharePointFilterWebURL,
[]string{substring},
wrapFilter(filters.Less)),
}
}
// ---------------------------------------------------------------------------
// Categories
// ---------------------------------------------------------------------------
// sharePointCategory enumerates the type of the lowest level
// of data () in a scope.
type sharePointCategory string
// interface compliance checks
var _ categorizer = SharePointCategoryUnknown
const (
SharePointCategoryUnknown sharePointCategory = ""
// types of data identified by SharePoint
SharePointSite sharePointCategory = "SharePointSitte"
SharePointFolder sharePointCategory = "SharePointFolder"
SharePointItem sharePointCategory = "SharePointItem"
// filterable topics identified by SharePoint
SharePointFilterWebURL sharePointCategory = "SharePointFilterWebURL"
)
// sharePointLeafProperties describes common metadata of the leaf categories
var sharePointLeafProperties = map[categorizer]leafProperty{
SharePointItem: {
pathKeys: []categorizer{SharePointSite, SharePointFolder, SharePointItem},
pathType: path.FilesCategory,
},
SharePointSite: { // the root category must be represented, even though it isn't a leaf
pathKeys: []categorizer{SharePointSite},
pathType: path.UnknownCategory,
},
}
func (c sharePointCategory) String() string {
return string(c)
}
// leafCat returns the leaf category of the receiver.
// If the receiver category has multiple leaves (ex: User) or no leaves,
// (ex: Unknown), the receiver itself is returned.
// Ex: ServiceTypeFolder.leafCat() => ServiceTypeItem
// Ex: ServiceUser.leafCat() => ServiceUser
func (c sharePointCategory) leafCat() categorizer {
switch c {
case SharePointFolder, SharePointItem,
SharePointFilterWebURL:
return SharePointItem
}
return c
}
// rootCat returns the root category type.
func (c sharePointCategory) rootCat() categorizer {
return SharePointSite
}
// unknownCat returns the unknown category type.
func (c sharePointCategory) unknownCat() categorizer {
return SharePointCategoryUnknown
}
// isLeaf is true if the category is a SharePointItem category.
func (c sharePointCategory) isLeaf() bool {
// return c == c.leafCat()??
return c == SharePointItem
}
// pathValues transforms a path to a map of identified properties.
//
// Example:
// [tenantID, service, siteID, category, folder, itemID]
// => {spSite: siteID, spFolder: folder, spItemID: itemID}
func (c sharePointCategory) pathValues(p path.Path) map[categorizer]string {
return map[categorizer]string{
SharePointSite: p.ResourceOwner(),
SharePointFolder: p.Folder(),
SharePointItem: p.Item(),
}
}
// pathKeys returns the path keys recognized by the receiver's leaf type.
func (c sharePointCategory) pathKeys() []categorizer {
return sharePointLeafProperties[c.leafCat()].pathKeys
}
// PathType converts the category's leaf type into the matching path.CategoryType.
func (c sharePointCategory) PathType() path.CategoryType {
return sharePointLeafProperties[c.leafCat()].pathType
}
// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------
// SharePointScope specifies the data available
// when interfacing with the SharePoint service.
type SharePointScope scope
// interface compliance checks
var _ scoper = &SharePointScope{}
// Category describes the type of the data in scope.
func (s SharePointScope) Category() sharePointCategory {
return sharePointCategory(getCategory(s))
}
// categorizer type is a generic wrapper around Category.
// Primarily used by scopes.go to for abstract comparisons.
func (s SharePointScope) categorizer() categorizer {
return s.Category()
}
// FilterCategory returns the category enum of the scope filter.
// If the scope is not a filter type, returns SharePointUnknownCategory.
func (s SharePointScope) FilterCategory() sharePointCategory {
return sharePointCategory(getFilterCategory(s))
}
// IncludeCategory checks whether the scope includes a
// certain category of data.
// Ex: to check if the scope includes file data:
// s.IncludesCategory(selector.SharePointFile)
func (s SharePointScope) IncludesCategory(cat sharePointCategory) bool {
return categoryMatches(s.Category(), cat)
}
// Matches returns true if the category is included in the scope's
// data type, and the target string matches that category's comparator.
func (s SharePointScope) Matches(cat sharePointCategory, target string) bool {
return matches(s, cat, target)
}
// returns true if the category is included in the scope's data type,
// and the value is set to Any().
func (s SharePointScope) IsAny(cat sharePointCategory) bool {
return isAnyTarget(s, cat)
}
// Get returns the data category in the scope. If the scope
// contains all data types for a user, it'll return the
// SharePointUser category.
func (s SharePointScope) Get(cat sharePointCategory) []string {
return getCatValue(s, cat)
}
// sets a value by category to the scope. Only intended for internal use.
func (s SharePointScope) set(cat sharePointCategory, v []string, opts ...option) SharePointScope {
os := []option{}
if cat == SharePointFolder {
os = append(os, pathType())
}
return set(s, cat, v, append(os, opts...)...)
}
// setDefaults ensures that site scopes express `AnyTgt` for their child category types.
func (s SharePointScope) setDefaults() {
switch s.Category() {
case SharePointSite:
s[SharePointFolder.String()] = passAny
s[SharePointItem.String()] = passAny
case SharePointFolder:
s[SharePointItem.String()] = passAny
}
}
// matchesInfo handles the standard behavior when comparing a scope and an sharePointInfo
// returns true if the scope and info match for the provided category.
func (s SharePointScope) matchesInfo(dii details.ItemInfo) bool {
// info := dii.SharePoint
// if info == nil {
// return false
// }
var (
filterCat = s.FilterCategory()
i = ""
)
// switch filterCat {
// case FileFilterCreatedAfter, FileFilterCreatedBefore:
// i = common.FormatTime(info.Created)
// case FileFilterModifiedAfter, FileFilterModifiedBefore:
// i = common.FormatTime(info.Modified)
// }
return s.Matches(filterCat, i)
}
// ---------------------------------------------------------------------------
// Backup Details Filtering
// ---------------------------------------------------------------------------
// Reduce filters the entries in a details struct to only those that match the
// inclusions, filters, and exclusions in the selector.
func (s sharePoint) Reduce(ctx context.Context, deets *details.Details) *details.Details {
return reduce[SharePointScope](
ctx,
deets,
s.Selector,
map[path.CategoryType]sharePointCategory{
// path.FilesCategory: SharePointItem,
},
)
}

View File

@ -0,0 +1,336 @@
package selectors
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type SharePointSelectorSuite struct {
suite.Suite
}
func TestSharePointSelectorSuite(t *testing.T) {
suite.Run(t, new(SharePointSelectorSuite))
}
func (suite *SharePointSelectorSuite) TestNewSharePointBackup() {
t := suite.T()
ob := NewSharePointBackup()
assert.Equal(t, ob.Service, ServiceSharePoint)
assert.NotZero(t, ob.Scopes())
}
func (suite *SharePointSelectorSuite) TestToSharePointBackup() {
t := suite.T()
ob := NewSharePointBackup()
s := ob.Selector
ob, err := s.ToSharePointBackup()
require.NoError(t, err)
assert.Equal(t, ob.Service, ServiceSharePoint)
assert.NotZero(t, ob.Scopes())
}
func (suite *SharePointSelectorSuite) TestSharePointBackup_DiscreteScopes() {
sites := []string{"s1", "s2"}
table := []struct {
name string
include []string
discrete []string
expect []string
}{
{
name: "any site",
include: Any(),
discrete: sites,
expect: sites,
},
{
name: "discrete sitet",
include: []string{"s3"},
discrete: sites,
expect: []string{"s3"},
},
{
name: "nil discrete slice",
include: Any(),
discrete: nil,
expect: Any(),
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
eb := NewSharePointBackup()
eb.Include(eb.Sites(test.include))
scopes := eb.DiscreteScopes(test.discrete)
for _, sc := range scopes {
sites := sc.Get(SharePointSite)
assert.Equal(t, test.expect, sites)
}
})
}
}
func (suite *SharePointSelectorSuite) TestSharePointSelector_Sites() {
t := suite.T()
sel := NewSharePointBackup()
const (
s1 = "s1"
s2 = "s2"
)
siteScopes := sel.Sites([]string{s1, s2})
for _, scope := range siteScopes {
// Scope value is either s1 or s2
assert.Contains(t, join(s1, s2), scope[SharePointSite.String()].Target)
}
// 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},
{"Filter Scopes", sel.Filters},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
require.Len(t, test.scopesToCheck, 1)
for _, scope := range test.scopesToCheck {
// Scope value is s1,s2
assert.Contains(t, join(s1, s2), scope[SharePointSite.String()].Target)
}
})
}
}
func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_Sites() {
t := suite.T()
sel := NewSharePointBackup()
const (
s1 = "s1"
s2 = "s2"
)
sel.Include(sel.Sites([]string{s1, s2}))
scopes := sel.Includes
require.Len(t, scopes, 1)
for _, sc := range scopes {
scopeMustHave(
t,
SharePointScope(sc),
map[categorizer]string{SharePointSite: join(s1, s2)},
)
}
}
func (suite *SharePointSelectorSuite) TestSharePointSelector_Exclude_Sites() {
t := suite.T()
sel := NewSharePointBackup()
const (
s1 = "s1"
s2 = "s2"
)
sel.Exclude(sel.Sites([]string{s1, s2}))
scopes := sel.Excludes
require.Len(t, scopes, 1)
for _, sc := range scopes {
scopeMustHave(
t,
SharePointScope(sc),
map[categorizer]string{SharePointSite: join(s1, s2)},
)
}
}
func (suite *SharePointSelectorSuite) TestNewSharePointRestore() {
t := suite.T()
or := NewSharePointRestore()
assert.Equal(t, or.Service, ServiceSharePoint)
assert.NotZero(t, or.Scopes())
}
func (suite *SharePointSelectorSuite) TestToSharePointRestore() {
t := suite.T()
eb := NewSharePointRestore()
s := eb.Selector
or, err := s.ToSharePointRestore()
require.NoError(t, err)
assert.Equal(t, or.Service, ServiceSharePoint)
assert.NotZero(t, or.Scopes())
}
// TODO: enable when sharepoint has path and detail representation
// func (suite *SharePointSelectorSuite) TestSharePointRestore_Reduce() {
// var (
// item = stubRepoRef(path.SharePointService, path.SharePointItemsCategory, "uid", "/folderA/folderB", "item")
// item2 = stubRepoRef(path.SharePointService, path.SharePointItemsCategory, "uid", "/folderA/folderC", "item2")
// item3 = stubRepoRef(path.SharePointService, path.SharePointItemsCategory, "uid", "/folderD/folderE", "item3")
// )
// deets := &details.Details{
// DetailsModel: details.DetailsModel{
// Entries: []details.DetailsEntry{
// {
// RepoRef: item,
// ItemInfo: details.ItemInfo{
// SharePoint: &details.SharePointInfo{
// ItemType: details.SharePointItem,
// },
// },
// },
// {
// RepoRef: item2,
// ItemInfo: details.ItemInfo{
// SharePoint: &details.SharePointInfo{
// ItemType: details.SharePointItem,
// },
// },
// },
// {
// RepoRef: item3,
// ItemInfo: details.ItemInfo{
// SharePoint: &details.SharePointInfo{
// ItemType: details.SharePointItem,
// },
// },
// },
// },
// },
// }
// arr := func(s ...string) []string {
// return s
// }
// table := []struct {
// name string
// deets *details.Details
// makeSelector func() *SharePointRestore
// expect []string
// }{
// {
// "all",
// deets,
// func() *SharePointRestore {
// odr := NewSharePointRestore()
// odr.Include(odr.Sites(Any()))
// return odr
// },
// arr(item, item2, item3),
// },
// {
// "only match item",
// deets,
// func() *SharePointRestore {
// odr := NewSharePointRestore()
// odr.Include(odr.Items(Any(), Any(), []string{"item2"}))
// return odr
// },
// arr(item2),
// },
// {
// "only match folder",
// deets,
// func() *SharePointRestore {
// odr := NewSharePointRestore()
// odr.Include(odr.Folders([]string{"uid"}, []string{"folderA/folderB", "folderA/folderC"}))
// return odr
// },
// arr(item, item2),
// },
// }
// for _, test := range table {
// suite.T().Run(test.name, func(t *testing.T) {
// ctx, flush := tester.NewContext()
// defer flush()
// sel := test.makeSelector()
// results := sel.Reduce(ctx, test.deets)
// paths := results.Paths()
// assert.Equal(t, test.expect, paths)
// })
// }
// }
// func (suite *SharePointSelectorSuite) TestSharePointCategory_PathValues() {
// t := suite.T()
// pathBuilder := path.Builder{}.Append("dir1", "dir2", "item")
// itemPath, err := pathBuilder.ToDataLayerSharePointPath("tenant", "site", true)
// require.NoError(t, err)
// expected := map[categorizer]string{
// SharePointSite: "site",
// SharePointFolder: "dir1/dir2",
// SharePointItem: "item",
// }
// assert.Equal(t, expected, SharePointItem.pathValues(itemPath))
// }
// func (suite *SharePointSelectorSuite) TestSharePointScope_MatchesInfo() {
// ods := NewSharePointRestore()
// var (
// url = "www.website.com"
// )
// itemInfo := details.ItemInfo{
// SharePoint: &details.SharepointInfo{
// ItemType: details.SharePointItem,
// WebURL: "www.website.com",
// },
// }
// table := []struct {
// name string
// scope []SharePointScope
// expect assert.BoolAssertionFunc
// }{
// {"item webURL match", ods.WebURL(url), assert.True},
// {"item webURL substring", ods.WebURL("website"), assert.True},
// {"item webURL mismatch", ods.WebURL("google"), assert.False},
// }
// for _, test := range table {
// suite.T().Run(test.name, func(t *testing.T) {
// scopes := setScopesToDefault(test.scope)
// for _, scope := range scopes {
// test.expect(t, scope.matchesInfo(itemInfo))
// }
// })
// }
// }
// func (suite *SharePointSelectorSuite) TestCategory_PathType() {
// table := []struct {
// cat sharePointCategory
// pathType path.CategoryType
// }{
// {SharePointCategoryUnknown, path.UnknownCategory},
// {SharePointSite, path.UnknownCategory},
// {SharePointFolder, path.SharePointItemCategory},
// {SharePointItem, path.SharePointItemCategory},
// {SharePointFilterWebURL, path.SharePointItemCategory},
// }
// for _, test := range table {
// suite.T().Run(test.cat.String(), func(t *testing.T) {
// assert.Equal(t, test.pathType, test.cat.PathType())
// })
// }
// }