Skip to content

Commit 54ed213

Browse files
authored
Merge branch 'master' into test-PR-enforcement
2 parents 9df52fe + 1168863 commit 54ed213

16 files changed

Lines changed: 3183 additions & 1758 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ jobs:
2424
git diff
2525
exit 1
2626
fi
27+
- name: Check for OpenAPI path conflicts
28+
run: go run ./cmd/check-path-conflicts/main.go openapi/openapiv2.json

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ grpc-install:
8181
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
8282
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
8383
@go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@latest
84-
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
84+
# v2.27.8 errors with --openapiv2_out: can't resolve OpenAPI name from ".temporal.api.protocol.v1.Message"
85+
@go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.27.7
8586
@go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
8687
@go install github.com/mikefarah/yq/v4@latest
8788

buf.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ breaking:
1313
- WIRE_JSON
1414
ignore:
1515
- google
16-
- temporal/api/enums/v1/failed_cause.proto
17-
# TODO: remove this once the changes with WorkflowExecutionExtendedInfo.pause_info is merged
18-
- temporal/api/workflow/v1/message.proto
1916
lint:
2017
use:
2118
- DEFAULT

cmd/check-path-conflicts/main.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// check-path-conflicts reads an OpenAPI v2 JSON file and detects HTTP path
2+
// conflicts where a literal path segment and a parameterized segment overlap at
3+
// the same position (e.g. /items/pause vs /items/{id}).
4+
package main
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
"sort"
11+
"strings"
12+
)
13+
14+
type openAPISpec struct {
15+
Paths map[string]map[string]json.RawMessage `json:"paths"`
16+
}
17+
18+
// httpMethods are the valid HTTP method keys in an OpenAPI path item.
19+
var httpMethods = map[string]bool{
20+
"get": true, "put": true, "post": true, "delete": true,
21+
"options": true, "head": true, "patch": true,
22+
}
23+
24+
// segment represents one piece of a URL path.
25+
type segment struct {
26+
value string
27+
param bool // true when the segment is a path parameter like {id}
28+
}
29+
30+
func parseSegments(path string) []segment {
31+
parts := strings.Split(strings.Trim(path, "/"), "/")
32+
segs := make([]segment, len(parts))
33+
for i, p := range parts {
34+
segs[i] = segment{
35+
value: p,
36+
param: strings.HasPrefix(p, "{") && strings.HasSuffix(p, "}"),
37+
}
38+
}
39+
return segs
40+
}
41+
42+
// Two paths conflict when they have the same number of segments and at every
43+
// position, they either match literally or at least one of them is a parameter,
44+
// AND there is at least one position where one path has a literal and the other
45+
// has a parameter (otherwise they are the same path or differ only in parameter
46+
// names, which is a different issue).
47+
func conflicts(a, b []segment) bool {
48+
if len(a) != len(b) {
49+
return false
50+
}
51+
hasParamLiteralMismatch := false
52+
for i := range a {
53+
aParam := a[i].param
54+
bParam := b[i].param
55+
if !aParam && !bParam {
56+
// Both literals — must match exactly.
57+
if a[i].value != b[i].value {
58+
return false
59+
}
60+
} else if aParam != bParam {
61+
// One is a param, the other is a literal — potential conflict.
62+
hasParamLiteralMismatch = true
63+
}
64+
// Both params — always compatible at this position, continue.
65+
}
66+
return hasParamLiteralMismatch
67+
}
68+
69+
func main() {
70+
if len(os.Args) < 2 {
71+
fmt.Fprintf(os.Stderr, "usage: %s <openapi-v2.json>\n", os.Args[0])
72+
os.Exit(2)
73+
}
74+
75+
data, err := os.ReadFile(os.Args[1])
76+
if err != nil {
77+
fmt.Fprintf(os.Stderr, "error reading file: %v\n", err)
78+
os.Exit(2)
79+
}
80+
81+
var spec openAPISpec
82+
if err := json.Unmarshal(data, &spec); err != nil {
83+
fmt.Fprintf(os.Stderr, "error parsing JSON: %v\n", err)
84+
os.Exit(2)
85+
}
86+
87+
type parsedPath struct {
88+
raw string
89+
methods map[string]bool
90+
segments []segment
91+
}
92+
93+
var parsed []parsedPath
94+
for p, item := range spec.Paths {
95+
methods := make(map[string]bool)
96+
for key := range item {
97+
if httpMethods[strings.ToLower(key)] {
98+
methods[strings.ToLower(key)] = true
99+
}
100+
}
101+
parsed = append(parsed, parsedPath{raw: p, methods: methods, segments: parseSegments(p)})
102+
}
103+
sort.Slice(parsed, func(i, j int) bool { return parsed[i].raw < parsed[j].raw })
104+
105+
var found []string
106+
for i := 0; i < len(parsed); i++ {
107+
for j := i + 1; j < len(parsed); j++ {
108+
if !conflicts(parsed[i].segments, parsed[j].segments) {
109+
continue
110+
}
111+
// Find overlapping HTTP methods.
112+
var shared []string
113+
for m := range parsed[i].methods {
114+
if parsed[j].methods[m] {
115+
shared = append(shared, strings.ToUpper(m))
116+
}
117+
}
118+
if len(shared) == 0 {
119+
continue
120+
}
121+
sort.Strings(shared)
122+
found = append(found, fmt.Sprintf(" %s\n %s\n methods: %s",
123+
parsed[i].raw, parsed[j].raw, strings.Join(shared, ", ")))
124+
}
125+
}
126+
127+
if len(found) > 0 {
128+
fmt.Fprintf(os.Stderr, "found %d path conflict(s):\n\n", len(found))
129+
for _, f := range found {
130+
fmt.Fprintln(os.Stderr, f)
131+
fmt.Fprintln(os.Stderr)
132+
}
133+
os.Exit(1)
134+
}
135+
136+
fmt.Println("no path conflicts found")
137+
}

0 commit comments

Comments
 (0)