Skip to content

Commit 02463e4

Browse files
SAPRD2daveshanley
authored andcommitted
implemented path parameter matching
1 parent 8e98662 commit 02463e4

File tree

7 files changed

+354
-160
lines changed

7 files changed

+354
-160
lines changed

helpers/regex_maker.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package helpers
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
func GetRegexForPath(tpl string) (*regexp.Regexp, error) {
11+
12+
// Check if it is well-formed.
13+
idxs, errBraces := BraceIndices(tpl)
14+
if errBraces != nil {
15+
return nil, errBraces
16+
}
17+
18+
// Backup the original.
19+
template := tpl
20+
21+
// Now let's parse it.
22+
defaultPattern := "[^/]+"
23+
24+
pattern := bytes.NewBufferString("^")
25+
var end int
26+
27+
for i := 0; i < len(idxs); i += 2 {
28+
29+
// Set all values we are interested in.
30+
raw := tpl[end:idxs[i]]
31+
end = idxs[i+1]
32+
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
33+
name := parts[0]
34+
patt := defaultPattern
35+
if len(parts) == 2 {
36+
patt = parts[1]
37+
}
38+
39+
// Name or pattern can't be empty.
40+
if name == "" || patt == "" {
41+
return nil, fmt.Errorf("mux: missing name or pattern in %q", tpl[idxs[i]:end])
42+
}
43+
44+
// Build the regexp pattern.
45+
_, err := fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
}
51+
52+
// Add the remaining.
53+
raw := tpl[end:]
54+
pattern.WriteString(regexp.QuoteMeta(raw))
55+
56+
pattern.WriteByte('$')
57+
58+
// Compile full regexp.
59+
reg, errCompile := regexp.Compile(pattern.String())
60+
if errCompile != nil {
61+
return nil, errCompile
62+
}
63+
64+
// Check for capturing groups which used to work in older versions
65+
if reg.NumSubexp() != len(idxs)/2 {
66+
return nil, fmt.Errorf(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
67+
}
68+
69+
// Done!
70+
return reg, nil
71+
}
72+
73+
func BraceIndices(s string) ([]int, error) {
74+
var level, idx int
75+
var idxs []int
76+
for i := 0; i < len(s); i++ {
77+
switch s[i] {
78+
case '{':
79+
if level++; level == 1 {
80+
idx = i
81+
}
82+
case '}':
83+
if level--; level == 0 {
84+
idxs = append(idxs, idx, i+1)
85+
} else if level < 0 {
86+
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
87+
}
88+
}
89+
}
90+
if level != 0 {
91+
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
92+
}
93+
return idxs, nil
94+
}

helpers/regex_maker_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package helpers
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGetRegexForPath(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
tpl string
11+
wantErr bool
12+
wantExpr string
13+
}{
14+
{
15+
name: "well-formed template with default pattern",
16+
tpl: "/orders/{id}",
17+
wantErr: false,
18+
wantExpr: "^/orders/([^/]+)$",
19+
},
20+
{
21+
name: "well-formed template with custom pattern",
22+
tpl: "/orders/{id:[0-9]+}",
23+
wantErr: false,
24+
wantExpr: "^/orders/([0-9]+)$",
25+
},
26+
{
27+
name: "missing name in template",
28+
tpl: "/orders/{:pattern}",
29+
wantErr: true,
30+
},
31+
{
32+
name: "missing pattern in template",
33+
tpl: "/orders/{name:}",
34+
wantErr: true,
35+
},
36+
{
37+
name: "unbalanced braces in template",
38+
tpl: "/orders/{id",
39+
wantErr: true,
40+
},
41+
{
42+
name: "unbalanced braces in template",
43+
tpl: "/orders/id}",
44+
wantErr: true,
45+
},
46+
{
47+
name: "template with multiple variables",
48+
tpl: "/orders/{id:[0-9]+}/items/{itemId}",
49+
wantErr: false,
50+
wantExpr: "^/orders/([0-9]+)/items/([^/]+)$",
51+
},
52+
{
53+
name: "OData formatted URL with single quotes",
54+
tpl: "/entities('{id}')",
55+
wantErr: false,
56+
wantExpr: "^/entities\\('([^/]+)'\\)$",
57+
},
58+
{
59+
name: "OData formatted URL with custom pattern",
60+
tpl: "/entities('{id:[0-9]+}')",
61+
wantErr: false,
62+
wantExpr: "^/entities\\('([0-9]+)'\\)$",
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
got, err := GetRegexForPath(tt.tpl)
69+
if (err != nil) != tt.wantErr {
70+
t.Errorf("GetRegexForPath() error = %v, wantErr %v", err, tt.wantErr)
71+
return
72+
}
73+
if !tt.wantErr && got.String() != tt.wantExpr {
74+
t.Errorf("GetRegexForPath() = %v, want %v", got.String(), tt.wantExpr)
75+
}
76+
})
77+
}
78+
}
79+
80+
func TestBraceIndices(t *testing.T) {
81+
tests := []struct {
82+
name string
83+
s string
84+
want []int
85+
wantErr bool
86+
}{
87+
{
88+
name: "well-formed braces",
89+
s: "/orders/{id}/items/{itemId}",
90+
want: []int{8, 12, 19, 27},
91+
wantErr: false,
92+
},
93+
{
94+
name: "unbalanced braces",
95+
s: "/orders/{id/items/{itemId}",
96+
wantErr: true,
97+
},
98+
{
99+
name: "unbalanced braces",
100+
s: "/orders/{id}/items/{itemId",
101+
wantErr: true,
102+
},
103+
{
104+
name: "no braces",
105+
s: "/orders/id/items/itemId",
106+
want: []int{},
107+
wantErr: false,
108+
},
109+
{
110+
name: "OData formatted URL with single quotes",
111+
s: "/entities('{id}')",
112+
want: []int{11, 15},
113+
wantErr: false,
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
got, err := BraceIndices(tt.s)
120+
if (err != nil) != tt.wantErr {
121+
t.Errorf("BraceIndices() error = %v, wantErr %v", err, tt.wantErr)
122+
return
123+
}
124+
if !tt.wantErr && !equal(got, tt.want) {
125+
t.Errorf("BraceIndices() = %v, want %v", got, tt.want)
126+
}
127+
})
128+
}
129+
}
130+
131+
func equal(a, b []int) bool {
132+
if len(a) != len(b) {
133+
return false
134+
}
135+
for i := range a {
136+
if a[i] != b[i] {
137+
return false
138+
}
139+
}
140+
return true
141+
}

parameters/path_parameters.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package parameters
66
import (
77
"fmt"
88
"net/http"
9+
"regexp"
910
"strconv"
1011
"strings"
1112

@@ -54,14 +55,34 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
5455
if pathSegments[x] == "" { // skip empty segments
5556
continue
5657
}
57-
i := strings.IndexRune(pathSegments[x], '{')
58-
if i > -1 {
58+
59+
r, err := helpers.GetRegexForPath(pathSegments[x])
60+
if err != nil {
61+
continue
62+
}
63+
64+
re := regexp.MustCompile(r.String())
65+
matches := re.FindStringSubmatch(submittedSegments[x])
66+
matches = matches[1:]
67+
68+
// Check if it is well-formed.
69+
idxs, errBraces := helpers.BraceIndices(pathSegments[x])
70+
if errBraces != nil {
71+
continue
72+
}
73+
74+
idx := 0
75+
76+
for _, match := range matches {
77+
5978
isMatrix := false
6079
isLabel := false
6180
// isExplode := false
6281
isSimple := true
63-
paramTemplate := pathSegments[x][i+1 : len(pathSegments[x])-1]
82+
paramTemplate := pathSegments[x][idxs[idx]+1 : idxs[idx+1]-1]
83+
idx += 2 // move to the next brace pair
6484
paramName := paramTemplate
85+
6586
// check for an asterisk on the end of the parameter (explode)
6687
if strings.HasSuffix(paramTemplate, helpers.Asterisk) {
6788
// isExplode = true
@@ -83,12 +104,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
83104
continue
84105
}
85106

86-
paramValue := ""
87-
88-
// extract the parameter value from the path.
89-
if x < len(submittedSegments) {
90-
paramValue = submittedSegments[x]
91-
}
107+
paramValue := match
92108

93109
if paramValue == "" {
94110
// Mandatory path parameter cannot be empty

parameters/path_parameters_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,3 +1443,60 @@ paths:
14431443
assert.False(t, valid)
14441444
assert.Len(t, errors, 1)
14451445
}
1446+
1447+
func TestNewValidator_ODataFormattedOpenAPISpecs(t *testing.T) {
1448+
1449+
spec := `openapi: 3.0.0
1450+
paths:
1451+
/entities('{Entity}'):
1452+
parameters:
1453+
- description: 'key: Entity'
1454+
in: path
1455+
name: Entity
1456+
required: true
1457+
schema:
1458+
type: integer
1459+
get:
1460+
operationId: one
1461+
/orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}'):
1462+
parameters:
1463+
- name: RelationshipNumber
1464+
in: path
1465+
required: true
1466+
schema:
1467+
type: integer
1468+
- name: ValidityEndDate
1469+
in: path
1470+
required: true
1471+
schema:
1472+
type: string
1473+
get:
1474+
operationId: one
1475+
`
1476+
doc, _ := libopenapi.NewDocument([]byte(spec))
1477+
1478+
m, _ := doc.BuildV3Model()
1479+
1480+
v := NewParameterValidator(&m.Model)
1481+
1482+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil)
1483+
1484+
valid, errors := v.ValidatePathParams(request)
1485+
1486+
assert.True(t, valid)
1487+
assert.Len(t, errors, 0)
1488+
1489+
request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='1234',ValidityEndDate=datetime'1492041600000')", nil)
1490+
1491+
valid, errors = v.ValidatePathParams(request)
1492+
1493+
assert.True(t, valid)
1494+
assert.Len(t, errors, 0)
1495+
1496+
request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil)
1497+
1498+
valid, errors = v.ValidatePathParams(request)
1499+
assert.False(t, valid)
1500+
assert.Len(t, errors, 1)
1501+
1502+
}

0 commit comments

Comments
 (0)