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:
ashmrtn 2022-08-30 15:45:47 -07:00 committed by GitHub
parent 67bc038c55
commit d1bf2b90a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 386 additions and 4 deletions

View 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]]
}

View File

@ -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.

View 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 ""
}

View 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())
})
}
}

View 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]]
}