From 0e261fb96acbab7336a66c2f17efc527c94282eb Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Tue, 21 Jun 2022 18:26:39 -0600 Subject: [PATCH] add /pkg/source skeleton (#198) * add /pkg/source skeleton Source acts as an intermediary between the client and internal packages (GraphConnector, Kopia) to specify the scope of data in a Backup or Restore operation request. --- src/pkg/source/exchange.go | 129 +++++++++++++++++++++++++++++++ src/pkg/source/exchange_test.go | 26 +++++++ src/pkg/source/service_string.go | 24 ++++++ src/pkg/source/source.go | 87 +++++++++++++++++++++ src/pkg/source/source_test.go | 65 ++++++++++++++++ 5 files changed, 331 insertions(+) create mode 100644 src/pkg/source/exchange.go create mode 100644 src/pkg/source/exchange_test.go create mode 100644 src/pkg/source/service_string.go create mode 100644 src/pkg/source/source.go create mode 100644 src/pkg/source/source_test.go diff --git a/src/pkg/source/exchange.go b/src/pkg/source/exchange.go new file mode 100644 index 000000000..a9079f704 --- /dev/null +++ b/src/pkg/source/exchange.go @@ -0,0 +1,129 @@ +package source + +import ( + "strconv" +) + +// ExchangeSource provides an api for scoping +// data in the Exchange service. +type ExchangeSource struct { + Source +} + +// ToExchange transforms the generic source into an ExchangeSource. +// Errors if the service defined by the source is not ServiceExchange. +func (s Source) ToExchange() (*ExchangeSource, error) { + if s.service != ServiceExchange { + return nil, badCastErr(ServiceExchange, s.service) + } + src := ExchangeSource{s} + return &src, nil +} + +// NewExchange produces a new Source with the service set to ServiceExchange. +func NewExchange(tenantID string) *ExchangeSource { + src := ExchangeSource{ + newSource(tenantID, ServiceExchange), + } + return &src +} + +// Scopes retrieves the list of exchangeScopes in the source. +func (s *ExchangeSource) Scopes() []exchangeScope { + scopes := []exchangeScope{} + for _, v := range s.scopes { + 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 source. + +// Users selects the specified users. All of their data is included. +func (s *ExchangeSource) Users(us ...string) { + // todo +} + +// Contacts selects the specified contacts owned by the user. +func (s *ExchangeSource) Contacts(u string, vs ...string) { + // todo +} + +// Events selects the specified events owned by the user. +func (s *ExchangeSource) Events(u string, vs ...string) { + // todo +} + +// MailFolders selects the specified mail folders owned by the user. +func (s *ExchangeSource) MailFolders(u string, vs ...string) { + // todo +} + +// MailMessages selects the specified mail messages within the given folder, +// owned by the user. +func (s *ExchangeSource) 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. +const ( + ExchangeCategoryUnknown exchangeCategory = iota + ExchangeContact + ExchangeEvent + ExchangeFolder + ExchangeMail + 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)) +} + +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)) +} + +// Granularity describes the breadth of data in scope. +func (s exchangeScope) Granularity() scopeGranularity { + return granularityOf(s) +} + +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] +} diff --git a/src/pkg/source/exchange_test.go b/src/pkg/source/exchange_test.go new file mode 100644 index 000000000..70422b28e --- /dev/null +++ b/src/pkg/source/exchange_test.go @@ -0,0 +1,26 @@ +package source_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/pkg/source" +) + +type ExchangeSourceSuite struct { + suite.Suite +} + +func TestExchangeSourceSuite(t *testing.T) { + suite.Run(t, new(ExchangeSourceSuite)) +} + +func (suite *ExchangeSourceSuite) TestNewExchangeSource() { + t := suite.T() + es := source.NewExchange("tid") + assert.Equal(t, es.TenantID, "tid") + assert.Equal(t, es.Service(), source.ServiceExchange) + assert.NotZero(t, es.Scopes()) +} diff --git a/src/pkg/source/service_string.go b/src/pkg/source/service_string.go new file mode 100644 index 000000000..c70010951 --- /dev/null +++ b/src/pkg/source/service_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=service -linecomment"; DO NOT EDIT. + +package source + +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[ServiceUnknown-0] + _ = x[ServiceExchange-1] +} + +const _service_name = "Unknown ServiceExchange" + +var _service_index = [...]uint8{0, 15, 23} + +func (i service) String() string { + if i < 0 || i >= service(len(_service_index)-1) { + return "service(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _service_name[_service_index[i]:_service_index[i+1]] +} diff --git a/src/pkg/source/source.go b/src/pkg/source/source.go new file mode 100644 index 000000000..8a7fa27f6 --- /dev/null +++ b/src/pkg/source/source.go @@ -0,0 +1,87 @@ +package source + +import ( + "strconv" + + "github.com/pkg/errors" +) + +type service int + +//go:generate stringer -type=service -linecomment +const ( + ServiceUnknown service = iota // Unknown Service + ServiceExchange // Exchange +) + +var ErrorBadSourceCast = errors.New("wrong source service type") + +const ( + scopeKeyGranularity = "granularity" + scopeKeyCategory = "category" +) + +const ( + // All is the wildcard value used to express "all data of " + // Ex: Events(u1, All) => all events for user u1. + All = "*" +) + +// The core source. Has no api for setting or retrieving data. +// Is only used to pass along more specific source instances. +type Source 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. +} + +// helper for specific source instance constructors. +func newSource(tenantID string, s service) Source { + return Source{ + TenantID: tenantID, + service: s, + scopes: []map[string]string{}, + } +} + +// Service return the service enum for the source. +func (s Source) Service() service { + return s.service +} + +func badCastErr(cast, is service) error { + return errors.Wrapf(ErrorBadSourceCast, "%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(source map[string]string) scopeGranularity { + return scopeGranularity(getIota(source, 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 +} diff --git a/src/pkg/source/source_test.go b/src/pkg/source/source_test.go new file mode 100644 index 000000000..3f6f725ac --- /dev/null +++ b/src/pkg/source/source_test.go @@ -0,0 +1,65 @@ +package source + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type SourceSuite struct { + suite.Suite +} + +func TestSourceSuite(t *testing.T) { + suite.Run(t, new(SourceSuite)) +} + +func (suite *SourceSuite) TestNewSource() { + t := suite.T() + s := newSource("tid", ServiceUnknown) + assert.NotNil(t, s) + assert.Equal(t, s.TenantID, "tid") + assert.Equal(t, s.service, ServiceUnknown) + assert.NotNil(t, s.scopes) +} + +func (suite *SourceSuite) TestSource_Service() { + table := []service{ + ServiceUnknown, + ServiceExchange, + } + for _, test := range table { + suite.T().Run(fmt.Sprintf("testing %d", test), func(t *testing.T) { + s := newSource("tid", test) + assert.Equal(t, s.Service(), test) + }) + } +} + +func (suite *SourceSuite) 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) + }) + } +} + +func (suite *SourceSuite) TestBadCastErr() { + err := badCastErr(ServiceUnknown, ServiceExchange) + assert.Error(suite.T(), err) +}