Skip to content

Commit 8e98662

Browse files
SAPRD2daveshanley
authored andcommitted
support for path validation for odata-formatted openapi specs
1 parent 2b2a132 commit 8e98662

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed

paths/paths.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
package paths
55

66
import (
7+
"bytes"
78
"fmt"
89
"net/http"
910
"net/url"
1011
"path/filepath"
12+
"regexp"
1113
"strings"
1214

1315
"github.com/pb33f/libopenapi/orderedmap"
@@ -195,7 +197,11 @@ func comparePaths(mapped, requested, basePaths []string) bool {
195197
var imploded []string
196198
for i, seg := range mapped {
197199
s := seg
198-
if strings.Contains(seg, "{") {
200+
r, err := getRegexForPath(seg)
201+
if err != nil {
202+
return false
203+
}
204+
if r.MatchString(requested[i]) {
199205
s = requested[i]
200206
}
201207
imploded = append(imploded, s)
@@ -204,3 +210,87 @@ func comparePaths(mapped, requested, basePaths []string) bool {
204210
r := filepath.Join(requested...)
205211
return checkPathAgainstBase(l, r, basePaths)
206212
}
213+
214+
func getRegexForPath(tpl string) (*regexp.Regexp, error) {
215+
216+
// Check if it is well-formed.
217+
idxs, errBraces := braceIndices(tpl)
218+
if errBraces != nil {
219+
return nil, errBraces
220+
}
221+
222+
// Backup the original.
223+
template := tpl
224+
225+
// Now let's parse it.
226+
defaultPattern := "[^/]+"
227+
228+
pattern := bytes.NewBufferString("^")
229+
var end int
230+
231+
for i := 0; i < len(idxs); i += 2 {
232+
233+
// Set all values we are interested in.
234+
raw := tpl[end:idxs[i]]
235+
end = idxs[i+1]
236+
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
237+
name := parts[0]
238+
patt := defaultPattern
239+
if len(parts) == 2 {
240+
patt = parts[1]
241+
}
242+
243+
// Name or pattern can't be empty.
244+
if name == "" || patt == "" {
245+
return nil, fmt.Errorf("mux: missing name or pattern in %q", tpl[idxs[i]:end])
246+
}
247+
248+
// Build the regexp pattern.
249+
_, err := fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt)
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
}
255+
256+
// Add the remaining.
257+
raw := tpl[end:]
258+
pattern.WriteString(regexp.QuoteMeta(raw))
259+
260+
// Compile full regexp.
261+
reg, errCompile := regexp.Compile(pattern.String())
262+
if errCompile != nil {
263+
return nil, errCompile
264+
}
265+
266+
// Check for capturing groups which used to work in older versions
267+
if reg.NumSubexp() != len(idxs)/2 {
268+
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)")
269+
}
270+
271+
// Done!
272+
return reg, nil
273+
}
274+
275+
func braceIndices(s string) ([]int, error) {
276+
var level, idx int
277+
var idxs []int
278+
for i := 0; i < len(s); i++ {
279+
switch s[i] {
280+
case '{':
281+
if level++; level == 1 {
282+
idx = i
283+
}
284+
case '}':
285+
if level--; level == 0 {
286+
idxs = append(idxs, idx, i+1)
287+
} else if level < 0 {
288+
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
289+
}
290+
}
291+
}
292+
if level != 0 {
293+
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
294+
}
295+
return idxs, nil
296+
}

paths/paths_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,22 @@ paths:
695695
assert.Equal(t, 0, len(errs), "Errors found: %v", errs)
696696
assert.NotNil(t, pathItem)
697697
}
698+
699+
func TestNewValidator_ODataFormattedOpenAPISpecs(t *testing.T) {
700+
701+
// load a doc
702+
b, _ := os.ReadFile("../test_specs/odata_spec.json")
703+
doc, _ := libopenapi.NewDocument(b)
704+
705+
m, _ := doc.BuildV3Model()
706+
707+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/entities('1')", nil)
708+
709+
pathItem, _, _ := FindPath(request, &m.Model)
710+
assert.NotNil(t, pathItem)
711+
712+
request, _ = http.NewRequest(http.MethodGet, "https://things.com/orders(RelationshipNumber='dummy',ValidityEndDate=datetime'1492041600000')", nil)
713+
714+
pathItem, _, _ = FindPath(request, &m.Model)
715+
assert.NotNil(t, pathItem)
716+
}

test_specs/odata_spec.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "Sample OData Service",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/entities('{Entity}')": {
9+
"parameters": [
10+
{
11+
"description": "key: Entity",
12+
"in": "path",
13+
"name": "Entity",
14+
"required": true,
15+
"schema": {
16+
"type": "integer",
17+
"format": "int32"
18+
}
19+
}
20+
],
21+
"get": {
22+
"responses": {
23+
"200": {
24+
"description": "Successful response"
25+
}
26+
}
27+
}
28+
},
29+
"/orders(RelationshipNumber='{RelationshipNumber}',ValidityEndDate=datetime'{ValidityEndDate}')": {
30+
"parameters": [
31+
{
32+
"name": "RelationshipNumber",
33+
"in": "path",
34+
"required": true,
35+
"description": "BP Relationship Number",
36+
"schema": {
37+
"type": "string",
38+
"maxLength": 12
39+
}
40+
},
41+
{
42+
"name": "ValidityEndDate",
43+
"in": "path",
44+
"required": true,
45+
"description": "Validity Date (Valid To)",
46+
"schema": {
47+
"type": "string",
48+
"example": "/Date(1492041600000)/"
49+
}
50+
}
51+
],
52+
"get": {
53+
"responses": {
54+
"200": {
55+
"description": "Retrieved entity"
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)