Skip to content

Commit d88f2c5

Browse files
committed
Refactor url helper functions
1 parent aef1477 commit d88f2c5

File tree

2 files changed

+300
-305
lines changed

2 files changed

+300
-305
lines changed

utils/url/url.go

Lines changed: 99 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,92 @@
11
package url
22

33
import (
4+
netUrl "net/url"
5+
"path"
6+
"regexp"
47
"strings"
58

69
"github.com/ARM-software/golang-utils/utils/commonerrors"
710
"github.com/ARM-software/golang-utils/utils/reflection"
811
)
912

10-
const defaultPathSeparator = "/"
13+
const (
14+
defaultPathSeparator = "/"
15+
minimumPathParameterLength = 3
16+
)
17+
18+
var validParamRegex = regexp.MustCompile(`^\{[A-Za-z0-9_-]+\}$`)
19+
20+
// The expected signature for path segment matcher functions.
21+
type PathSegmentMatcherFunc = func(segmentA, segmentB string) (match bool, err error)
22+
23+
// ValidatePathParameter checks whether a path parameter is valid. An error is returned if it is invalid.
24+
// Version 3.1.0 of the OpenAPI spec provides some guidance for path parameter values (see https://spec.openapis.org/oas/v3.1.0.html#path-templating)
25+
func ValidatePathParameter(parameter string) error {
26+
if reflection.IsEmpty(parameter) || len(parameter) < minimumPathParameterLength {
27+
return commonerrors.Newf(commonerrors.ErrInvalid, "parameter segment %q must have length greater than or equal to three", parameter)
28+
}
29+
30+
unescapedSegment, err := netUrl.PathUnescape(parameter)
31+
if err != nil {
32+
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "an error occurred during path unescaping for parameter %q", parameter)
33+
}
1134

12-
// The expected function signature for checking whether two path segments match.
13-
type PathSegmentMatcherFunc = func(segmentA, segmentB string) bool
35+
if !validParamRegex.MatchString(unescapedSegment) {
36+
return commonerrors.Newf(commonerrors.ErrInvalid, "parameter %q unescaped to %q can only contain alphanumeric characters, dashes, underscores, and a single pair of braces", parameter, unescapedSegment)
37+
}
38+
39+
return nil
40+
}
1441

15-
// IsParamSegment checks whether the segment string is a path parameter as described by the OpenAPI spec (see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#path-templating).
16-
func IsParamSegment(segment string) bool {
17-
return len(segment) >= 2 && strings.HasPrefix(segment, "{") && strings.HasSuffix(segment, "}")
42+
// IsPathParameter checks whether the parameter string is a path parameter as described by the OpenAPI spec (see https://spec.openapis.org/oas/v3.0.0.html#path-templating).
43+
func IsPathParameter(parameter string) bool {
44+
return !reflection.IsEmpty(parameter) && strings.HasPrefix(parameter, "{") && strings.HasSuffix(parameter, "}")
1845
}
1946

20-
// HasMatchingPathSegments checks whether two path strings match based on their segments.
21-
func HasMatchingPathSegments(pathA, pathB string) (bool, error) {
22-
return MatchingPathSegments(pathA, pathB, func(segmentA, segmentB string) bool {
23-
return segmentA == segmentB
24-
})
47+
// HasMatchingPathSegments checks whether two path strings match based on their segments by doing a simple equality check on each path segment pair.
48+
func HasMatchingPathSegments(pathA, pathB string) (match bool, err error) {
49+
return MatchingPathSegments(pathA, pathB, BasicEqualityPathSegmentMatcher)
2550
}
2651

2752
// HasMatchingPathSegmentsWithParams is similar to HasMatchingPathSegments but also considers segments as matching if at least one of them contains a path parameter.
2853
//
2954
// HasMatchingPathSegmentsWithParams("/some/{param}/path", "/some/{param}/path") // true
3055
// HasMatchingPathSegmentsWithParams("/some/abc/path", "/some/{param}/path") // true
3156
// HasMatchingPathSegmentsWithParams("/some/abc/path", "/some/def/path") // false
32-
func HasMatchingPathSegmentsWithParams(pathA, pathB string) (bool, error) {
33-
return MatchingPathSegments(pathA, pathB, func(pathASeg, pathBSeg string) bool {
34-
switch {
35-
case IsParamSegment(pathASeg):
36-
return !reflection.IsEmpty(pathBSeg)
37-
case IsParamSegment(pathBSeg):
38-
return !reflection.IsEmpty(pathASeg)
39-
default:
40-
return pathASeg == pathBSeg
57+
func HasMatchingPathSegmentsWithParams(pathA, pathB string) (match bool, err error) {
58+
return MatchingPathSegments(pathA, pathB, BasicEqualityPathSegmentWithParamMatcher)
59+
}
60+
61+
// BasicEqualityPathSegmentMatcher is a PathSegmentMatcherFunc that performs direct string comparison of two path segments.
62+
func BasicEqualityPathSegmentMatcher(segmentA, segmentB string) (match bool, err error) {
63+
match = segmentA == segmentB
64+
return
65+
}
66+
67+
// BasicEqualityPathSegmentWithParamMatcher is a PathSegmentMatcherFunc that is similar to BasicEqualityPathSegmentMatcher but accounts for path parameter segments.
68+
func BasicEqualityPathSegmentWithParamMatcher(segmentA, segmentB string) (match bool, err error) {
69+
if IsPathParameter(segmentA) {
70+
if errValidatePathASeg := ValidatePathParameter(segmentA); errValidatePathASeg != nil {
71+
err = commonerrors.WrapErrorf(commonerrors.ErrInvalid, errValidatePathASeg, "an error occurred while validating path parameter %q", segmentA)
72+
return
4173
}
42-
})
74+
75+
match = !reflection.IsEmpty(segmentB)
76+
return
77+
}
78+
79+
if IsPathParameter(segmentB) {
80+
if errValidatePathBSeg := ValidatePathParameter(segmentB); errValidatePathBSeg != nil {
81+
err = commonerrors.WrapErrorf(commonerrors.ErrInvalid, errValidatePathBSeg, "an error occurred while validating path parameter %q", segmentB)
82+
return
83+
}
84+
85+
match = !reflection.IsEmpty(segmentA)
86+
return
87+
}
88+
89+
return BasicEqualityPathSegmentMatcher(segmentA, segmentB)
4390
}
4491

4592
// MatchingPathSegments checks whether two path strings match based on their segments using the provided matcher function.
@@ -59,14 +106,32 @@ func MatchingPathSegments(pathA, pathB string, matcherFn PathSegmentMatcherFunc)
59106
return
60107
}
61108

62-
pathASegments := SplitPath(pathA)
63-
pathBSegments := SplitPath(pathB)
109+
unescapedPathA, errPathASeg := netUrl.PathUnescape(pathA)
110+
if errPathASeg != nil {
111+
err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, errPathASeg, "an error occurred while unescaping path %q", pathA)
112+
return
113+
}
114+
115+
unescapedPathB, errPathBSeg := netUrl.PathUnescape(pathB)
116+
if errPathBSeg != nil {
117+
err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, errPathBSeg, "an error occurred while unescaping path %q", pathB)
118+
return
119+
}
120+
121+
pathASegments := SplitPath(unescapedPathA)
122+
pathBSegments := SplitPath(unescapedPathB)
64123
if len(pathASegments) != len(pathBSegments) {
65124
return
66125
}
67126

68127
for i := range pathBSegments {
69-
if !matcherFn(pathASegments[i], pathBSegments[i]) {
128+
match, err = matcherFn(pathASegments[i], pathBSegments[i])
129+
if err != nil {
130+
err = commonerrors.WrapErrorf(commonerrors.ErrUnexpected, err, "an error occurred during execution of the matcher function for path segments %q and %q", pathASegments[i], pathBSegments[i])
131+
return
132+
}
133+
134+
if !match {
70135
return
71136
}
72137
}
@@ -75,66 +140,19 @@ func MatchingPathSegments(pathA, pathB string, matcherFn PathSegmentMatcherFunc)
75140
return
76141
}
77142

78-
// SplitPath returns a slice containing the individual segments that make up the path string. It looks for the default "/" path separator when splitting.
79-
func SplitPath(path string) []string {
80-
return SplitPathWithSeparator(path, defaultPathSeparator)
81-
}
82-
83-
// SplitPathWithSeparator is similar to SplitPath but allows for specifying the path separator to look for when splitting.
84-
func SplitPathWithSeparator(path string, separator string) []string {
85-
path = strings.TrimSpace(path)
86-
if reflection.IsEmpty(path) || path == separator {
143+
// SplitPath returns a slice containing the individual segments that make up the path string p.
144+
// It looks for the default forward slash path separator when splitting.
145+
func SplitPath(p string) []string {
146+
p = strings.TrimSpace(p)
147+
if reflection.IsEmpty(p) || p == defaultPathSeparator {
87148
return nil
88149
}
89150

90-
path = strings.Trim(path, separator)
91-
segments := strings.Split(path, separator)
92-
out := segments[:0]
93-
for _, p := range segments {
94-
if !reflection.IsEmpty(p) {
95-
out = append(out, p)
96-
}
151+
p = path.Clean(p)
152+
p = strings.Trim(p, defaultPathSeparator)
153+
if reflection.IsEmpty(p) {
154+
return []string{}
97155
}
98-
return out
99-
}
100-
101-
// JoinPaths returns a single concatenated path string from the supplied paths and correctly sets the default "/" separator between them.
102-
func JoinPaths(paths ...string) (joinedPath string, err error) {
103-
return JoinPathsWithSeparator(defaultPathSeparator, paths...)
104-
}
105156

106-
// JoinPathsWithSeparator is similar to JoinPaths but allows for specifying the path separator to use.
107-
func JoinPathsWithSeparator(separator string, paths ...string) (joinedPath string, err error) {
108-
if paths == nil {
109-
err = commonerrors.UndefinedVariable("paths")
110-
return
111-
}
112-
if len(paths) == 0 {
113-
return
114-
}
115-
if len(paths) == 1 {
116-
joinedPath = paths[0]
117-
return
118-
}
119-
120-
if reflection.IsEmpty(separator) {
121-
separator = defaultPathSeparator
122-
}
123-
124-
joinedPath = paths[0]
125-
for _, p := range paths[1:] {
126-
pathAHasSlashSuffix := strings.HasSuffix(joinedPath, separator)
127-
pathBHasSlashPrefix := strings.HasPrefix(p, separator)
128-
129-
switch {
130-
case pathAHasSlashSuffix && pathBHasSlashPrefix:
131-
joinedPath += p[1:]
132-
case !pathAHasSlashSuffix && !pathBHasSlashPrefix:
133-
joinedPath += separator + p
134-
default:
135-
joinedPath += p
136-
}
137-
}
138-
139-
return
157+
return strings.Split(p, defaultPathSeparator)
140158
}

0 commit comments

Comments
 (0)