Re-add logic for getting Exchange mail paths (#648)
* Re-add logic for getting Exchange mail paths Paths can either be for an item (email) or a folder. Returned struct provides safe access to underlying information in the path once it is created. * Refine tests, add struct comment Add test to make sure Item() and Folder() work properly for a resource that has no parent folder, just the prefix stuff. * Rework resource-specific paths based on comments * use a single type to represent resource specific paths * use service/category enums to denote the resource the path represents * update tests to check for service/category of the path * rename exchange_path*.go -> resource_path*.go
This commit is contained in:
parent
67bc038c55
commit
d1bf2b90a6
24
src/internal/path/categorytype_string.go
Normal file
24
src/internal/path/categorytype_string.go
Normal file
@ -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]]
|
||||||
|
}
|
||||||
@ -51,16 +51,17 @@ var charactersToEscape = map[rune]struct{}{
|
|||||||
escapeCharacter: {},
|
escapeCharacter: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ashmrtn): Getting the category should either be through type-switches or
|
var errMissingSegment = errors.New("missing required path element")
|
||||||
// through a function, but if it's a function it should re-use existing enums
|
|
||||||
// for resource types.
|
|
||||||
// For now, adding generic functions to pull information from segments.
|
// For now, adding generic functions to pull information from segments.
|
||||||
// Resources that don't have the requested information should return an empty
|
// Resources that don't have the requested information should return an empty
|
||||||
// string.
|
// string.
|
||||||
type Path interface {
|
type Path interface {
|
||||||
String() string
|
String() string
|
||||||
|
Service() ServiceType
|
||||||
|
Category() CategoryType
|
||||||
Tenant() string
|
Tenant() string
|
||||||
User() string
|
ResourceOwner() string
|
||||||
Folder() string
|
Folder() string
|
||||||
Item() string
|
Item() string
|
||||||
}
|
}
|
||||||
@ -150,6 +151,72 @@ func (pb Builder) join(start, end int) string {
|
|||||||
return join(pb.elements[start:end])
|
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
|
// escapeElement takes a single path element and escapes all characters that
|
||||||
// require an escape sequence. If there are no characters that need escaping,
|
// require an escape sequence. If there are no characters that need escaping,
|
||||||
// the input is returned unchanged.
|
// the input is returned unchanged.
|
||||||
|
|||||||
78
src/internal/path/resource_path.go
Normal file
78
src/internal/path/resource_path.go
Normal file
@ -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:
|
||||||
|
// <tenant ID>/<service>/<resource owner ID>/<category>
|
||||||
|
// 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 ""
|
||||||
|
}
|
||||||
189
src/internal/path/resource_path_test.go
Normal file
189
src/internal/path/resource_path_test.go
Normal file
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/internal/path/servicetype_string.go
Normal file
24
src/internal/path/servicetype_string.go
Normal file
@ -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]]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user