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)) + }) + } + } +}