Logic to create Path structs from strings (#691)

## Description

* logic to split escaped path strings into elements
* wire up validity checks/struct construction
* tests for both invalid and valid path strings

## Type of change

Please check the type of change your PR introduces:
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 🐹 Trivial/Minor

## Issue(s)
<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->

part of #671 
merge after:
* #648
* #689 
* #690 

## Test Plan

<!-- How will this be tested prior to merging.-->

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2022-08-31 13:21:46 -07:00 committed by GitHub
parent aa3cc21cdb
commit 9fe6ca7f56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 257 additions and 0 deletions

View File

@ -41,6 +41,8 @@ import (
"github.com/pkg/errors"
)
const templateErrPathParsing = "parsing resource path from %s"
const (
escapeCharacter = '\\'
pathSeparator = '/'
@ -217,6 +219,43 @@ func (pb Builder) ToDataLayerExchangeMailItem(tenant, user string) (Path, error)
}, nil
}
// FromDataLayerPath parses the escaped path p, validates the elements in p
// match a resource-specific path format, and returns a Path struct for that
// resource-specific type. If p does not match any resource-specific paths or
// is malformed returns an error.
func FromDataLayerPath(p string, isItem bool) (Path, error) {
p = trimTrailingSlash(p)
// If p was just the path separator then it will be empty now.
if len(p) == 0 {
return nil, errors.Errorf("logically empty path given: %s", p)
}
// Turn into a Builder to reuse code that ignores empty elements.
pb, err := Builder{}.UnescapeAndAppend(split(p)...)
if err != nil {
return nil, errors.Wrapf(err, templateErrPathParsing, p)
}
if len(pb.elements) < 5 {
return nil, errors.Errorf("path has too few segments: %s", p)
}
service, category, err := validateServiceAndCategory(
pb.elements[1],
pb.elements[3],
)
if err != nil {
return nil, errors.Wrapf(err, templateErrPathParsing, p)
}
return &dataLayerResourcePath{
Builder: *pb,
service: service,
category: category,
hasItem: isItem,
}, 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.
@ -346,3 +385,56 @@ func join(elements []string) string {
// '\' according to the escaping rules.
return strings.Join(elements, string(pathSeparator))
}
// split takes an escaped string and returns a slice of path elements. The
// string is split on the path separator according to the escaping rules. The
// provided string must not contain an unescaped trailing path separator.
func split(segment string) []string {
res := make([]string, 0)
numEscapes := 0
startIdx := 0
// Start with true to ignore leading separator.
prevWasSeparator := true
for i, c := range segment {
if c == escapeCharacter {
prevWasSeparator = false
numEscapes++
continue
}
if c != pathSeparator {
prevWasSeparator = false
numEscapes = 0
continue
}
// Remaining is just path separator handling.
if numEscapes%2 != 0 {
// This is an escaped separator.
prevWasSeparator = false
numEscapes = 0
continue
}
// Ignore leading separator characters and don't add elements that would
// be empty.
if !prevWasSeparator {
res = append(res, segment[startIdx:i])
}
// We don't want to include the path separator in the result.
startIdx = i + 1
prevWasSeparator = true
numEscapes = 0
}
// Add the final segment because the loop above won't catch it. There should
// be no trailing separator character.
res = append(res, segment[startIdx:])
return res
}

View File

@ -293,3 +293,168 @@ func (suite *PathUnitSuite) TestTrailingEscapeChar() {
})
}
}
func (suite *PathUnitSuite) TestFromStringErrors() {
table := []struct {
name string
escapedPath string
}{
{
name: "TooFewElements",
escapedPath: `some/short/path`,
},
{
name: "TooFewElementsEmptyElement",
escapedPath: `tenant/exchange//email/folder`,
},
{
name: "BadEscapeSequence",
escapedPath: `tenant/exchange/user/email/folder\a`,
},
{
name: "TrailingEscapeCharacter",
escapedPath: `tenant/exchange/user/email/folder\`,
},
{
name: "UnknownService",
escapedPath: `tenant/badService/user/email/folder`,
},
{
name: "UnknownCategory",
escapedPath: `tenant/exchange/user/badCategory/folder`,
},
{
name: "NoFolderOrItem",
escapedPath: `tenant/exchange/user/email`,
},
{
name: "EmptyPath",
escapedPath: ``,
},
{
name: "JustPathSeparator",
escapedPath: `/`,
},
{
name: "JustMultiplePathSeparators",
escapedPath: `//`,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
_, err := FromDataLayerPath(test.escapedPath, false)
assert.Error(t, err)
})
}
}
func (suite *PathUnitSuite) TestFromString() {
const (
testTenant = "tenant"
testUser = "user"
testElement1 = "folder"
testElement2 = "folder2"
testElement3 = "other"
)
isItem := []struct {
name string
isItem bool
}{
{
name: "Folder",
isItem: false,
},
{
name: "Item",
isItem: true,
},
}
table := []struct {
name string
// Should have placeholders of '%s' for service and category.
unescapedPath string
// Expected result for Folder() if path is marked as a folder.
expectedFolder string
// Expected result for Item() if path is marked as an item.
expectedItem string
// Expected result for Folder() if path is marked as an item.
expectedItemFolder string
}{
{
name: "BasicPath",
unescapedPath: fmt.Sprintf(
"%s/%%s/%s/%%s/%s/%s/%s",
testTenant,
testUser,
testElement1,
testElement2,
testElement3,
),
expectedFolder: fmt.Sprintf(
"%s/%s/%s",
testElement1,
testElement2,
testElement3,
),
expectedItem: testElement3,
expectedItemFolder: fmt.Sprintf(
"%s/%s",
testElement1,
testElement2,
),
},
{
name: "PathWithEmptyElements",
unescapedPath: fmt.Sprintf(
"/%s//%%s//%s//%%s//%s///%s//%s//",
testTenant,
testUser,
testElement1,
testElement2,
testElement3,
),
expectedFolder: fmt.Sprintf(
"%s/%s/%s",
testElement1,
testElement2,
testElement3,
),
expectedItem: testElement3,
expectedItemFolder: fmt.Sprintf(
"%s/%s",
testElement1,
testElement2,
),
},
}
for service, cats := range serviceCategories {
for cat := range cats {
for _, item := range isItem {
suite.T().Run(fmt.Sprintf("%s-%s-%s", service, cat, item.name), func(t1 *testing.T) {
for _, test := range table {
t1.Run(test.name, func(t *testing.T) {
testPath := fmt.Sprintf(test.unescapedPath, service, cat)
p, err := FromDataLayerPath(testPath, item.isItem)
require.NoError(t, err)
assert.Equal(t, service, p.Service())
assert.Equal(t, cat, p.Category())
assert.Equal(t, testTenant, p.Tenant())
assert.Equal(t, testUser, p.ResourceOwner())
if !item.isItem {
assert.Equal(t, test.expectedFolder, p.Folder())
} else {
assert.Equal(t, test.expectedItemFolder, p.Folder())
assert.Equal(t, test.expectedItem, p.Item())
}
})
}
})
}
}
}
}