introduce service_resource tuple

the service resource tuple will form the basis for
having paths contain multiple nested service
declarations, by allowing it to swap out the
Service() and ResourceOwner() funcs in favor of
a func that returns the ordered list of service-resource
tuples found in the path.
This commit is contained in:
ryanfkeepers 2023-08-09 18:37:11 -06:00
parent 7aed7eba0e
commit b7aea6bab4
5 changed files with 388 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/ptr"
)
func AnyValueToT[T any](k string, m map[string]any) (T, error) {
@ -23,3 +24,21 @@ func AnyValueToT[T any](k string, m map[string]any) (T, error) {
return vt, nil
}
func AnyToT[T any](a any) (T, error) {
if a == nil {
return *new(T), clues.New("missing value")
}
pt, ok := a.(*T)
if ok {
return ptr.Val(pt), nil
}
t, ok := a.(T)
if ok {
return t, nil
}
return *new(T), clues.New(fmt.Sprintf("unexpected type: %T", a))
}

View File

@ -0,0 +1,114 @@
package path
import (
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/common/tform"
)
// ---------------------------------------------------------------------------
// Tuple
// ---------------------------------------------------------------------------
// ServiceResource holds a service + resource tuple. The tuple implies
// that the resource owns some data in the given service.
type ServiceResource struct {
Service ServiceType
ProtectedResource string
}
func MakeServiceResource(
st ServiceType,
protectedResource string,
) ServiceResource {
return ServiceResource{
Service: st,
ProtectedResource: protectedResource,
}
}
func (sr ServiceResource) validate() error {
if len(sr.ProtectedResource) == 0 {
return clues.Stack(errMissingSegment, clues.New("protected resource"))
}
return nil
}
// ---------------------------------------------------------------------------
// Collection
// ---------------------------------------------------------------------------
// NewServiceResources is a lenient constructor for building a
// new []ServiceResource. It allows the caller to pass in any
// number of arbitrary values, but will require the following:
// 1. even values must be path.ServiceType typed
// 2. odd values must be string typed
// 3. a non-zero, even number of values must be provided
func NewServiceResources(elems ...any) ([]ServiceResource, error) {
if len(elems) == 0 {
return nil, clues.New("missing service resources")
}
if len(elems)%2 == 1 {
return nil, clues.New("odd number of service resources")
}
srs := make([]ServiceResource, 0, len(elems)/2)
for i, j := 0, 1; i < len(elems); i, j = i+2, j+2 {
srv, err := tform.AnyToT[ServiceType](elems[i])
if err != nil {
return nil, clues.Wrap(err, "service")
}
pr, err := str.AnyToString(elems[j])
if err != nil {
return nil, clues.Wrap(err, "protected resource")
}
srs = append(srs, MakeServiceResource(srv, pr))
}
return srs, nil
}
func ServiceResourcesToElements(srs []ServiceResource) Elements {
es := make(Elements, 0, len(srs)*2)
for _, tuple := range srs {
es = append(es, tuple.Service.String())
es = append(es, tuple.ProtectedResource)
}
return es
}
// checks for the following:
// 1. each ServiceResource is valid
// 2. if len(srs) > 1, srs[i], srs[i+1] pass subservice checks.
func validateServiceResources(srs []ServiceResource) error {
switch len(srs) {
case 0:
return clues.Stack(errMissingSegment, clues.New("service"))
case 1:
return srs[0].validate()
}
for i, tuple := range srs {
if err := tuple.validate(); err != nil {
return err
}
if i+1 >= len(srs) {
continue
}
if err := ValidateServiceAndSubService(tuple.Service, srs[i+1].Service); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,166 @@
package path
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/tester"
)
type ServiceResourceUnitSuite struct {
tester.Suite
}
func TestServiceResourceUnitSuite(t *testing.T) {
suite.Run(t, &ServiceResourceUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ServiceResourceUnitSuite) TestNewServiceResource() {
table := []struct {
name string
input []any
expectErr assert.ErrorAssertionFunc
expectResult []ServiceResource
}{
{
name: "empty",
input: []any{},
expectErr: assert.Error,
expectResult: nil,
},
{
name: "odd elems: 1",
input: []any{ExchangeService},
expectErr: assert.Error,
expectResult: nil,
},
{
name: "odd elems: 3",
input: []any{ExchangeService, "mailbox", OneDriveService},
expectErr: assert.Error,
expectResult: nil,
},
{
name: "non-service even index",
input: []any{"foo", "bar"},
expectErr: assert.Error,
expectResult: nil,
},
{
name: "non-string odd index",
input: []any{ExchangeService, OneDriveService},
expectErr: assert.Error,
expectResult: nil,
},
{
name: "valid single",
input: []any{ExchangeService, "mailbox"},
expectErr: assert.NoError,
expectResult: []ServiceResource{{ExchangeService, "mailbox"}},
},
{
name: "valid multiple",
input: []any{ExchangeService, "mailbox", OneDriveService, "user"},
expectErr: assert.NoError,
expectResult: []ServiceResource{
{ExchangeService, "mailbox"},
{OneDriveService, "user"},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result, err := NewServiceResources(test.input...)
test.expectErr(t, err, clues.ToCore(err))
assert.Equal(t, test.expectResult, result)
})
}
}
func (suite *ServiceResourceUnitSuite) TestValidateServiceResources() {
table := []struct {
name string
srs []ServiceResource
expect assert.ErrorAssertionFunc
}{
{
name: "empty",
srs: []ServiceResource{},
expect: assert.Error,
},
{
name: "invalid resource",
srs: []ServiceResource{{ExchangeService, ""}},
expect: assert.Error,
},
{
name: "invalid subservice",
srs: []ServiceResource{
{ExchangeService, "mailbox"},
{OneDriveService, "user"},
},
expect: assert.Error,
},
{
name: "valid",
srs: []ServiceResource{
{GroupsService, "group"},
{SharePointService, "site"},
},
expect: assert.NoError,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
err := validateServiceResources(test.srs)
test.expect(t, err, clues.ToCore(err))
})
}
}
func (suite *ServiceResourceUnitSuite) TestServiceResourceElements() {
table := []struct {
name string
srs []ServiceResource
expect Elements
}{
{
name: "empty",
srs: []ServiceResource{},
expect: Elements{},
},
{
name: "single",
srs: []ServiceResource{{ExchangeService, "user"}},
expect: Elements{ExchangeService.String(), "user"},
},
{
name: "multiple",
srs: []ServiceResource{
{ExchangeService, "mailbox"},
{OneDriveService, "user"},
},
expect: Elements{
ExchangeService.String(), "mailbox",
OneDriveService.String(), "user",
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := ServiceResourcesToElements(test.srs)
// not ElementsMatch, order matters
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -29,6 +29,8 @@ const (
ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata
GroupsService // groups
GroupsMetadataService // groupsMetadata
)
func toServiceType(service string) ServiceType {
@ -51,3 +53,36 @@ func toServiceType(service string) ServiceType {
return UnknownService
}
}
// subServices is a mapping of all valid service/subService pairs.
// a subService pair occurs when one service contains a reference
// to a protected resource of another service type, and the resource
// for that second service is the identifier which is used to discover
// data. A subService relationship may imply that the subservice data
// is wholly replicated/owned by the primary service, or it may not,
// each case differs.
//
// Ex:
// - groups/<gID>/sharepoint/<siteID> => each team in groups contains a
// complete sharepoint site.
// - groups/<gID>/member/<userID> => each user in a team can own one or
// more Chats. But the group does not contain the complete user data.
var subServices = map[ServiceType]map[ServiceType]struct{}{
GroupsService: {
SharePointService: {},
},
}
func ValidateServiceAndSubService(service, subService ServiceType) error {
subs, ok := subServices[service]
if !ok {
return clues.New("unsupported service").With("service", service)
}
if _, ok := subs[subService]; !ok {
return clues.New("unknown service/subService combination").
With("service", service, "subService", subService)
}
return nil
}

View File

@ -0,0 +1,54 @@
package path
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/tester"
)
type ServiceTypeUnitSuite struct {
tester.Suite
}
func TestServiceTypeUnitSuite(t *testing.T) {
suite.Run(t, &ServiceTypeUnitSuite{Suite: tester.NewUnitSuite(t)})
}
var knownServices = []ServiceType{
UnknownService,
ExchangeService,
OneDriveService,
SharePointService,
ExchangeMetadataService,
OneDriveMetadataService,
SharePointMetadataService,
GroupsService,
GroupsMetadataService,
}
func (suite *ServiceTypeUnitSuite) TestValildateServiceAndSubService() {
table := map[ServiceType]map[ServiceType]assert.ErrorAssertionFunc{}
for _, si := range knownServices {
table[si] = map[ServiceType]assert.ErrorAssertionFunc{}
for _, sj := range knownServices {
table[si][sj] = assert.Error
}
}
// expected successful
table[GroupsService][SharePointService] = assert.NoError
for srv, ti := range table {
for sub, expect := range ti {
suite.Run(srv.String()+"-"+sub.String(), func() {
err := ValidateServiceAndSubService(srv, sub)
expect(suite.T(), err, clues.ToCore(err))
})
}
}
}