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:
parent
aa3cc21cdb
commit
9fe6ca7f56
@ -41,6 +41,8 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const templateErrPathParsing = "parsing resource path from %s"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
escapeCharacter = '\\'
|
escapeCharacter = '\\'
|
||||||
pathSeparator = '/'
|
pathSeparator = '/'
|
||||||
@ -217,6 +219,43 @@ func (pb Builder) ToDataLayerExchangeMailItem(tenant, user string) (Path, error)
|
|||||||
}, nil
|
}, 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
|
// 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.
|
||||||
@ -346,3 +385,56 @@ func join(elements []string) string {
|
|||||||
// '\' according to the escaping rules.
|
// '\' according to the escaping rules.
|
||||||
return strings.Join(elements, string(pathSeparator))
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user