update selector to match current design (#266)

* update selector to match current design

The selector design is progressing in the Showdown doc.
This updates the existing structs to match the expectations in that doc.
This commit is contained in:
Keepers 2022-07-05 10:20:19 -06:00 committed by GitHub
parent cdf368ad20
commit 25a1e972e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 493 additions and 187 deletions

View File

@ -1,129 +1,258 @@
package selectors
import (
"strconv"
"strings"
)
// Exchange provides an api for scoping
// data in the Exchange service.
type Exchange struct {
Selector
}
// ---------------------------------------------------------------------------
// Selectors
// ---------------------------------------------------------------------------
// ToExchange transforms the generic selector into an Exchange.
// Errors if the service defined by the selector is not ServiceExchange.
func (s Selector) ToExchange() (*Exchange, error) {
if s.service != ServiceExchange {
return nil, badCastErr(ServiceExchange, s.service)
type (
// exchange provides an api for selecting
// data scopes applicable to the Exchange service.
exchange struct {
Selector
}
src := Exchange{s}
return &src, nil
}
// ExchangeBackup provides an api for selecting
// data scopes applicable to the Exchange service,
// plus backup-specific methods.
ExchangeBackup struct {
exchange
}
// ExchangeRestore provides an api for selecting
// data scopes applicable to the Exchange service,
// plus restore-specific methods.
ExchangeRestore struct {
exchange
}
)
// NewExchange produces a new Selector with the service set to ServiceExchange.
func NewExchange(tenantID string) *Exchange {
src := Exchange{
newSelector(tenantID, ServiceExchange),
func NewExchangeBackup() *ExchangeBackup {
src := ExchangeBackup{
exchange{
newSelector(ServiceExchange, ""),
},
}
return &src
}
// ToExchangeBackup transforms the generic selector into an ExchangeBackup.
// Errors if the service defined by the selector is not ServiceExchange.
func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) {
if s.Service != ServiceExchange {
return nil, badCastErr(ServiceExchange, s.Service)
}
src := ExchangeBackup{exchange{s}}
return &src, nil
}
// NewExchangeRestore produces a new Selector with the service set to ServiceExchange.
func NewExchangeRestore(restorePointID string) *ExchangeRestore {
src := ExchangeRestore{
exchange{
newSelector(ServiceExchange, restorePointID),
},
}
return &src
}
// ToExchangeRestore transforms the generic selector into an ExchangeRestore.
// Errors if the service defined by the selector is not ServiceExchange.
func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) {
if s.Service != ServiceExchange {
return nil, badCastErr(ServiceExchange, s.Service)
}
src := ExchangeRestore{exchange{s}}
return &src, nil
}
// IncludeContacts selects the specified contacts owned by the user.
func (s *exchange) IncludeContacts(u string, vs ...string) {
// todo
}
// IncludeContactFolders selects the specified contactFolders owned by the user.
func (s *exchange) IncludeContactFolders(u string, vs ...string) {
// todo
}
// IncludeEvents selects the specified events owned by the user.
func (s *exchange) IncludeEvents(u string, vs ...string) {
// todo
}
// IncludeMail selects the specified mail messages within the given folder,
// owned by the user.
func (s *exchange) IncludeMail(u, f string, vs ...string) {
// todo
}
// IncludeMailFolders selects the specified mail folders owned by the user.
func (s *exchange) IncludeMailFolders(u string, vs ...string) {
// todo
}
// IncludeUsers selects the specified users. All of their data is included.
func (s *exchange) IncludeUsers(us ...string) {
// todo
}
// ExcludeContacts selects the specified contacts owned by the user.
func (s *exchange) ExcludeContacts(u string, vs ...string) {
// todo
}
// ExcludeContactFolders selects the specified contactFolders owned by the user.
func (s *exchange) ExcludeContactFolders(u string, vs ...string) {
// todo
}
// ExcludeEvents selects the specified events owned by the user.
func (s *exchange) ExcludeEvents(u string, vs ...string) {
// todo
}
// ExcludeMail selects the specified mail messages within the given folder,
// owned by the user.
func (s *exchange) ExcludeMail(u, f string, vs ...string) {
// todo
}
// ExcludeMailFolders selects the specified mail folders owned by the user.
func (s *exchange) ExcludeMailFolders(u string, vs ...string) {
// todo
}
// ExcludeUsers selects the specified users. All of their data is excluded.
func (s *exchange) ExcludeUsers(us ...string) {
// todo
}
// ---------------------------------------------------------------------------
// Destination
// ---------------------------------------------------------------------------
type ExchangeDestination Destination
func NewExchangeDestination() ExchangeDestination {
return ExchangeDestination{}
}
// GetOrDefault gets the destination of the provided category. If no
// destination is set, returns the current value.
func (d ExchangeDestination) GetOrDefault(cat exchangeCategory, current string) string {
dest, ok := d[cat.String()]
if !ok {
return current
}
return dest
}
// Sets the destination value of the provided category. Returns an error
// if a destination is already declared for that category.
func (d ExchangeDestination) Set(cat exchangeCategory, dest string) error {
if len(dest) == 0 {
return nil
}
cs := cat.String()
if curr, ok := d[cs]; ok {
return existingDestinationErr(cs, curr)
}
d[cs] = dest
return nil
}
// ---------------------------------------------------------------------------
// Scopes
// ---------------------------------------------------------------------------
type (
// exchangeScope specifies the data available
// when interfacing with the Exchange service.
exchangeScope map[string]string
// exchangeCategory enumerates the type of the lowest level
// of data () in a scope.
exchangeCategory int
)
// Scopes retrieves the list of exchangeScopes in the selector.
func (s *Exchange) Scopes() []exchangeScope {
func (s *exchange) Scopes() []exchangeScope {
scopes := []exchangeScope{}
for _, v := range s.scopes {
for _, v := range s.Includes {
scopes = append(scopes, exchangeScope(v))
}
return scopes
}
// the following are called by the client to specify the constraints
// each call appends one or more scopes to the selector.
// Users selects the specified users. All of their data is included.
func (s *Exchange) Users(us ...string) {
// todo
}
// Contacts selects the specified contacts owned by the user.
func (s *Exchange) Contacts(u string, vs ...string) {
// todo
}
// Events selects the specified events owned by the user.
func (s *Exchange) Events(u string, vs ...string) {
// todo
}
// MailFolders selects the specified mail folders owned by the user.
func (s *Exchange) MailFolders(u string, vs ...string) {
// todo
}
// MailMessages selects the specified mail messages within the given folder,
// owned by the user.
func (s *Exchange) MailMessages(u, f string, vs ...string) {
// todo
}
// -----------------------
// exchangeScope specifies the data available
// when interfacing with the Exchange service.
type exchangeScope map[string]string
type exchangeCategory int
// exchangeCategory describes the type of data in scope.
//go:generate stringer -type=exchangeCategory
const (
ExchangeCategoryUnknown exchangeCategory = iota
ExchangeContact
ExchangeContactFolder
ExchangeEvent
ExchangeFolder
ExchangeMail
ExchangeMailFolder
ExchangeUser
)
// String complies with the stringer interface, so that exchangeCategories
// can be added into the scope map.
func (ec exchangeCategory) String() string {
return strconv.Itoa(int(ec))
func exchangeCatAtoI(s string) exchangeCategory {
switch s {
case ExchangeContact.String():
return ExchangeContact
case ExchangeContactFolder.String():
return ExchangeContactFolder
case ExchangeEvent.String():
return ExchangeEvent
case ExchangeMail.String():
return ExchangeMail
case ExchangeMailFolder.String():
return ExchangeMailFolder
case ExchangeUser.String():
return ExchangeUser
default:
return ExchangeCategoryUnknown
}
}
var (
exchangeScopeKeyContactID = ExchangeContact.String()
exchangeScopeKeyEventID = ExchangeEvent.String()
exchangeScopeKeyFolderID = ExchangeFolder.String()
exchangeScopeKeyMessageID = ExchangeMail.String()
exchangeScopeKeyUserID = ExchangeUser.String()
)
// Category describes the type of the data in scope.
func (s exchangeScope) Category() exchangeCategory {
return exchangeCategory(getIota(s, scopeKeyCategory))
return exchangeCatAtoI(s[scopeKeyCategory])
}
// Granularity describes the breadth of data in scope.
func (s exchangeScope) Granularity() scopeGranularity {
return granularityOf(s)
// IncludeCategory checks whether the scope includes a
// certain category of data.
// Ex: to check if the scope includes mail data:
// s.IncludesCategory(selector.ExchangeMail)
func (s exchangeScope) IncludesCategory(cat exchangeCategory) bool {
sCat := s.Category()
if cat == ExchangeCategoryUnknown || sCat == ExchangeCategoryUnknown {
return false
}
if cat == ExchangeUser || sCat == ExchangeUser {
return true
}
switch sCat {
case ExchangeContact, ExchangeContactFolder:
return cat == ExchangeContact || cat == ExchangeContactFolder
case ExchangeEvent:
return cat == ExchangeEvent
case ExchangeMail, ExchangeMailFolder:
return cat == ExchangeMail || cat == ExchangeMailFolder
}
return false
}
func (s exchangeScope) UserID() string {
return s[exchangeScopeKeyUserID]
}
func (s exchangeScope) ContactID() string {
return s[exchangeScopeKeyContactID]
}
func (s exchangeScope) EventID() string {
return s[exchangeScopeKeyEventID]
}
func (s exchangeScope) FolderID() string {
return s[exchangeScopeKeyFolderID]
}
func (s exchangeScope) MessageID() string {
return s[exchangeScopeKeyMessageID]
// Get returns the data category in the scope. If the scope
// contains all data types for a user, it'll return the
// ExchangeUser category.
func (s exchangeScope) Get(cat exchangeCategory) []string {
v, ok := s[cat.String()]
if !ok {
return []string{None}
}
return strings.Split(v, ",")
}

View File

@ -1,12 +1,11 @@
package selectors_test
package selectors
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/pkg/selectors"
)
type ExchangeSourceSuite struct {
@ -17,10 +16,205 @@ func TestExchangeSourceSuite(t *testing.T) {
suite.Run(t, new(ExchangeSourceSuite))
}
func (suite *ExchangeSourceSuite) TestNewExchangeSource() {
func (suite *ExchangeSourceSuite) TestNewExchangeBackup() {
t := suite.T()
es := selectors.NewExchange("tid")
assert.Equal(t, es.TenantID, "tid")
assert.Equal(t, es.Service(), selectors.ServiceExchange)
assert.NotZero(t, es.Scopes())
eb := NewExchangeBackup()
assert.Equal(t, eb.Service, ServiceExchange)
assert.Zero(t, eb.RestorePointID)
assert.NotZero(t, eb.Scopes())
}
func (suite *ExchangeSourceSuite) TestToExchangeBackup() {
t := suite.T()
eb := NewExchangeBackup()
s := eb.Selector
eb, err := s.ToExchangeBackup()
require.NoError(t, err)
assert.Equal(t, eb.Service, ServiceExchange)
assert.Zero(t, eb.RestorePointID)
assert.NotZero(t, eb.Scopes())
}
func (suite *ExchangeSourceSuite) TestNewExchangeRestore() {
t := suite.T()
er := NewExchangeRestore("rpid")
assert.Equal(t, er.Service, ServiceExchange)
assert.Equal(t, er.RestorePointID, "rpid")
assert.NotZero(t, er.Scopes())
}
func (suite *ExchangeSourceSuite) TestToExchangeRestore() {
t := suite.T()
eb := NewExchangeRestore("rpid")
s := eb.Selector
eb, err := s.ToExchangeRestore()
require.NoError(t, err)
assert.Equal(t, eb.Service, ServiceExchange)
assert.Equal(t, eb.RestorePointID, "rpid")
assert.NotZero(t, eb.Scopes())
}
func (suite *ExchangeSourceSuite) TestNewExchangeDestination() {
t := suite.T()
dest := NewExchangeDestination()
assert.Len(t, dest, 0)
}
func (suite *ExchangeSourceSuite) TestExchangeDestination_Set() {
dest := NewExchangeDestination()
table := []exchangeCategory{
ExchangeCategoryUnknown,
ExchangeContact,
ExchangeContactFolder,
ExchangeEvent,
ExchangeMail,
ExchangeMailFolder,
ExchangeUser,
}
for _, test := range table {
suite.T().Run(test.String(), func(t *testing.T) {
assert.NoError(t, dest.Set(test, "foo"))
assert.Error(t, dest.Set(test, "foo"))
})
}
assert.NoError(suite.T(), dest.Set(ExchangeUser, ""))
}
func (suite *ExchangeSourceSuite) TestExchangeDestination_GetOrDefault() {
dest := NewExchangeDestination()
table := []exchangeCategory{
ExchangeCategoryUnknown,
ExchangeContact,
ExchangeContactFolder,
ExchangeEvent,
ExchangeMail,
ExchangeMailFolder,
ExchangeUser,
}
for _, test := range table {
suite.T().Run(test.String(), func(t *testing.T) {
assert.Equal(t, "bar", dest.GetOrDefault(test, "bar"))
assert.NoError(t, dest.Set(test, "foo"))
assert.Equal(t, "foo", dest.GetOrDefault(test, "bar"))
})
}
}
var allScopesExceptUnknown = map[string]string{
ExchangeContact.String(): All,
ExchangeContactFolder.String(): All,
ExchangeEvent.String(): All,
ExchangeMail.String(): All,
ExchangeMailFolder.String(): All,
ExchangeUser.String(): All,
}
func (suite *ExchangeSourceSuite) TestExchangeBackup_Scopes() {
eb := NewExchangeBackup()
eb.Includes = []map[string]string{allScopesExceptUnknown}
// todo: swap the above for this
// eb := NewExchangeBackup().IncludeUsers(All)
scopes := eb.Scopes()
assert.Len(suite.T(), scopes, 1)
assert.Equal(
suite.T(),
allScopesExceptUnknown,
map[string]string(scopes[0]))
}
func (suite *ExchangeSourceSuite) TestExchangeScope_Category() {
table := []struct {
is exchangeCategory
expect exchangeCategory
check assert.ComparisonAssertionFunc
}{
{ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.Equal},
{ExchangeCategoryUnknown, ExchangeUser, assert.NotEqual},
{ExchangeContact, ExchangeContact, assert.Equal},
{ExchangeContact, ExchangeMailFolder, assert.NotEqual},
{ExchangeContactFolder, ExchangeContactFolder, assert.Equal},
{ExchangeContactFolder, ExchangeMailFolder, assert.NotEqual},
{ExchangeEvent, ExchangeEvent, assert.Equal},
{ExchangeEvent, ExchangeContact, assert.NotEqual},
{ExchangeMail, ExchangeMail, assert.Equal},
{ExchangeMail, ExchangeMailFolder, assert.NotEqual},
{ExchangeMailFolder, ExchangeMailFolder, assert.Equal},
{ExchangeMailFolder, ExchangeContactFolder, assert.NotEqual},
{ExchangeUser, ExchangeUser, assert.Equal},
{ExchangeUser, ExchangeCategoryUnknown, assert.NotEqual},
}
for _, test := range table {
suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) {
eb := NewExchangeBackup()
eb.Includes = []map[string]string{{scopeKeyCategory: test.is.String()}}
scope := eb.Scopes()[0]
test.check(t, test.expect, scope.Category())
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesCategory() {
table := []struct {
is exchangeCategory
expect exchangeCategory
check assert.BoolAssertionFunc
}{
{ExchangeCategoryUnknown, ExchangeCategoryUnknown, assert.False},
{ExchangeCategoryUnknown, ExchangeUser, assert.False},
{ExchangeContact, ExchangeContactFolder, assert.True},
{ExchangeContact, ExchangeMailFolder, assert.False},
{ExchangeContactFolder, ExchangeContact, assert.True},
{ExchangeContactFolder, ExchangeMailFolder, assert.False},
{ExchangeEvent, ExchangeUser, assert.True},
{ExchangeEvent, ExchangeContact, assert.False},
{ExchangeMail, ExchangeMailFolder, assert.True},
{ExchangeMail, ExchangeContact, assert.False},
{ExchangeMailFolder, ExchangeMail, assert.True},
{ExchangeMailFolder, ExchangeContactFolder, assert.False},
{ExchangeUser, ExchangeUser, assert.True},
{ExchangeUser, ExchangeCategoryUnknown, assert.False},
{ExchangeUser, ExchangeMail, assert.True},
}
for _, test := range table {
suite.T().Run(test.is.String()+test.expect.String(), func(t *testing.T) {
eb := NewExchangeBackup()
eb.Includes = []map[string]string{{scopeKeyCategory: test.is.String()}}
scope := eb.Scopes()[0]
test.check(t, scope.IncludesCategory(test.expect))
})
}
}
func (suite *ExchangeSourceSuite) TestExchangeScope_Get() {
eb := NewExchangeBackup()
eb.Includes = []map[string]string{allScopesExceptUnknown}
// todo: swap the above for this
// eb := NewExchangeBackup().IncludeUsers(All)
scope := eb.Scopes()[0]
table := []exchangeCategory{
ExchangeContact,
ExchangeContactFolder,
ExchangeEvent,
ExchangeMail,
ExchangeMailFolder,
ExchangeUser,
}
assert.Equal(
suite.T(),
[]string{None},
scope.Get(ExchangeCategoryUnknown))
expect := []string{All}
for _, test := range table {
suite.T().Run(test.String(), func(t *testing.T) {
assert.Equal(t, expect, scope.Get(test))
})
}
}

View File

@ -0,0 +1,29 @@
// Code generated by "stringer -type=exchangeCategory"; DO NOT EDIT.
package selectors
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ExchangeCategoryUnknown-0]
_ = x[ExchangeContact-1]
_ = x[ExchangeContactFolder-2]
_ = x[ExchangeEvent-3]
_ = x[ExchangeMail-4]
_ = x[ExchangeMailFolder-5]
_ = x[ExchangeUser-6]
}
const _exchangeCategory_name = "ExchangeCategoryUnknownExchangeContactExchangeContactFolderExchangeEventExchangeMailExchangeMailFolderExchangeUser"
var _exchangeCategory_index = [...]uint8{0, 23, 38, 59, 72, 84, 102, 114}
func (i exchangeCategory) String() string {
if i < 0 || i >= exchangeCategory(len(_exchangeCategory_index)-1) {
return "exchangeCategory(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _exchangeCategory_name[_exchangeCategory_index[i]:_exchangeCategory_index[i+1]]
}

View File

@ -1,8 +1,6 @@
package selectors
import (
"strconv"
"github.com/pkg/errors"
)
@ -17,71 +15,57 @@ const (
var ErrorBadSelectorCast = errors.New("wrong selector service type")
const (
scopeKeyGranularity = "granularity"
scopeKeyCategory = "category"
scopeKeyCategory = "category"
)
const (
// All is the wildcard value used to express "all data of <type>"
// Ex: Events(u1, All) => all events for user u1.
All = "*"
// Ex: {user: u1, events: All) => all events for user u1.
All = "ß∂ƒ∑´®≈ç√¬˜"
// None is usesd to express "no data of <type>"
// Ex: {user: u1, events: None} => no events for user u1.
None = "√ç≈œ´∆¬˚¨π"
)
// ---------------------------------------------------------------------------
// Selector
// ---------------------------------------------------------------------------
// The core selector. Has no api for setting or retrieving data.
// Is only used to pass along more specific selector instances.
type Selector struct {
TenantID string // The tenant making the request.
service service // The service scope of the data. Exchange, Teams, Sharepoint, etc.
scopes []map[string]string // A slice of scopes. Expected to get cast to fooScope within each service handler.
RestorePointID string `json:"restorePointID,omitempty"` // A restore point id, used only by restore operations.
Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc.
Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusions. Each exclusion applies to all inclusions.
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusions. Expected to get cast to a service wrapper within each service handler.
}
// helper for specific selector instance constructors.
func newSelector(tenantID string, s service) Selector {
func newSelector(s service, restorePointID string) Selector {
return Selector{
TenantID: tenantID,
service: s,
scopes: []map[string]string{},
RestorePointID: restorePointID,
Service: s,
Excludes: []map[string]string{},
Includes: []map[string]string{},
}
}
// Service return the service enum for the selector.
func (s Selector) Service() service {
return s.service
}
// ---------------------------------------------------------------------------
// Destination
// ---------------------------------------------------------------------------
type Destination map[string]string
var ErrorDestinationAlreadySet = errors.New("destination is already declared")
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
func badCastErr(cast, is service) error {
return errors.Wrapf(ErrorBadSelectorCast, "%s service is not %s", cast, is)
}
type scopeGranularity int
// granularity expresses the breadth of the request
const (
GranularityUnknown scopeGranularity = iota
SingleItem
AllIn
)
// String complies with the stringer interface, so that granularities
// can be added into the scope map.
func (g scopeGranularity) String() string {
return strconv.Itoa(int(g))
}
func granularityOf(selector map[string]string) scopeGranularity {
return scopeGranularity(getIota(selector, scopeKeyGranularity))
}
// retrieves the iota, stored as a string, and transforms it to
// an int. Any errors will return a 0 by default.
func getIota(m map[string]string, key string) int {
v, ok := m[key]
if !ok {
return 0
}
i, err := strconv.Atoi(v)
if err != nil {
return 0
}
return i
func existingDestinationErr(category, is string) error {
return errors.Wrapf(ErrorDestinationAlreadySet, "%s destination already set to %s", category, is)
}

View File

@ -1,7 +1,6 @@
package selectors
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -18,48 +17,19 @@ func TestSelectorSuite(t *testing.T) {
func (suite *SelectorSuite) TestNewSelector() {
t := suite.T()
s := newSelector("tid", ServiceUnknown)
s := newSelector(ServiceUnknown, "rpid")
assert.NotNil(t, s)
assert.Equal(t, s.TenantID, "tid")
assert.Equal(t, s.service, ServiceUnknown)
assert.NotNil(t, s.scopes)
}
func (suite *SelectorSuite) TestSelector_Service() {
table := []service{
ServiceUnknown,
ServiceExchange,
}
for _, test := range table {
suite.T().Run(fmt.Sprintf("testing %d", test), func(t *testing.T) {
s := newSelector("tid", test)
assert.Equal(t, s.Service(), test)
})
}
}
func (suite *SelectorSuite) TestGetIota() {
table := []struct {
name string
val string
expect int
}{
{"zero", "0", 0},
{"positive", "1", 1},
{"negative", "-1", -1},
{"empty", "", 0},
{"NaN", "fnords", 0},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
m := map[string]string{"test": test.val}
result := getIota(m, "test")
assert.Equal(t, result, test.expect)
})
}
assert.Equal(t, s.Service, ServiceUnknown)
assert.Equal(t, s.RestorePointID, "rpid")
assert.NotNil(t, s.Includes)
}
func (suite *SelectorSuite) TestBadCastErr() {
err := badCastErr(ServiceUnknown, ServiceExchange)
assert.Error(suite.T(), err)
}
func (suite *SelectorSuite) TestExistingDestinationErr() {
err := existingDestinationErr("foo", "bar")
assert.Error(suite.T(), err)
}