From 5f7f1092ec834a827fd1b1de86e58cb849833c31 Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Wed, 9 Aug 2023 18:37:11 -0600 Subject: [PATCH] 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. --- src/internal/common/tform/tform.go | 19 +++ src/pkg/path/service_resource.go | 114 ++++++++++++++++++ src/pkg/path/service_resource_test.go | 166 ++++++++++++++++++++++++++ src/pkg/path/service_type.go | 35 ++++++ src/pkg/path/service_type_test.go | 54 +++++++++ 5 files changed, 388 insertions(+) create mode 100644 src/pkg/path/service_resource.go create mode 100644 src/pkg/path/service_resource_test.go create mode 100644 src/pkg/path/service_type_test.go diff --git a/src/internal/common/tform/tform.go b/src/internal/common/tform/tform.go index 661d543d4..ab83772b5 100644 --- a/src/internal/common/tform/tform.go +++ b/src/internal/common/tform/tform.go @@ -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)) +} diff --git a/src/pkg/path/service_resource.go b/src/pkg/path/service_resource.go new file mode 100644 index 000000000..bbfc3f4c7 --- /dev/null +++ b/src/pkg/path/service_resource.go @@ -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 +} diff --git a/src/pkg/path/service_resource_test.go b/src/pkg/path/service_resource_test.go new file mode 100644 index 000000000..78dea7aab --- /dev/null +++ b/src/pkg/path/service_resource_test.go @@ -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) + }) + } +} diff --git a/src/pkg/path/service_type.go b/src/pkg/path/service_type.go index 318de2c62..009ed75f4 100644 --- a/src/pkg/path/service_type.go +++ b/src/pkg/path/service_type.go @@ -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//sharepoint/ => each team in groups contains a +// complete sharepoint site. +// - groups//member/ => 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 +} diff --git a/src/pkg/path/service_type_test.go b/src/pkg/path/service_type_test.go new file mode 100644 index 000000000..8d7f77233 --- /dev/null +++ b/src/pkg/path/service_type_test.go @@ -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)) + }) + } + } +}