diff --git a/src/internal/path/categorytype_string.go b/src/internal/path/categorytype_string.go new file mode 100644 index 000000000..a3ad8aa05 --- /dev/null +++ b/src/internal/path/categorytype_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=CategoryType -linecomment"; DO NOT EDIT. + +package path + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[UnknownCategory-0] + _ = x[EmailCategory-1] +} + +const _CategoryType_name = "UnknownCategoryemail" + +var _CategoryType_index = [...]uint8{0, 15, 20} + +func (i CategoryType) String() string { + if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) { + return "CategoryType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _CategoryType_name[_CategoryType_index[i]:_CategoryType_index[i+1]] +} diff --git a/src/internal/path/path.go b/src/internal/path/path.go index 099aa4ae0..f158473aa 100644 --- a/src/internal/path/path.go +++ b/src/internal/path/path.go @@ -51,16 +51,17 @@ var charactersToEscape = map[rune]struct{}{ escapeCharacter: {}, } -// TODO(ashmrtn): Getting the category should either be through type-switches or -// through a function, but if it's a function it should re-use existing enums -// for resource types. +var errMissingSegment = errors.New("missing required path element") + // For now, adding generic functions to pull information from segments. // Resources that don't have the requested information should return an empty // string. type Path interface { String() string + Service() ServiceType + Category() CategoryType Tenant() string - User() string + ResourceOwner() string Folder() string Item() string } @@ -150,6 +151,72 @@ func (pb Builder) join(start, end int) string { return join(pb.elements[start:end]) } +func (pb Builder) verifyPrefix(tenant, resourceOwner string) error { + if len(tenant) == 0 { + return errors.Wrap(errMissingSegment, "tenant") + } + + if len(resourceOwner) == 0 { + return errors.Wrap(errMissingSegment, "user") + } + + if len(pb.elements) == 0 { + return errors.New("missing path beyond prefix") + } + + return nil +} + +func (pb Builder) withPrefix(elements ...string) *Builder { + res := Builder{}.Append(elements...) + res.elements = append(res.elements, pb.elements...) + + return res +} + +// ToDataLayerExchangeMailFolder returns a Path for an Exchange mail folder +// resource with information useful to the data layer. This includes prefix +// elements of the path such as the tenant ID, user ID, service, and service +// category. +func (pb Builder) ToDataLayerExchangeMailFolder(tenant, user string) (Path, error) { + if err := pb.verifyPrefix(tenant, user); err != nil { + return nil, err + } + + return &dataLayerResourcePath{ + Builder: *pb.withPrefix( + tenant, + ExchangeService.String(), + user, + EmailCategory.String(), + ), + service: ExchangeService, + category: EmailCategory, + }, nil +} + +// ToDataLayerExchangeMailFolder returns a Path for an Exchange mail item +// resource with information useful to the data layer. This includes prefix +// elements of the path such as the tenant ID, user ID, service, and service +// category. +func (pb Builder) ToDataLayerExchangeMailItem(tenant, user string) (Path, error) { + if err := pb.verifyPrefix(tenant, user); err != nil { + return nil, err + } + + return &dataLayerResourcePath{ + Builder: *pb.withPrefix( + tenant, + ExchangeService.String(), + user, + EmailCategory.String(), + ), + service: ExchangeService, + category: EmailCategory, + hasItem: true, + }, nil +} + // escapeElement takes a single path element and escapes all characters that // require an escape sequence. If there are no characters that need escaping, // the input is returned unchanged. diff --git a/src/internal/path/resource_path.go b/src/internal/path/resource_path.go new file mode 100644 index 000000000..505a0c6ba --- /dev/null +++ b/src/internal/path/resource_path.go @@ -0,0 +1,78 @@ +package path + +type ServiceType int + +//go:generate stringer -type=ServiceType -linecomment +const ( + UnknownService ServiceType = iota + ExchangeService // exchange +) + +type CategoryType int + +//go:generate stringer -type=CategoryType -linecomment +const ( + UnknownCategory CategoryType = iota + EmailCategory // email +) + +// dataLayerResourcePath allows callers to extract information from a +// resource-specific path. This struct is unexported so that callers are +// forced to use the pre-defined constructors, making it impossible to create a +// dataLayerResourcePath with invalid service/category combinations. +// +// All dataLayerResourcePaths start with the same prefix: +// /// +// which allows extracting high-level information from the path. The path +// elements after this prefix represent zero or more folders and, if the path +// refers to a file or item, an item ID. A valid dataLayerResourcePath must have +// at least one folder or an item so that the resulting path has at least one +// element after the prefix. +type dataLayerResourcePath struct { + Builder + category CategoryType + service ServiceType + hasItem bool +} + +// Tenant returns the tenant ID embedded in the dataLayerResourcePath. +func (rp dataLayerResourcePath) Tenant() string { + return rp.Builder.elements[0] +} + +// Service returns the ServiceType embedded in the dataLayerResourcePath. +func (rp dataLayerResourcePath) Service() ServiceType { + return rp.service +} + +// Category returns the CategoryType embedded in the dataLayerResourcePath. +func (rp dataLayerResourcePath) Category() CategoryType { + return rp.category +} + +// ResourceOwner returns the user ID or group ID embedded in the +// dataLayerResourcePath. +func (rp dataLayerResourcePath) ResourceOwner() string { + return rp.Builder.elements[2] +} + +// Folder returns the folder segment embedded in the dataLayerResourcePath. +func (rp dataLayerResourcePath) Folder() string { + endIdx := len(rp.Builder.elements) + + if rp.hasItem { + endIdx-- + } + + return rp.Builder.join(4, endIdx) +} + +// Item returns the item embedded in the dataLayerResourcePath if the path +// refers to an item. +func (rp dataLayerResourcePath) Item() string { + if rp.hasItem { + return rp.Builder.elements[len(rp.Builder.elements)-1] + } + + return "" +} diff --git a/src/internal/path/resource_path_test.go b/src/internal/path/resource_path_test.go new file mode 100644 index 000000000..8e735df95 --- /dev/null +++ b/src/internal/path/resource_path_test.go @@ -0,0 +1,189 @@ +package path_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/internal/path" +) + +const ( + testTenant = "aTenant" + testUser = "aUser" +) + +var ( + // Purposely doesn't have characters that need escaping so it can be easily + // computed using strings.Join(). + rest = []string{"some", "folder", "path", "with", "possible", "item"} + + missingInfo = []struct { + name string + tenant string + user string + rest []string + }{ + { + name: "NoTenant", + tenant: "", + user: testUser, + rest: rest, + }, + { + name: "NoResourceOwner", + tenant: testTenant, + user: "", + rest: rest, + }, + { + name: "NoFolderOrItem", + tenant: testTenant, + user: testUser, + rest: nil, + }, + } + + modes = []struct { + name string + builderFunc func(b path.Builder, tenant, user string) (path.Path, error) + expectedFolder string + expectedItem string + expectedService path.ServiceType + expectedCategory path.CategoryType + }{ + { + name: "ExchangeMailFolder", + builderFunc: path.Builder.ToDataLayerExchangeMailFolder, + expectedFolder: strings.Join(rest, "/"), + expectedItem: "", + expectedService: path.ExchangeService, + expectedCategory: path.EmailCategory, + }, + { + name: "ExchangeMailItem", + builderFunc: path.Builder.ToDataLayerExchangeMailItem, + expectedFolder: strings.Join(rest[0:len(rest)-1], "/"), + expectedItem: rest[len(rest)-1], + expectedService: path.ExchangeService, + expectedCategory: path.EmailCategory, + }, + } +) + +type DataLayerResourcePath struct { + suite.Suite +} + +func TestDataLayerResourcePath(t *testing.T) { + suite.Run(t, new(DataLayerResourcePath)) +} + +func (suite *DataLayerResourcePath) TestMissingInfoErrors() { + for _, m := range modes { + suite.T().Run(m.name, func(tOuter *testing.T) { + for _, test := range missingInfo { + tOuter.Run(test.name, func(t *testing.T) { + b := path.Builder{}.Append(test.rest...) + + _, err := m.builderFunc(*b, test.tenant, test.user) + assert.Error(t, err) + }) + } + }) + } +} + +func (suite *DataLayerResourcePath) TestMailItemNoFolder() { + t := suite.T() + item := "item" + b := path.Builder{}.Append(item) + + p, err := b.ToDataLayerExchangeMailItem(testTenant, testUser) + require.NoError(t, err) + + assert.Empty(t, p.Folder()) + assert.Equal(t, item, p.Item()) +} + +type PopulatedDataLayerResourcePath struct { + suite.Suite + b *path.Builder +} + +func TestPopulatedDataLayerResourcePath(t *testing.T) { + suite.Run(t, new(PopulatedDataLayerResourcePath)) +} + +func (suite *PopulatedDataLayerResourcePath) SetupSuite() { + suite.b = path.Builder{}.Append(rest...) +} + +func (suite *PopulatedDataLayerResourcePath) TestTenant() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, testTenant, p.Tenant()) + }) + } +} + +func (suite *PopulatedDataLayerResourcePath) TestService() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, m.expectedService, p.Service()) + }) + } +} + +func (suite *PopulatedDataLayerResourcePath) TestCategory() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, m.expectedCategory, p.Category()) + }) + } +} + +func (suite *PopulatedDataLayerResourcePath) TestResourceOwner() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, testUser, p.ResourceOwner()) + }) + } +} + +func (suite *PopulatedDataLayerResourcePath) TestFolder() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, m.expectedFolder, p.Folder()) + }) + } +} + +func (suite *PopulatedDataLayerResourcePath) TestItem() { + for _, m := range modes { + suite.T().Run(m.name, func(t *testing.T) { + p, err := m.builderFunc(*suite.b, testTenant, testUser) + require.NoError(t, err) + + assert.Equal(t, m.expectedItem, p.Item()) + }) + } +} diff --git a/src/internal/path/servicetype_string.go b/src/internal/path/servicetype_string.go new file mode 100644 index 000000000..208bba5aa --- /dev/null +++ b/src/internal/path/servicetype_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=ServiceType -linecomment"; DO NOT EDIT. + +package path + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[UnknownService-0] + _ = x[ExchangeService-1] +} + +const _ServiceType_name = "UnknownServiceexchange" + +var _ServiceType_index = [...]uint8{0, 14, 22} + +func (i ServiceType) String() string { + if i < 0 || i >= ServiceType(len(_ServiceType_index)-1) { + return "ServiceType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ServiceType_name[_ServiceType_index[i]:_ServiceType_index[i+1]] +}