Skip to content

Commit

Permalink
Support nested fields, path segments
Browse files Browse the repository at this point in the history
Closes #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)
  • Loading branch information
mbarrien committed Mar 30, 2022
1 parent f3ef8ab commit 07fb69d
Show file tree
Hide file tree
Showing 9 changed files with 1,536 additions and 630 deletions.
15 changes: 12 additions & 3 deletions generator/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,15 +468,24 @@ func renderURL(r *registry.Registry) func(method data.Method) string {
fieldNameFn := fieldName(r)
return func(method data.Method) string {
methodURL := method.URL
reg := regexp.MustCompile("{([^}]+)}")
// capture fields like {abc} or {abc=def/ghi/*}. Discard the pattern after the equal sign.
reg := regexp.MustCompile("{([^=}]+)=?([^}]+)?}")
matches := reg.FindAllStringSubmatch(methodURL, -1)
fieldsInPath := make([]string, 0, len(matches))
if len(matches) > 0 {
log.Debugf("url matches %v", matches)
for _, m := range matches {
expToReplace := m[0]
fieldName := fieldNameFn(m[1])
part := fmt.Sprintf(`${req["%s"]}`, fieldName)
// convert foo_bar.baz_qux to fieldName `fooBar.bazQux`, part `${req["fooBar"]["bazQux"]}`
subFields := strings.Split(m[1], ".")
var subFieldNames, partNames []string
for _, subField := range subFields {
subFieldName := fieldNameFn(subField)
subFieldNames = append(subFieldNames, subFieldName)
partNames = append(partNames, fmt.Sprintf(`["%s"]`, subFieldName))
}
fieldName := strings.Join(subFieldNames, ".")
part := fmt.Sprintf(`${req%s}`, strings.Join(partNames, ""))
methodURL = strings.ReplaceAll(methodURL, expToReplace, part)
fieldsInPath = append(fieldsInPath, fmt.Sprintf(`"%s"`, fieldName))
}
Expand Down
15 changes: 15 additions & 0 deletions integration_tests/integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,19 @@ describe("test grpc-gateway-ts communication", () => {
const result = await CounterService.HTTPGetWithZeroValueURLSearchParams({ a: "A", b: "", [getFieldName('zero_value_msg')]: { c: 1, d: [1, 0, 2], e: false } }, { pathPrefix: "http://localhost:8081" })
expect(result).to.deep.equal({ a: "A", b: "hello", [getFieldName('zero_value_msg')]: { c: 2, d: [2, 1, 3], e: true } })
})

it('http get request with path segments', async () => {
const result = await CounterService.HTTPGetWithPathSegments({ a: "segmented/foo" }, { pathPrefix: "http://localhost:8081" })
expect(result.a).to.equal("segmented/foo/hello")
})

it('http post with field paths', async () => {
const result = await CounterService.HTTPPostWithFieldPath({ y: { x: 5, [getFieldName("nested_value")]: "foo" } }, { pathPrefix: "http://localhost:8081" })
expect(result).to.deep.equal({ xout: 5, yout: "hello/foo" })
})

it('http post with field paths and path segments', async () => {
const result = await CounterService.HTTPPostWithFieldPathAndSegments({ y: { x: 10, [getFieldName("nested_value")]: "segmented/foo" } }, { pathPrefix: "http://localhost:8081" })
expect(result).to.deep.equal({ xout: 10, yout: "hello/segmented/foo" })
})
})
9 changes: 2 additions & 7 deletions integration_tests/msg.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions integration_tests/scripts/gen-server-proto.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/bin/bash

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

go install \
github.com/golang/protobuf/protoc-gen-go \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

protoc -I . -I ../.. --go_out ./ --go_opt plugins=grpc --go_opt paths=source_relative \
protoc -I . -I ../.. --go_out ./ --go-grpc_out ./ --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./ --grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt paths=source_relative \
--grpc-gateway_opt generate_unbound_methods=true \
Expand Down
20 changes: 20 additions & 0 deletions integration_tests/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ func (r *RealCounterService) HTTPGetWithZeroValueURLSearchParams(ctx context.Con
},
}, nil
}

func (r *RealCounterService) HTTPGetWithPathSegments(ctx context.Context, in *HTTPGetWithPathSegmentsRequest) (*HTTPGetWithPathSegmentsResponse, error) {
return &HTTPGetWithPathSegmentsResponse{
A: in.GetA() + "/hello",
}, nil
}

func (r *RealCounterService) HTTPPostWithFieldPath(ctx context.Context, in *HTTPPostWithFieldPathRequest) (*HTTPPostWithFieldPathResponse, error) {
return &HTTPPostWithFieldPathResponse{
Xout: in.GetY().GetX(),
Yout: "hello/" + in.GetY().GetNestedValue(),
}, nil
}

func (r *RealCounterService) HTTPPostWithFieldPathAndSegments(ctx context.Context, in *HTTPPostWithFieldPathRequest) (*HTTPPostWithFieldPathResponse, error) {
return &HTTPPostWithFieldPathResponse{
Xout: in.GetY().GetX(),
Yout: "hello/" + in.GetY().GetNestedValue(),
}, nil
}

0 comments on commit 07fb69d

Please sign in to comment.