11package url
22
33import (
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