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:
parent
7aed7eba0e
commit
b7aea6bab4
@ -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))
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
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