diff --git a/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java b/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java index d8f06d2..ebaa24c 100644 --- a/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java +++ b/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package org.wiremock.grpc.dsl; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.matching.StringValuePattern; import com.google.protobuf.MessageOrBuilder; import org.wiremock.annotations.Beta; @@ -48,6 +50,15 @@ public static GrpcResponseDefinitionBuilder message(MessageOrBuilder messageOrBu return new GrpcResponseDefinitionBuilder(Status.OK).fromJson(json); } + public static GrpcResponseDefinitionBuilder messageAsAny(MessageOrBuilder messageOrBuilder) { + final String initialJson = JsonMessageUtils.toJson(messageOrBuilder); + final ObjectNode jsonObject = Json.read(initialJson, ObjectNode.class); + jsonObject.put( + "@type", "type.googleapis.com/" + messageOrBuilder.getDescriptorForType().getFullName()); + String finalJson = Json.write(jsonObject); + return new GrpcResponseDefinitionBuilder(Status.OK).fromJson(finalJson); + } + public enum Status { OK(0), CANCELLED(1), diff --git a/src/main/java/org/wiremock/grpc/internal/BaseCallHandler.java b/src/main/java/org/wiremock/grpc/internal/BaseCallHandler.java new file mode 100644 index 0000000..a8ef674 --- /dev/null +++ b/src/main/java/org/wiremock/grpc/internal/BaseCallHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wiremock.grpc.internal; + +import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.google.protobuf.Descriptors; + +public abstract class BaseCallHandler { + + protected final StubRequestHandler stubRequestHandler; + protected final Descriptors.ServiceDescriptor serviceDescriptor; + protected final Descriptors.MethodDescriptor methodDescriptor; + + protected final JsonMessageConverter jsonMessageConverter; + + protected BaseCallHandler( + StubRequestHandler stubRequestHandler, + Descriptors.ServiceDescriptor serviceDescriptor, + Descriptors.MethodDescriptor methodDescriptor, + JsonMessageConverter jsonMessageConverter) { + this.stubRequestHandler = stubRequestHandler; + this.serviceDescriptor = serviceDescriptor; + this.methodDescriptor = methodDescriptor; + this.jsonMessageConverter = jsonMessageConverter; + } +} diff --git a/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java b/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java index 615e068..ea493b5 100644 --- a/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java +++ b/src/main/java/org/wiremock/grpc/internal/ClientStreamingServerCallHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,20 +29,15 @@ import java.util.concurrent.atomic.AtomicReference; import org.wiremock.grpc.dsl.WireMockGrpc; -public class ClientStreamingServerCallHandler +public class ClientStreamingServerCallHandler extends BaseCallHandler implements ServerCalls.ClientStreamingMethod { - private final StubRequestHandler stubRequestHandler; - private final Descriptors.ServiceDescriptor serviceDescriptor; - private final Descriptors.MethodDescriptor methodDescriptor; - public ClientStreamingServerCallHandler( StubRequestHandler stubRequestHandler, Descriptors.ServiceDescriptor serviceDescriptor, - Descriptors.MethodDescriptor methodDescriptor) { - this.stubRequestHandler = stubRequestHandler; - this.serviceDescriptor = serviceDescriptor; - this.methodDescriptor = methodDescriptor; + Descriptors.MethodDescriptor methodDescriptor, + JsonMessageConverter jsonMessageConverter) { + super(stubRequestHandler, serviceDescriptor, methodDescriptor, jsonMessageConverter); } @Override @@ -67,7 +62,7 @@ public void onNext(DynamicMessage request) { serverAddress.port, serviceDescriptor.getFullName(), methodDescriptor.getName(), - request); + jsonMessageConverter.toJson(request)); stubRequestHandler.handle( wireMockRequest, @@ -97,7 +92,7 @@ public void onNext(DynamicMessage request) { DynamicMessage.newBuilder(methodDescriptor.getOutputType()); final DynamicMessage response = - JsonMessageUtils.toMessage(resp.getBodyAsString(), messageBuilder); + jsonMessageConverter.toMessage(resp.getBodyAsString(), messageBuilder); responseStatus.set(WireMockGrpc.Status.OK); firstResponse.set(response); diff --git a/src/main/java/org/wiremock/grpc/internal/GrpcFilter.java b/src/main/java/org/wiremock/grpc/internal/GrpcFilter.java index 9076266..da992f3 100644 --- a/src/main/java/org/wiremock/grpc/internal/GrpcFilter.java +++ b/src/main/java/org/wiremock/grpc/internal/GrpcFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.github.tomakehurst.wiremock.http.StubRequestHandler; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.TypeRegistry; import io.grpc.*; import io.grpc.protobuf.ProtoUtils; import io.grpc.servlet.jakarta.GrpcServlet; @@ -42,10 +43,22 @@ public class GrpcFilter extends HttpFilter { private final StubRequestHandler stubRequestHandler; private final List fileDescriptors; + private final JsonMessageConverter jsonMessageConverter; + public GrpcFilter( StubRequestHandler stubRequestHandler, List fileDescriptors) { this.stubRequestHandler = stubRequestHandler; this.fileDescriptors = fileDescriptors; + + final TypeRegistry.Builder typeRegistryBuilder = TypeRegistry.newBuilder(); + fileDescriptors.forEach( + fileDescriptor -> { + fileDescriptor.getMessageTypes().forEach(typeRegistryBuilder::add); + }); + + final TypeRegistry typeRegistry = typeRegistryBuilder.build(); + jsonMessageConverter = new JsonMessageConverter(typeRegistry); + grpcServlet = new GrpcServlet(buildServices()); } @@ -77,9 +90,10 @@ private ServerCallHandler buildHandler( return methodDescriptor.isClientStreaming() ? ServerCalls.asyncClientStreamingCall( new ClientStreamingServerCallHandler( - stubRequestHandler, serviceDescriptor, methodDescriptor)) + stubRequestHandler, serviceDescriptor, methodDescriptor, jsonMessageConverter)) : ServerCalls.asyncUnaryCall( - new UnaryServerCallHandler(stubRequestHandler, serviceDescriptor, methodDescriptor)); + new UnaryServerCallHandler( + stubRequestHandler, serviceDescriptor, methodDescriptor, jsonMessageConverter)); } private static MethodDescriptor buildMessageDescriptorInstance( diff --git a/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java b/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java index e90b593..79382e8 100644 --- a/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java +++ b/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,33 @@ public class GrpcHttpServerFactory implements HttpServerFactory { - private final BlobStore protoDescriptorStore; + private final List fileDescriptors = new ArrayList<>(); public GrpcHttpServerFactory(BlobStore protoDescriptorStore) { - this.protoDescriptorStore = protoDescriptorStore; + protoDescriptorStore + .getAllKeys() + .filter(key -> key.endsWith(".dsc") || key.endsWith(".desc")) + .map( + key -> + protoDescriptorStore + .get(key) + .map( + data -> + Exceptions.uncheck( + () -> DescriptorProtos.FileDescriptorSet.parseFrom(data), + DescriptorProtos.FileDescriptorSet.class))) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(fileDescriptorSet -> fileDescriptorSet.getFileList().stream()) + .forEach( + fileDescriptorProto -> + Exceptions.uncheck( + () -> + fileDescriptors.add( + Descriptors.FileDescriptor.buildFrom( + fileDescriptorProto, + fileDescriptors.toArray(Descriptors.FileDescriptor[]::new), + true)))); } @Override @@ -56,33 +79,6 @@ public HttpServer buildHttpServer( protected void decorateMockServiceContextBeforeConfig( ServletContextHandler mockServiceContext) { - List fileDescriptors = new ArrayList<>(); - - protoDescriptorStore - .getAllKeys() - .filter(key -> key.endsWith(".dsc") || key.endsWith(".desc")) - .map( - key -> - protoDescriptorStore - .get(key) - .map( - data -> - Exceptions.uncheck( - () -> DescriptorProtos.FileDescriptorSet.parseFrom(data), - DescriptorProtos.FileDescriptorSet.class))) - .filter(Optional::isPresent) - .map(Optional::get) - .flatMap(fileDescriptorSet -> fileDescriptorSet.getFileList().stream()) - .forEach( - fileDescriptorProto -> - Exceptions.uncheck( - () -> - fileDescriptors.add( - Descriptors.FileDescriptor.buildFrom( - fileDescriptorProto, - fileDescriptors.toArray(Descriptors.FileDescriptor[]::new), - true)))); - final GrpcFilter grpcFilter = new GrpcFilter(stubRequestHandler, fileDescriptors); final FilterHolder filterHolder = new FilterHolder(grpcFilter); mockServiceContext.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); diff --git a/src/main/java/org/wiremock/grpc/internal/GrpcRequest.java b/src/main/java/org/wiremock/grpc/internal/GrpcRequest.java index c6181ce..d46d742 100644 --- a/src/main/java/org/wiremock/grpc/internal/GrpcRequest.java +++ b/src/main/java/org/wiremock/grpc/internal/GrpcRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.github.tomakehurst.wiremock.common.Encoding; import com.github.tomakehurst.wiremock.common.Strings; import com.github.tomakehurst.wiremock.http.*; -import com.google.protobuf.DynamicMessage; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -37,19 +36,13 @@ public class GrpcRequest implements Request { private final String body; public GrpcRequest( - String scheme, - String host, - int port, - String serviceName, - String methodName, - DynamicMessage message) { + String scheme, String host, int port, String serviceName, String methodName, String body) { this.scheme = scheme; this.host = host; this.port = port; this.serviceName = serviceName; this.methodName = methodName; - - body = JsonMessageUtils.toJson(message); + this.body = body; } @Override diff --git a/src/main/java/org/wiremock/grpc/internal/JsonMessageConverter.java b/src/main/java/org/wiremock/grpc/internal/JsonMessageConverter.java new file mode 100644 index 0000000..44ec61b --- /dev/null +++ b/src/main/java/org/wiremock/grpc/internal/JsonMessageConverter.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wiremock.grpc.internal; + +import com.github.tomakehurst.wiremock.common.Exceptions; +import com.google.protobuf.Message; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.TypeRegistry; +import com.google.protobuf.util.JsonFormat; + +public class JsonMessageConverter { + + private final JsonFormat.Printer jsonPrinter; + private final JsonFormat.Parser jsonParser; + + public JsonMessageConverter(TypeRegistry typeRegistry) { + jsonPrinter = JsonFormat.printer().usingTypeRegistry(typeRegistry); + jsonParser = JsonFormat.parser().usingTypeRegistry(typeRegistry); + } + + public String toJson(MessageOrBuilder message) { + return Exceptions.uncheck(() -> jsonPrinter.print(message), String.class); + } + + @SuppressWarnings("unchecked") + public T toMessage(String json, B builder) { + Exceptions.uncheck(() -> jsonParser.merge(json, builder)); + return (T) builder.build(); + } +} diff --git a/src/main/java/org/wiremock/grpc/internal/JsonMessageUtils.java b/src/main/java/org/wiremock/grpc/internal/JsonMessageUtils.java index 0fb3f06..a6ddf94 100644 --- a/src/main/java/org/wiremock/grpc/internal/JsonMessageUtils.java +++ b/src/main/java/org/wiremock/grpc/internal/JsonMessageUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,25 +15,22 @@ */ package org.wiremock.grpc.internal; -import com.github.tomakehurst.wiremock.common.Exceptions; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.util.JsonFormat; +import com.google.protobuf.TypeRegistry; public class JsonMessageUtils { - private static final JsonFormat.Printer jsonPrinter = JsonFormat.printer(); - private static final JsonFormat.Parser jsonParser = JsonFormat.parser(); + private static final JsonMessageConverter converter = + new JsonMessageConverter(TypeRegistry.getEmptyTypeRegistry()); private JsonMessageUtils() {} public static String toJson(MessageOrBuilder message) { - return Exceptions.uncheck(() -> jsonPrinter.print(message), String.class); + return converter.toJson(message); } - @SuppressWarnings("unchecked") public static T toMessage(String json, B builder) { - Exceptions.uncheck(() -> jsonParser.merge(json, builder)); - return (T) builder.build(); + return converter.toMessage(json, builder); } } diff --git a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java index fcaff4b..0cb1e88 100644 --- a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java +++ b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,20 +29,15 @@ import io.grpc.stub.StreamObserver; import org.wiremock.grpc.dsl.WireMockGrpc; -public class UnaryServerCallHandler +public class UnaryServerCallHandler extends BaseCallHandler implements ServerCalls.UnaryMethod { - private final StubRequestHandler stubRequestHandler; - private final Descriptors.ServiceDescriptor serviceDescriptor; - private final Descriptors.MethodDescriptor methodDescriptor; - public UnaryServerCallHandler( StubRequestHandler stubRequestHandler, Descriptors.ServiceDescriptor serviceDescriptor, - Descriptors.MethodDescriptor methodDescriptor) { - this.stubRequestHandler = stubRequestHandler; - this.serviceDescriptor = serviceDescriptor; - this.methodDescriptor = methodDescriptor; + Descriptors.MethodDescriptor methodDescriptor, + JsonMessageConverter jsonMessageConverter) { + super(stubRequestHandler, serviceDescriptor, methodDescriptor, jsonMessageConverter); } @Override @@ -56,7 +51,7 @@ public void invoke(DynamicMessage request, StreamObserver respon serverAddress.port, serviceDescriptor.getFullName(), methodDescriptor.getName(), - request); + jsonMessageConverter.toJson(request)); stubRequestHandler.handle( wireMockRequest, @@ -92,7 +87,7 @@ public void invoke(DynamicMessage request, StreamObserver respon DynamicMessage.newBuilder(methodDescriptor.getOutputType()); final DynamicMessage response = - JsonMessageUtils.toMessage(resp.getBodyAsString(), messageBuilder); + jsonMessageConverter.toMessage(resp.getBodyAsString(), messageBuilder); responseObserver.onNext(response); responseObserver.onCompleted(); }, diff --git a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java index 53bc240..bf0f24c 100644 --- a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java +++ b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java @@ -331,4 +331,39 @@ private void verifyDefaultMappings() { assertThat(request.getMethod().value(), Matchers.equalTo("GET")); assertThat(request.getUrlPath(), Matchers.equalTo("/hello")); } + + @Test + void unaryMethodWithAnyRequest() { + mockGreetingService.stubFor( + method("greetingAnyRequest") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hiya").build()))); + + String greeting = greetingsClient.greetAnyRequest(); + + assertThat(greeting, is("Hiya")); + } + + @Test + void unaryMethodWithAnyResponse() { + mockGreetingService.stubFor( + method("greetingAnyResponse") + .willReturn(messageAsAny(HelloResponse.newBuilder().setGreeting("Hiya").build()))); + + String typeUrl = greetingsClient.greetAnyResponse(); + + assertThat(typeUrl, is("type.googleapis.com/com.example.grpc.response.HelloResponse")); + } + + @Test + void unaryMethodWithAnyResponseFromJson() { + mockGreetingService.stubFor( + method("greetingAnyResponse") + .willReturn( + json( + "{ \"@type\": \"type.googleapis.com/com.example.grpc.response.HelloResponse\", \"greeting\": \"Hiya\" }"))); + + String typeUrl = greetingsClient.greetAnyResponse(); + + assertThat(typeUrl, is("type.googleapis.com/com.example.grpc.response.HelloResponse")); + } } diff --git a/src/test/java/org/wiremock/grpc/client/GreetingsClient.java b/src/test/java/org/wiremock/grpc/client/GreetingsClient.java index 1c6117a..edd500c 100644 --- a/src/test/java/org/wiremock/grpc/client/GreetingsClient.java +++ b/src/test/java/org/wiremock/grpc/client/GreetingsClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.example.grpc.request.HelloRequest; import com.example.grpc.response.HelloResponse; import com.github.tomakehurst.wiremock.common.Exceptions; +import com.google.protobuf.Any; import com.google.protobuf.Empty; import io.grpc.Channel; import io.grpc.stub.StreamObserver; @@ -44,6 +45,17 @@ public String greet(String name) { return stub.greeting(HelloRequest.newBuilder().setName(name).build()).getGreeting(); } + public String greetAnyRequest() { + return stub.greetingAnyRequest( + Any.newBuilder().setTypeUrl("https://com.example.grpc.request.HelloRequest").build()) + .getGreeting(); + } + + public String greetAnyResponse() { + return stub.greetingAnyResponse(HelloRequest.newBuilder().setName("whatever").build()) + .getTypeUrl(); + } + public List oneGreetingManyReplies(String name) { final CountDownLatch latch = new CountDownLatch(1); diff --git a/src/test/proto/ExampleServices.proto b/src/test/proto/ExampleServices.proto index f565f52..e3bb8f0 100644 --- a/src/test/proto/ExampleServices.proto +++ b/src/test/proto/ExampleServices.proto @@ -23,6 +23,7 @@ option java_multiple_files = true; import "request/helloRequest.proto"; import "response/helloResponse.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/any.proto"; // 4. service, unary request/response service GreetingService { @@ -30,6 +31,8 @@ service GreetingService { rpc manyGreetingsOneReply(stream com.example.grpc.request.HelloRequest) returns (com.example.grpc.response.HelloResponse); rpc oneGreetingManyReplies(com.example.grpc.request.HelloRequest) returns (stream com.example.grpc.response.HelloResponse); rpc oneGreetingEmptyReply(com.example.grpc.request.HelloRequest) returns (google.protobuf.Empty); + rpc greetingAnyRequest(google.protobuf.Any) returns ( com.example.grpc.response.HelloResponse) ; + rpc greetingAnyResponse(com.example.grpc.request.HelloRequest) returns (google.protobuf.Any) ; } service AnotherGreetingService { diff --git a/src/test/resources/wiremock/grpc/services.dsc b/src/test/resources/wiremock/grpc/services.dsc index df663d2..1d3ff59 100644 Binary files a/src/test/resources/wiremock/grpc/services.dsc and b/src/test/resources/wiremock/grpc/services.dsc differ