Compare commits
1 Commits
main
...
3993-multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7aea6bab4 |
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AnyValueToT[T any](k string, m map[string]any) (T, error) {
|
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
|
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))
|
||||||
|
}
|
||||||
|
|||||||
114
src/pkg/path/service_resource.go
Normal file
114
src/pkg/path/service_resource.go
Normal 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
|
||||||
|
}
|
||||||
166
src/pkg/path/service_resource_test.go
Normal file
166
src/pkg/path/service_resource_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,8 @@ const (
|
|||||||
ExchangeMetadataService // exchangeMetadata
|
ExchangeMetadataService // exchangeMetadata
|
||||||
OneDriveMetadataService // onedriveMetadata
|
OneDriveMetadataService // onedriveMetadata
|
||||||
SharePointMetadataService // sharepointMetadata
|
SharePointMetadataService // sharepointMetadata
|
||||||
|
GroupsService // groups
|
||||||
|
GroupsMetadataService // groupsMetadata
|
||||||
)
|
)
|
||||||
|
|
||||||
func toServiceType(service string) ServiceType {
|
func toServiceType(service string) ServiceType {
|
||||||
@ -51,3 +53,36 @@ func toServiceType(service string) ServiceType {
|
|||||||
return UnknownService
|
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
|
||||||
|
}
|
||||||
|
|||||||
54
src/pkg/path/service_type_test.go
Normal file
54
src/pkg/path/service_type_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user