Skip to content

Commit 7cd8c2c

Browse files
committed
Support nested fields, path segments
Closes grpc-ecosystem#28 Adds support for 2 related cases in URLs: * Support path segments like in https://google.aip.dev/127, where the URL contains a pattern like `post: "/v1/{parent=publishers/*}/books"` * Support nested field names in the URL, where the URL is structured like: ``` option (google.api.http) = { patch: "/v3/{intent.name=projects/*/locations/*/agents/*/intents/*}" body: "intent" }; ``` This gets translated to `/v3/${req["intent"]["name"]}` While here, use the newer protoc-gen-go-grpc plugin for generating server test code, due to deprecated usage of plugin=grpc (see https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.20.0#v1.20-grpc-support)
1 parent f3ef8ab commit 7cd8c2c

File tree

9 files changed

+1572
-632
lines changed

9 files changed

+1572
-632
lines changed

generator/template.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,24 @@ func renderURL(r *registry.Registry) func(method data.Method) string {
468468
fieldNameFn := fieldName(r)
469469
return func(method data.Method) string {
470470
methodURL := method.URL
471-
reg := regexp.MustCompile("{([^}]+)}")
471+
// capture fields like {abc} or {abc=def/ghi/*}. Discard the pattern after the equal sign.
472+
reg := regexp.MustCompile("{([^=}]+)=?([^}]+)?}")
472473
matches := reg.FindAllStringSubmatch(methodURL, -1)
473474
fieldsInPath := make([]string, 0, len(matches))
474475
if len(matches) > 0 {
475476
log.Debugf("url matches %v", matches)
476477
for _, m := range matches {
477478
expToReplace := m[0]
478-
fieldName := fieldNameFn(m[1])
479-
part := fmt.Sprintf(`${req["%s"]}`, fieldName)
479+
// convert foo_bar.baz_qux to fieldName `fooBar.bazQux`, part `${req["fooBar"]["bazQux"]}`
480+
subFields := strings.Split(m[1], ".")
481+
var subFieldNames, partNames []string
482+
for _, subField := range subFields {
483+
subFieldName := fieldNameFn(subField)
484+
subFieldNames = append(subFieldNames, subFieldName)
485+
partNames = append(partNames, fmt.Sprintf(`["%s"]`, subFieldName))
486+
}
487+
fieldName := strings.Join(subFieldNames, ".")
488+
part := fmt.Sprintf(`${req%s}`, strings.Join(partNames, ""))
480489
methodURL = strings.ReplaceAll(methodURL, expToReplace, part)
481490
fieldsInPath = append(fieldsInPath, fmt.Sprintf(`"%s"`, fieldName))
482491
}

integration_tests/integration_test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,19 @@ describe("test grpc-gateway-ts communication", () => {
8787
const result = await CounterService.HTTPGetWithZeroValueURLSearchParams({ a: "A", b: "", [getFieldName('zero_value_msg')]: { c: 1, d: [1, 0, 2], e: false } }, { pathPrefix: "http://localhost:8081" })
8888
expect(result).to.deep.equal({ a: "A", b: "hello", [getFieldName('zero_value_msg')]: { c: 2, d: [2, 1, 3], e: true } })
8989
})
90+
91+
it('http get request with path segments', async () => {
92+
const result = await CounterService.HTTPGetWithPathSegments({ a: "segmented/foo" }, { pathPrefix: "http://localhost:8081" })
93+
expect(result.a).to.equal("segmented/foo/hello")
94+
})
95+
96+
it('http post with field paths', async () => {
97+
const result = await CounterService.HTTPPostWithFieldPath({ a: 5, b: { [getFieldName("nested_value")]: "foo" } }, { pathPrefix: "http://localhost:8081" })
98+
expect(result).to.deep.equal({ a: 5, b: "foo/hello" })
99+
})
100+
101+
it('http post with field paths and path segments', async () => {
102+
const result = await CounterService.HTTPPostWithFieldPathAndSegments({ a: 10, b: { [getFieldName("nested_value")]: "segmented/foo" } }, { pathPrefix: "http://localhost:8081" })
103+
expect(result).to.deep.equal({ a: 10, b: "segmented/foo/hello" })
104+
})
90105
})

integration_tests/msg.pb.go

+2-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration_tests/scripts/gen-server-proto.sh

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#!/bin/bash
22

33
# remove binaries to ensure that binaries present in tools.go are installed
4-
rm -f $GOBIN/protoc-gen-go $GOBIN/protoc-gen-grpc-gateway $GOBIN/protoc-gen-swagger
4+
rm -f $GOBIN/protoc-gen-go $GOBIN/protoc-gen-go-grpc $GOBIN/protoc-gen-grpc-gateway $GOBIN/protoc-gen-swagger
55

66
go install \
7-
github.com/golang/protobuf/protoc-gen-go \
7+
google.golang.org/protobuf/cmd/protoc-gen-go \
8+
google.golang.org/grpc/cmd/protoc-gen-go-grpc \
89
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \
910
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
1011

11-
protoc -I . -I ../.. --go_out ./ --go_opt plugins=grpc --go_opt paths=source_relative \
12+
protoc -I . -I ../.. --go_out ./ --go-grpc_out ./ --go-grpc_opt paths=source_relative \
1213
--grpc-gateway_out ./ --grpc-gateway_opt logtostderr=true \
1314
--grpc-gateway_opt paths=source_relative \
1415
--grpc-gateway_opt generate_unbound_methods=true \

integration_tests/service.go

+20
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,23 @@ func (r *RealCounterService) HTTPGetWithZeroValueURLSearchParams(ctx context.Con
107107
},
108108
}, nil
109109
}
110+
111+
func (r *RealCounterService) HTTPGetWithPathSegments(ctx context.Context, in *HTTPGetWithPathSegmentsRequest) (*HTTPGetWithPathSegmentsResponse, error) {
112+
return &HTTPGetWithPathSegmentsResponse{
113+
A: in.GetA() + "/hello",
114+
}, nil
115+
}
116+
117+
func (r *RealCounterService) HTTPPostWithFieldPath(ctx context.Context, in *HTTPPostWithFieldPathRequest) (*HTTPPostWithFieldPathResponse, error) {
118+
return &HTTPPostWithFieldPathResponse{
119+
A: in.GetA(),
120+
B: in.GetB().GetNestedValue() + "/hello",
121+
}, nil
122+
}
123+
124+
func (r *RealCounterService) HTTPPostWithFieldPathAndSegments(ctx context.Context, in *HTTPPostWithFieldPathRequest) (*HTTPPostWithFieldPathResponse, error) {
125+
return &HTTPPostWithFieldPathResponse{
126+
A: in.GetA(),
127+
B: in.GetB().GetNestedValue() + "/hello",
128+
}, nil
129+
}

0 commit comments

Comments
 (0)