diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs index a65ed0fcade6..2a61ad1e697e 100644 --- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs +++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using Grpc.Shared; using Microsoft.AspNetCore.Http; @@ -55,6 +56,8 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) var rewriteActions = new List>(); var tempSegments = pattern.Segments.ToList(); + var haveCatchAll = false; + var i = 0; while (i < tempSegments.Count) { @@ -63,8 +66,16 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) { var fullPath = string.Join(".", segmentVariable.FieldPath); - var segmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment; - if (segmentCount == 1) + var remainingSegmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment; + + // Handle situation where the last segment is catch all but there is a verb. + if (remainingSegmentCount == 1 && segmentVariable.HasCatchAllPath && pattern.Verb != null) + { + // Move past the catch all so the regex added below just includes the verb. + remainingSegmentCount++; + } + + if (remainingSegmentCount == 1) { // Single segment parameter. Include in route with its default name. tempSegments[i] = segmentVariable.HasCatchAllPath @@ -77,7 +88,6 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) var routeParameterParts = new List(); var routeValueFormatTemplateParts = new List(); var variableParts = new List(); - var haveCatchAll = false; var catchAllSuffix = string.Empty; while (i < segmentVariable.EndSegment && !haveCatchAll) @@ -101,15 +111,15 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) case SegmentType.CatchAll: { var parameterName = $"__Complex_{fullPath}_{i}"; - var suffix = string.Join("/", tempSegments.Skip(i + 1)); - catchAllSuffix = string.Join("/", tempSegments.Skip(i + segmentCount - 1)); + var suffix = BuildSuffix(tempSegments.Skip(i + 1), pattern.Verb); + catchAllSuffix = BuildSuffix(tempSegments.Skip(i + remainingSegmentCount - 1), pattern.Verb); // It's possible to have multiple routes with catch-all parameters that have different suffixes. // For example: // - /{name=v1/**/b}/one // - /{name=v1/**/b}/two // The suffix is added as a route constraint to avoid matching multiple routes to a request. - var constraint = suffix.Length > 0 ? $":regex({suffix}$)" : string.Empty; + var constraint = suffix.Length > 0 ? $":regex({Regex.Escape(suffix)}$)" : string.Empty; tempSegments[i] = $"{{**{parameterName}{constraint}}}"; routeValueFormatTemplateParts.Add($"{{{variableParts.Count}}}"); @@ -145,7 +155,7 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) // the entire remainder of the URL in the route value, we must trim the suffix from that route value. if (!string.IsNullOrEmpty(catchAllSuffix)) { - finalValue = finalValue.Substring(0, finalValue.Length - catchAllSuffix.Length - 1); + finalValue = finalValue[..^catchAllSuffix.Length]; } context.Request.RouteValues[fullPath] = finalValue; }); @@ -169,7 +179,15 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) break; case SegmentType.CatchAll: // Ignore remaining segment values. - tempSegments[i] = $"{{**__Discard_{i}}}"; + if (pattern.Verb != null) + { + tempSegments[i] = $"{{**__Discard_{i}:regex({Regex.Escape($":{pattern.Verb}")}$)}}"; + } + else + { + tempSegments[i] = $"{{**__Discard_{i}}}"; + } + haveCatchAll = true; break; } @@ -177,7 +195,27 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern) } } - return new JsonTranscodingRouteAdapter(pattern, "/" + string.Join("/", tempSegments), rewriteActions); + string resolvedRoutePattern = "/" + string.Join("/", tempSegments); + // If the route has a catch all then the verb is included in the catch all regex constraint. + if (pattern.Verb != null && !haveCatchAll) + { + resolvedRoutePattern += ":" + pattern.Verb; + } + return new JsonTranscodingRouteAdapter(pattern, resolvedRoutePattern, rewriteActions); + + static string BuildSuffix(IEnumerable segments, string? verb) + { + var pattern = string.Join("/", segments); + if (!string.IsNullOrEmpty(pattern)) + { + pattern = "/" + pattern; + } + if (verb != null) + { + pattern += ":" + verb; + } + return pattern; + } } private static SegmentType GetSegmentType(string segment) diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs index 6ccaeffd65aa..c3482d12264d 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -8,8 +9,6 @@ using Grpc.Core; using IntegrationTestsWebsite; using Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.Infrastructure; -using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal; -using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.Infrastructure; using Microsoft.AspNetCore.Testing; using Xunit.Abstractions; @@ -105,4 +104,163 @@ Task UnaryMethod(ComplextHelloRequest request, ServerCallContext con // Assert Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString()); } + + [Fact] + public async Task SimpleCatchAllParameter_PrefixSuffixSlashes_MatchUrl_SuccessResult() + { + // Arrange + Task UnaryMethod(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}!" }); + } + var method = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod, + Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll4")); + + var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") }; + + // Act + var response = await client.GetAsync("/v1/greeter//name/one/two//").DefaultTimeout(); + var responseStream = await response.Content.ReadAsStreamAsync(); + using var result = await JsonDocument.ParseAsync(responseStream); + + // Assert + Assert.Equal("Hello /name/one/two//!", result.RootElement.GetProperty("message").GetString()); + } + + [Fact] + public async Task ParameterVerb_MatchUrl_SuccessResult() + { + // Arrange + Task UnaryMethod1(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" }); + } + Task UnaryMethod2(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" }); + } + var method1 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod1, + Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbOne")); + var method2 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod2, + Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbTwo")); + + var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") }; + + // Act 1 + var response1 = await client.GetAsync("/v1/greeter_custom/test:one").DefaultTimeout(); + var responseStream1 = await response1.Content.ReadAsStreamAsync(); + using var result1 = await JsonDocument.ParseAsync(responseStream1); + + // Assert 2 + Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString()); + + // Act 2 + var response2 = await client.GetAsync("/v1/greeter_custom/test:two").DefaultTimeout(); + var responseStream2 = await response2.Content.ReadAsStreamAsync(); + using var result2 = await JsonDocument.ParseAsync(responseStream2); + + // Assert 2 + Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString()); + + // Act 3 + var response3 = await client.GetAsync("/v1/greeter_custom/test").DefaultTimeout(); + + // Assert 3 + Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode); + } + + [Fact] + public async Task CatchAllVerb_MatchUrl_SuccessResult() + { + // Arrange + Task UnaryMethod1(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" }); + } + Task UnaryMethod2(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" }); + } + var method1 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod1, + Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbOne")); + var method2 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod2, + Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbTwo")); + + var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") }; + + // Act 1 + var response1 = await client.GetAsync("/v1/greeter_customcatchall/test/name:one").DefaultTimeout(); + var responseStream1 = await response1.Content.ReadAsStreamAsync(); + using var result1 = await JsonDocument.ParseAsync(responseStream1); + + // Assert 2 + Assert.Equal("Hello test/name one!", result1.RootElement.GetProperty("message").GetString()); + + // Act 2 + var response2 = await client.GetAsync("/v1/greeter_customcatchall/test/name:two").DefaultTimeout(); + var responseStream2 = await response2.Content.ReadAsStreamAsync(); + using var result2 = await JsonDocument.ParseAsync(responseStream2); + + // Assert 2 + Assert.Equal("Hello test/name two!", result2.RootElement.GetProperty("message").GetString()); + + // Act 3 + var response3 = await client.GetAsync("/v1/greeter_customcatchall/test/name").DefaultTimeout(); + + // Assert 3 + Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode); + } + + [Fact] + public async Task PostVerb_MatchUrl_SuccessResult() + { + // Arrange + Task UnaryMethod1(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" }); + } + Task UnaryMethod2(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" }); + } + var method1 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod1, + Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbOne")); + var method2 = Fixture.DynamicGrpc.AddUnaryMethod( + UnaryMethod2, + Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbTwo")); + + var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") }; + + var requestMessage = new HelloRequest { Name = "test" }; + var content = new ByteArrayContent(Encoding.UTF8.GetBytes(requestMessage.ToString())); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act 1 + var response1 = await client.PostAsync("/v1/greeter_custompost:one", content).DefaultTimeout(); + var responseStream1 = await response1.Content.ReadAsStreamAsync(); + using var result1 = await JsonDocument.ParseAsync(responseStream1); + + // Assert 2 + Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString()); + + // Act 2 + var response2 = await client.PostAsync("/v1/greeter_custompost:two", content).DefaultTimeout(); + var responseStream2 = await response2.Content.ReadAsStreamAsync(); + using var result2 = await JsonDocument.ParseAsync(responseStream2); + + // Assert 2 + Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString()); + + // Act 3 + var response3 = await client.PostAsync("/v1/greeter_custompost", content).DefaultTimeout(); + + // Assert 3 + Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode); + } } diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs index c27ffe47e6e5..2fddd21e9aff 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs @@ -73,7 +73,7 @@ public void ParseVerb() var pattern = HttpRoutePattern.Parse("/a:foo"); var adapter = JsonTranscodingRouteAdapter.Parse(pattern); - Assert.Equal("/a", adapter.ResolvedRouteTemplate); + Assert.Equal("/a:foo", adapter.ResolvedRouteTemplate); Assert.Empty(adapter.RewriteVariableActions); } @@ -133,7 +133,7 @@ public void ParseComplexPrefixSuffixCatchAll() var pattern = HttpRoutePattern.Parse("/{x.y.z=a/**/b}/c/d"); var adapter = JsonTranscodingRouteAdapter.Parse(pattern); - Assert.Equal("/a/{**__Complex_x.y.z_1:regex(b/c/d$)}", adapter.ResolvedRouteTemplate); + Assert.Equal("/a/{**__Complex_x.y.z_1:regex(/b/c/d$)}", adapter.ResolvedRouteTemplate); var httpContext = new DefaultHttpContext(); httpContext.Request.RouteValues = new RouteValueDictionary @@ -146,6 +146,25 @@ public void ParseComplexPrefixSuffixCatchAll() Assert.Equal("a/my/value/b", httpContext.Request.RouteValues["x.y.z"]); } + [Fact] + public void ParseComplexPrefixSuffixCatchAllVerb() + { + var pattern = HttpRoutePattern.Parse("/{x.y.z=a/**/b}/c/d:verb"); + var adapter = JsonTranscodingRouteAdapter.Parse(pattern); + + Assert.Equal("/a/{**__Complex_x.y.z_1:regex(/b/c/d:verb$)}", adapter.ResolvedRouteTemplate); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary + { + { "__Complex_x.y.z_1", "my/value/b/c/d:verb" } + }; + + adapter.RewriteVariableActions[0](httpContext); + + Assert.Equal("a/my/value/b", httpContext.Request.RouteValues["x.y.z"]); + } + [Fact] public void ParseComplexPrefixSegment() { @@ -213,11 +232,58 @@ public void ParseManyVariables() } [Fact] - public void ParseCatchAllVerb() + public void ParseCatchAllDiscardVerb() { var pattern = HttpRoutePattern.Parse("/a/{b=*}/**:verb"); var adapter = JsonTranscodingRouteAdapter.Parse(pattern); - Assert.Equal("/a/{b}/{**__Discard_2}", adapter.ResolvedRouteTemplate); + Assert.Equal("/a/{b}/{**__Discard_2:regex(:verb$)}", adapter.ResolvedRouteTemplate); + } + + [Fact] + public void ParseCatchAllParameterVerb() + { + var pattern = HttpRoutePattern.Parse("/v1/greeter/{name=**}:verb"); + var adapter = JsonTranscodingRouteAdapter.Parse(pattern); + + Assert.Equal("/v1/greeter/{**__Complex_name_2:regex(:verb$)}", adapter.ResolvedRouteTemplate); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary + { + { "__Complex_name_2", "test/name:verb" } + }; + + adapter.RewriteVariableActions[0](httpContext); + + Assert.Equal("test/name", httpContext.Request.RouteValues["name"]); + } + + [Fact] + public void ParseCatchAllParameterVerb_TrailingSlash() + { + var pattern = HttpRoutePattern.Parse("/v1/greeter/{name=**}:verb"); + var adapter = JsonTranscodingRouteAdapter.Parse(pattern); + + Assert.Equal("/v1/greeter/{**__Complex_name_2:regex(:verb$)}", adapter.ResolvedRouteTemplate); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary + { + { "__Complex_name_2", "test/name/:verb" } + }; + + adapter.RewriteVariableActions[0](httpContext); + + Assert.Equal("test/name/", httpContext.Request.RouteValues["name"]); + } + + [Fact] + public void ParseParameterVerb() + { + var pattern = HttpRoutePattern.Parse("/v1/greeter/{name}:verb"); + var adapter = JsonTranscodingRouteAdapter.Parse(pattern); + + Assert.Equal("/v1/greeter/{name}:verb", adapter.ResolvedRouteTemplate); } } diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs index 277015707da8..a59540d67c9a 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs @@ -68,6 +68,25 @@ public void AddMethod_OptionAdditionalBindings_ResolveMethods() Assert.Equal("/v1/additional_bindings/{name}", additionalMethodModel.RoutePattern.RawText); } + [Fact] + public void AddMethod_PatternVerb_RouteEndsWithVerb() + { + // Arrange & Act + var endpoints = MapEndpoints(); + + var startFrameImport = Assert.Single(FindGrpcEndpoints(endpoints, nameof(JsonTranscodingColonRouteService.StartFrameImport))); + var getFrameImport = Assert.Single(FindGrpcEndpoints(endpoints, nameof(JsonTranscodingColonRouteService.GetFrameImport))); + + // Assert + Assert.Equal("POST", startFrameImport.Metadata.GetMetadata()?.HttpMethods.Single()); + Assert.Equal("/v1/frames:startFrameImport", startFrameImport.Metadata.GetMetadata()?.HttpRule.Post); + Assert.Equal("/v1/frames:startFrameImport", startFrameImport.RoutePattern.RawText); + + Assert.Equal("POST", getFrameImport.Metadata.GetMetadata()?.HttpMethods.Single()); + Assert.Equal("/v1/frames:getFrameImport", getFrameImport.Metadata.GetMetadata()?.HttpRule.Post); + Assert.Equal("/v1/frames:getFrameImport", getFrameImport.RoutePattern.RawText); + } + [Fact] public void AddMethod_NoHttpRuleInProto_ThrowNotFoundError() { diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto index 4d4423d7e450..f78c171f1802 100644 --- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto @@ -130,6 +130,21 @@ service JsonTranscodingStreaming { } } +service JsonTranscodingColonRoute { + rpc StartFrameImport(HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/frames:startFrameImport", + body: "*", + }; + } + rpc GetFrameImport(HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/frames:getFrameImport", + body: "*", + }; + } +} + message HelloRequest { message SubMessage { string subfield = 1; diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/TestObjects/Services/JsonTranscodingColonRouteService.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/TestObjects/Services/JsonTranscodingColonRouteService.cs new file mode 100644 index 000000000000..9ee209f172c6 --- /dev/null +++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/TestObjects/Services/JsonTranscodingColonRouteService.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Transcoding; + +namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.TestObjects; + +public class JsonTranscodingColonRouteService : JsonTranscodingColonRoute.JsonTranscodingColonRouteBase +{ +} diff --git a/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto b/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto index 50f158800cf3..890132f0e54c 100644 --- a/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto +++ b/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto @@ -46,6 +46,43 @@ service Greeter { get: "/v1/{name.last_name}/{name.first_name=complex_greeter/**/b}/c/d/two" }; } + rpc SayHelloComplexCatchAll4 (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter/{name=**}" + }; + } + rpc SayHelloCustomVerbOne (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter_custom/{name}:one" + }; + } + rpc SayHelloCustomVerbTwo (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter_custom/{name}:two" + }; + } + rpc SayHelloCatchAllCustomVerbOne (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter_customcatchall/{name=**}:one" + }; + } + rpc SayHelloCatchAllCustomVerbTwo (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter_customcatchall/{name=**}:two" + }; + } + rpc SayHelloPostCustomVerbOne (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/greeter_custompost:one", + body: "*" + }; + } + rpc SayHelloPostCustomVerbTwo (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/greeter_custompost:two", + body: "*" + }; + } } // The request message containing the user's name.