From 320a63f646f7fbbb2e2710c8f1dfcc0bb83afb2b Mon Sep 17 00:00:00 2001 From: Daniel Fiala Date: Tue, 23 Apr 2024 14:30:51 +0200 Subject: [PATCH] feat(transcoding): initial version of complex paths --- .../grpc/GrpcTranscodingDescriptor.java | 21 + .../grpc/MutinyTranscodingService.java | 5 +- .../main/resources/MutinyMarshalling.mustache | 39 +- .../grpc/runtime/GrpcTranscodingRecorder.java | 14 +- .../transcoding/GrpcTranscodingRequest.java | 6 +- .../transcoding/GrpcTranscodingResponse.java | 18 +- .../transcoding/GrpcTranscodingServer.java | 360 +++++++++++++++++- .../TranscodingMessageDecoder.java | 8 + .../examples/hello/HelloWorldNewService.java | 8 + .../src/main/proto/helloworld.proto | 6 + .../hello/HelloWorldNewEndpointTest.java | 9 + 11 files changed, 446 insertions(+), 48 deletions(-) create mode 100644 extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscodingDescriptor.java diff --git a/extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscodingDescriptor.java b/extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscodingDescriptor.java new file mode 100644 index 00000000000000..68b608a65e1203 --- /dev/null +++ b/extensions/grpc/api/src/main/java/io/quarkus/grpc/GrpcTranscodingDescriptor.java @@ -0,0 +1,21 @@ +package io.quarkus.grpc; + +public class GrpcTranscodingDescriptor { + + private final GrpcTranscodingMarshaller requestMarshaller; + private final GrpcTranscodingMarshaller responseMarshaller; + + public GrpcTranscodingDescriptor(GrpcTranscodingMarshaller requestMarshaller, + GrpcTranscodingMarshaller responseMarshaller) { + this.requestMarshaller = requestMarshaller; + this.responseMarshaller = responseMarshaller; + } + + public GrpcTranscodingMarshaller getRequestMarshaller() { + return requestMarshaller; + } + + public GrpcTranscodingMarshaller getResponseMarshaller() { + return responseMarshaller; + } +} diff --git a/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java b/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java index c1491703cc75ad..7d410ebe8e105a 100644 --- a/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java +++ b/extensions/grpc/api/src/main/java/io/quarkus/grpc/MutinyTranscodingService.java @@ -6,7 +6,6 @@ public interface MutinyTranscodingService { String getGrpcServiceName(); - GrpcTranscodingMarshaller findRequestMarshaller(String methodName); - - GrpcTranscodingMarshaller findResponseMarshaller(String methodName); + GrpcTranscodingDescriptor findTranscodingDescriptor( + String methodName); } diff --git a/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache b/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache index a2322a820ec548..a507ddd304a721 100644 --- a/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache +++ b/extensions/grpc/protoc/src/main/resources/MutinyMarshalling.mustache @@ -5,6 +5,8 @@ package {{packageName}}; import jakarta.ws.rs.core.Response; import io.quarkus.grpc.GrpcService; import io.quarkus.grpc.MutinyTranscodingService; +import io.quarkus.grpc.GrpcTranscodingDescriptor; +import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; import com.google.protobuf.InvalidProtocolBufferException; @@ -19,40 +21,23 @@ public class {{serviceName}}Marshalling implements MutinyTranscodingService { } @Override - public io.quarkus.grpc.GrpcTranscodingMarshaller findRequestMarshaller(String method) { + public GrpcTranscodingDescriptor findTranscodingDescriptor(String method) { switch (method) { - {{#unaryUnaryMethods}} + {{#unaryUnaryMethods}} case "{{methodName}}": - return (io.quarkus.grpc.GrpcTranscodingMarshaller) {{methodName}}RequestMarshaller(); - {{/unaryUnaryMethods}} - default: - throw new IllegalArgumentException("Unknown request method: " + method); + return (GrpcTranscodingDescriptor) {{methodName}}TranscodingDescriptor(); + {{/unaryUnaryMethods}} + default: + throw new IllegalArgumentException("Unknown request method: " + method); } } - @Override - public io.quarkus.grpc.GrpcTranscodingMarshaller findResponseMarshaller(String method) { - switch (method) { - {{#unaryUnaryMethods}} - case "{{methodName}}": - return (io.quarkus.grpc.GrpcTranscodingMarshaller) {{methodName}}ResponseMarshaller(); - {{/unaryUnaryMethods}} - default: - throw new IllegalArgumentException("Unknown response method: " + method); - } - } - - {{#unaryUnaryMethods}} - @io.quarkus.grpc.GrpcTranscodingMethod(grpcMethodName = "{{methodName}}", httpMethod = "{{httpMethod}}", httpPath = "{{httpPath}}") - public io.quarkus.grpc.GrpcTranscodingMarshaller<{{inputType}}> {{methodName}}RequestMarshaller() { - return new io.quarkus.grpc.GrpcTranscodingMarshaller<{{inputType}}>({{inputType}}.getDefaultInstance()); - } - {{/unaryUnaryMethods}} - {{#unaryUnaryMethods}} @io.quarkus.grpc.GrpcTranscodingMethod(grpcMethodName = "{{methodName}}", httpMethod = "{{httpMethod}}", httpPath = "{{httpPath}}") - public io.quarkus.grpc.GrpcTranscodingMarshaller<{{outputType}}> {{methodName}}ResponseMarshaller() { - return new io.quarkus.grpc.GrpcTranscodingMarshaller<{{outputType}}>({{outputType}}.getDefaultInstance()); + public io.quarkus.grpc.GrpcTranscodingDescriptor<{{inputType}}, {{outputType}}> {{methodName}}TranscodingDescriptor() { + return new io.quarkus.grpc.GrpcTranscodingDescriptor<{{inputType}}, {{outputType}}>( + new io.quarkus.grpc.GrpcTranscodingMarshaller<{{inputType}}>({{inputType}}.getDefaultInstance()), + new io.quarkus.grpc.GrpcTranscodingMarshaller<{{outputType}}>({{outputType}}.getDefaultInstance())); } {{/unaryUnaryMethods}} } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java index 049eb54942f694..b20ff878593ff5 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/GrpcTranscodingRecorder.java @@ -15,7 +15,7 @@ import io.grpc.ServerMethodDefinition; import io.grpc.ServerServiceDefinition; import io.quarkus.arc.Arc; -import io.quarkus.grpc.GrpcTranscodingMarshaller; +import io.quarkus.grpc.GrpcTranscodingDescriptor; import io.quarkus.grpc.MutinyTranscodingService; import io.quarkus.grpc.auth.GrpcSecurityInterceptor; import io.quarkus.grpc.runtime.transcoding.*; @@ -78,7 +78,7 @@ public RuntimeValue initializeMarshallingServer(RuntimeVa mappedMethods.add(serviceDefinition); - Route route = routerSupplier.getValue().route(httpMethod, path).handler(ctx -> { + Route route = routerSupplier.getValue().route().handler(ctx -> { if (securityPresent) { GrpcSecurityInterceptor.propagateSecurityIdentityWithDuplicatedCtx(ctx); } @@ -115,11 +115,13 @@ private GrpcTranscodingMetadata requestMarshaller = transcodingService.findRequestMarshaller(fullMethodName); - GrpcTranscodingMarshaller responseMarshaller = transcodingService.findResponseMarshaller(fullMethodName); + GrpcTranscodingDescriptor descriptor = transcodingService.findTranscodingDescriptor(fullMethodName); - return new GrpcTranscodingMetadata<>(transcodingMethod.getHttpMethodName(), fullMethodName, requestMarshaller, - responseMarshaller, methodDescriptor); + return new GrpcTranscodingMetadata<>(transcodingMethod.getHttpMethodName(), + fullMethodName, + descriptor.getRequestMarshaller(), + descriptor.getResponseMarshaller(), + methodDescriptor); } private List findTranscodingMethods(Map> transcodingMethods, diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java index cc634119f368e3..cf4aea824843bf 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingRequest.java @@ -84,7 +84,7 @@ public void init() { }); queue.drainHandler(v -> stream.resume()); queue.handler(msg -> { - if (msg == END_SENTINEL) { + if (msg == END_SENTINEL && httpRequest.bytesRead() != 0) { handleEnd(); } else { handleMessage(msg); @@ -267,4 +267,8 @@ public Future collecting(Collector collector) { public HttpConnection connection() { return httpRequest.connection(); } + + public HttpServerRequest httpRequest() { + return httpRequest; + } } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java index a9ef02b80d095e..efc11cbea5ba84 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingResponse.java @@ -152,11 +152,6 @@ private Future writeMessage(GrpcMessage message, boolean end) { throw new IllegalStateException(); } - Buffer encoded = encode(message); - if (encoded == null) { - throw new IllegalStateException("The message is null"); - } - boolean trailersOnly = status != GrpcStatus.OK && !headersSent && end; MultiMap responseHeaders = httpResponse.headers(); @@ -169,7 +164,6 @@ private Future writeMessage(GrpcMessage message, boolean end) { } responseHeaders.set("content-type", "application/json"); - responseHeaders.set("content-length", String.valueOf(encoded.length())); } if (end) { @@ -200,11 +194,23 @@ private Future writeMessage(GrpcMessage message, boolean end) { responseTrailers.remove("grpc-message"); } if (message != null) { + Buffer encoded = encode(message); + if (encoded == null) { + throw new IllegalStateException("The message is null"); + } + + responseHeaders.set("content-length", String.valueOf(encoded.length())); return httpResponse.end(encoded); } else { return httpResponse.end(); } } else { + Buffer encoded = encode(message); + if (encoded == null) { + throw new IllegalStateException("The message is null"); + } + + responseHeaders.set("content-length", String.valueOf(encoded.length())); return httpResponse.write(encoded); } } diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java index 83ce07a6f979b7..6a1b487fa8525a 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/GrpcTranscodingServer.java @@ -2,6 +2,8 @@ import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import io.grpc.MethodDescriptor; import io.vertx.core.Handler; @@ -14,6 +16,278 @@ import io.vertx.grpc.server.GrpcServer; import io.vertx.grpc.server.GrpcServerRequest; +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They +// are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL +// query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP +// request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. public class GrpcTranscodingServer implements GrpcServer { private final Vertx vertx; @@ -28,14 +302,27 @@ public GrpcTranscodingServer(Vertx vertx) { @Override public void handle(HttpServerRequest httpRequest) { - String mappedMethod = methodMapping.get(httpRequest.path()); - if (mappedMethod == null) { - return; + String requestPath = httpRequest.path(); + + // Find the best matching method based on path parsing + for (Map.Entry entry : methodMapping.entrySet()) { + String pathTemplate = entry.getKey(); + String mappedMethod = entry.getValue(); + if (isPathMatch(requestPath, pathTemplate)) { + handleWithMappedMethod(httpRequest, mappedMethod); + return; // Match found, stop searching + } } + // If no match found - handle as needed (e.g., 404) + httpRequest.response().setStatusCode(404).end(); + } + + private void handleWithMappedMethod(HttpServerRequest httpRequest, String mappedMethod) { GrpcMethodCall methodCall = new GrpcMethodCall("/" + mappedMethod); String fmn = methodCall.fullMethodName(); - GrpcTranscodingServer.MethodCallHandler method = methodCallHandlers.get(fmn); + MethodCallHandler method = methodCallHandlers.get(fmn); + if (method != null) { handle(method, httpRequest, methodCall); } else { @@ -43,7 +330,35 @@ public void handle(HttpServerRequest httpRequest) { } } - private void handle(GrpcTranscodingServer.MethodCallHandler method, HttpServerRequest httpRequest, + private boolean isPathMatch(String requestPath, String pathTemplate) { + String[] requestParts = requestPath.split("/"); + String[] templateParts = parsePath(pathTemplate).split("/"); // Use the enhanced parsePath + + if (requestParts.length != templateParts.length) { + return false; // Mismatch in number of segments + } + + for (int i = 0; i < requestParts.length; i++) { + String requestPart = requestParts[i]; + String templatePart = templateParts[i]; + + if (templatePart.startsWith("{") && templatePart.endsWith("}")) { + // It's a variable, any non-empty segment would match + if (requestPart.isEmpty()) { + return false; + } + } else { + // It's a literal segment + if (!requestPart.equals(templatePart)) { + return false; + } + } + } + + return true; // All segments matched + } + + private void handle(MethodCallHandler method, HttpServerRequest httpRequest, GrpcMethodCall methodCall) { GrpcTranscodingRequest grpcRequest = new GrpcTranscodingRequest<>(httpRequest, method.messageDecoder, method.messageEncoder, methodCall); @@ -94,6 +409,41 @@ public MethodDescriptor.Marshaller findResponseMarshaller(String fullMeth return (MethodDescriptor.Marshaller) metadata.getResponseMarshaller(); } + public String parsePath(String path) { + // Regular expressions for template segments + String singleSegmentVarPattern = "\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}"; + String multiSegmentVarPattern = "\\{\\+([a-zA-Z_][a-zA-Z0-9_]*)\\}"; + + Map extractedParams = new HashMap<>(); + + // Split the path and process segments + String[] parts = path.split("/"); + StringBuilder parsedPathBuilder = new StringBuilder(); + + for (String part : parts) { + if (part.isEmpty()) { + continue; // Skip empty segments + } + + Matcher singleMatcher = Pattern.compile(singleSegmentVarPattern).matcher(part); + Matcher multiMatcher = Pattern.compile(multiSegmentVarPattern).matcher(part); + + if (singleMatcher.matches()) { + String paramName = singleMatcher.group(1); + extractedParams.put(paramName, ""); // Initialize as empty; to be populated later + parsedPathBuilder.append("/{").append(paramName).append("}"); + } else if (multiMatcher.matches()) { + String paramName = multiMatcher.group(1); + extractedParams.put(paramName, ""); + parsedPathBuilder.append("/{+").append(paramName).append("}"); + } else { + parsedPathBuilder.append("/").append(part); + } + } + + return parsedPathBuilder.toString(); + } + private static class MethodCallHandler implements Handler> { final MethodDescriptor def; diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java index f9abad865ad652..278045a5b7e8e8 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/transcoding/TranscodingMessageDecoder.java @@ -1,8 +1,10 @@ package io.quarkus.grpc.runtime.transcoding; import java.io.ByteArrayInputStream; +import java.net.http.HttpRequest; import io.grpc.MethodDescriptor; +import io.vertx.core.http.HttpServerRequest; import io.vertx.grpc.common.GrpcMessage; import io.vertx.grpc.common.GrpcMessageDecoder; @@ -16,6 +18,12 @@ public TranscodingMessageDecoder(MethodDescriptor.Marshaller marshaller) { @Override public T decode(GrpcMessage msg) { + if(!(msg instanceof GrpcTranscodingRequest)) { + throw new IllegalArgumentException("Invalid message"); + } + + HttpServerRequest request = ((GrpcTranscodingRequest) msg).httpRequest(); + return marshaller.parse(new ByteArrayInputStream(msg.payload().getBytes())); } } diff --git a/integration-tests/grpc-transcoding/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java b/integration-tests/grpc-transcoding/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java index 59c38312672329..42ea08ca66b54e 100644 --- a/integration-tests/grpc-transcoding/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java +++ b/integration-tests/grpc-transcoding/src/main/java/io/quarkus/grpc/examples/hello/HelloWorldNewService.java @@ -19,7 +19,15 @@ public Uni sayHello(HelloRequest request) { @Override public Uni threadName(HelloRequest request) { + System.out.println("Received request: " + request.getName()); return Uni.createFrom().item(Thread.currentThread().getName()) .map(res -> HelloReply.newBuilder().setMessage(res).build()); } + + @Override + public Uni complexPath(HelloRequest request) { + System.out.println("Received request: " + request.getName()); + return Uni.createFrom().item("Hello " + request.getName()) + .map(res -> HelloReply.newBuilder().setMessage(res).build()); + } } diff --git a/integration-tests/grpc-transcoding/src/main/proto/helloworld.proto b/integration-tests/grpc-transcoding/src/main/proto/helloworld.proto index 657e0edd1dee07..a5720e94f977a6 100644 --- a/integration-tests/grpc-transcoding/src/main/proto/helloworld.proto +++ b/integration-tests/grpc-transcoding/src/main/proto/helloworld.proto @@ -53,6 +53,12 @@ service Greeter { body: "*" }; } + rpc ComplexPath (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/complex/{name}/path" + body: "*" + }; + } } // The request message containing the user's name. diff --git a/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java b/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java index c5f5714c3746d0..1a209b2df233bd 100644 --- a/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java +++ b/integration-tests/grpc-transcoding/src/test/java/io/quarkus/grpc/examples/hello/HelloWorldNewEndpointTest.java @@ -35,6 +35,15 @@ public void testHelloWorldGrpcServiceUsingBlockingStub() { .statusCode(200); } + @Test + public void testHelloWorldGrpcServiceComplexPathUsingBlockingStub() { + given().when() + .post("/v1/complex/hello/path") + .then() + .statusCode(200) + .body("message", is("Hello test")); + } + private String getJsonRequest() { try { return JsonFormat.printer().omittingInsignificantWhitespace()