diff --git a/servicetalk-examples/grpc/protoc-options/build.gradle b/servicetalk-examples/grpc/protoc-options/build.gradle index 8a240681e0..6bddce8cb7 100644 --- a/servicetalk-examples/grpc/protoc-options/build.gradle +++ b/servicetalk-examples/grpc/protoc-options/build.gradle @@ -74,6 +74,7 @@ protobuf { outputSubDir = "java" // Option to append a suffix to type names to avoid naming collisions with other code generation. option 'typeNameSuffix=St' + option 'defaultServiceMethods=true' } } } diff --git a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java index b0dbe12dec..42782b42c7 100644 --- a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java +++ b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java @@ -15,6 +15,8 @@ */ package io.servicetalk.examples.grpc.protocoptions; +import io.grpc.examples.helloworld.HelloRequest; +import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.GrpcServers; import io.grpc.examples.helloworld.GreeterSt.BlockingGreeterService; @@ -23,8 +25,12 @@ public final class BlockingProtocOptionsServer { public static void main(String[] args) throws Exception { GrpcServers.forPort(8080) - .listenAndAwait((BlockingGreeterService) (ctx, request) -> - HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()) + .listenAndAwait(new BlockingGreeterService() { + @Override + public HelloReply sayHello(GrpcServiceContext ctx, HelloRequest request) { + return HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); + } + }) .awaitShutdown(); } } diff --git a/servicetalk-examples/grpc/protoc-options/src/main/proto/test_service.proto b/servicetalk-examples/grpc/protoc-options/src/main/proto/test_service.proto new file mode 100644 index 0000000000..ffe5eeb379 --- /dev/null +++ b/servicetalk-examples/grpc/protoc-options/src/main/proto/test_service.proto @@ -0,0 +1,37 @@ +// +// Copyright © 2019 Apple Inc. and the ServiceTalk project authors +// +// 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. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.servicetalk.grpc.netty"; +option java_outer_classname = "TesterProto"; + +package grpc.netty; + +service Tester { + rpc test (TestRequest) returns (TestResponse) {} + rpc testBiDiStream (stream TestRequest) returns (stream TestResponse) {} + rpc testResponseStream (TestRequest) returns (stream TestResponse) {} + rpc testRequestStream (stream TestRequest) returns (TestResponse) {} +} + +message TestRequest { + string name = 1; +} + +message TestResponse { + string message = 1; +} diff --git a/servicetalk-grpc-protoc/README.adoc b/servicetalk-grpc-protoc/README.adoc index de99af32cd..9f62ed0165 100644 --- a/servicetalk-grpc-protoc/README.adoc +++ b/servicetalk-grpc-protoc/README.adoc @@ -78,3 +78,34 @@ And with Maven: ---- + +==== `defaultServiceMethods= (default: false)` +Generates default service interface methods to ensure implementations will continue to compile as the API is evolved. + +If you are using the +link:https://github.com/google/protobuf-gradle-plugin#configure-what-to-generate[protobuf-gradle-plugin] this is how you +can specify an option: + +[source,gradle] +---- +task.plugins { + servicetalk_grpc { + option 'defaultServiceMethods=true' + } +} +---- + +And with Maven: + +And with Maven: + +[source, xml] +---- + + servicetalk-grpc-protoc + + + defaultServiceMethods=true + + +---- diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java index a0bb8f237f..d4f701389c 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java @@ -248,13 +248,15 @@ private State(ServiceDescriptorProto serviceProto, GenerationContext context, St private final Map messageTypesMap; private final ServiceCommentsMap serviceCommentsMap; private final boolean printJavaDocs; + private final boolean defaultServiceMethods; Generator(final GenerationContext context, final Map messageTypesMap, - final boolean printJavaDocs, SourceCodeInfo sourceCodeInfo) { + final boolean printJavaDocs, final boolean defaultServiceMethods, SourceCodeInfo sourceCodeInfo) { this.context = context; this.messageTypesMap = messageTypesMap; this.serviceCommentsMap = printJavaDocs ? new DefaultServiceCommentsMap(sourceCodeInfo) : NOOP_MAP; this.printJavaDocs = printJavaDocs; + this.defaultServiceMethods = defaultServiceMethods; } /** @@ -1465,6 +1467,33 @@ private TypeSpec newServiceInterfaceSpec(final State state, final boolean blocki .addStatement("return $L", blocking ? BLOCKING_METHOD_DESCRIPTORS : ASYNC_METHOD_DESCRIPTORS) .build()); + // generate default service methods + if (defaultServiceMethods) { + for (int i = 0; i < state.serviceProto.getMethodList().size(); i++) { + final MethodDescriptorProto methodProto = state.serviceProto.getMethodList().get(i); + final ClassName inClass = messageTypesMap.get(methodProto.getInputType()); + final ClassName outClass = messageTypesMap.get(methodProto.getOutputType()); + final String methodName = sanitizeIdentifier(methodProto.getName(), true); + final int methodIndex = i; + interfaceSpecBuilder.addMethod(newRpcMethodSpec(inClass, outClass, methodName, + methodProto.getClientStreaming(), + methodProto.getServerStreaming(), + !blocking ? EnumSet.of(INTERFACE) : EnumSet.of(INTERFACE, BLOCKING), + printJavaDocs, (__, c) -> { + c.addModifiers(DEFAULT) + .addParameter(GrpcServiceContext, ctx) + .addStatement("throw new UnsupportedOperationException(\"Method " + methodName + + " is unimplemented\")"); + if (printJavaDocs) { + extractJavaDocComments(state, methodIndex, c); + c.addJavadoc(JAVADOC_PARAM + ctx + + " context associated with this service and request." + lineSeparator()); + } + return c; + })); + } + } + return interfaceSpecBuilder.build(); } diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Main.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Main.java index 013e332abc..86314a0d66 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Main.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Main.java @@ -91,6 +91,28 @@ public final class Main { * } */ private static final String PRINT_JAVA_DOCS_OPTION = "javaDocs"; + /** + * Supports an option to generate default service interface methods. + *

+ * Gradle: + *

+     * task.plugins {
+     *   servicetalk_grpc {
+     *     option 'defaultServiceMethods=true'
+     *   }
+     * }
+     * 
+ *

+ * Maven: + *

{@code
+     * 
+     *   
+     *     defaultServiceMethods=true
+     *   
+     * 
+     * }
+ */ + private static final String DEFAULT_SERVICE_METHODS = "defaultServiceMethods"; private Main() { // no instances } @@ -149,6 +171,7 @@ private static CodeGeneratorResponse generate(final CodeGeneratorRequest request } final String typeSuffixValue = optionsMap.get(TYPE_NAME_SUFFIX_OPTION); final boolean printJavaDocs = parseBoolean(optionsMap.getOrDefault(PRINT_JAVA_DOCS_OPTION, "true")); + final boolean defaultServiceMethods = parseBoolean(optionsMap.getOrDefault(DEFAULT_SERVICE_METHODS, "false")); final List fileDescriptors = request.getProtoFileList().stream() .map(protoFile -> new FileDescriptor(protoFile, typeSuffixValue)).collect(toList()); @@ -161,7 +184,8 @@ private static CodeGeneratorResponse generate(final CodeGeneratorRequest request for (FileDescriptor f : fileDescriptors) { if (filesToGenerate.contains(f.protoFileName())) { - final Generator generator = new Generator(f, messageTypesMap, printJavaDocs, f.sourceCodeInfo()); + final Generator generator = new Generator(f, messageTypesMap, printJavaDocs, defaultServiceMethods, + f.sourceCodeInfo()); List serviceDescriptorProtoList = f.protoServices(); for (int i = 0; i < serviceDescriptorProtoList.size(); ++i) { ServiceDescriptorProto serviceDescriptor = serviceDescriptorProtoList.get(i);