Add a gRPC server generator for blocking unary methods
This commit is contained in:
parent
63f6fc8634
commit
59cf445a2c
81
pom.xml
81
pom.xml
@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
@ -7,11 +12,87 @@
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>simple-grpc-generator</module>
|
||||
<module>simple-grpc-runtime</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<os.maven.plugin.version>1.7.1</os.maven.plugin.version>
|
||||
<protobuf.plugin.version>0.6.1</protobuf.plugin.version>
|
||||
|
||||
<grpc.version>1.71.0</grpc.version>
|
||||
<jprotoc.version>1.2.2</jprotoc.version>
|
||||
<junit.version>5.12.1</junit.version>
|
||||
<mockito.version>5.16.1</mockito.version>
|
||||
<protoc.version>3.25.5</protoc.version> <!-- Same version as grpc-protobuf -->
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>jprotoc</artifactId>
|
||||
<version>${jprotoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-inprocess</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- necessary for Java 9+ -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
<version>6.0.53</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>${protobuf.plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
96
simple-grpc-generator/pom.xml
Normal file
96
simple-grpc-generator/pom.xml
Normal file
@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>simple-grpc-generator</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>jprotoc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>${os.maven.plugin.version}</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
|
||||
<!-- TODO: If we just need the dump, can we skip the default gRPC generation? -->
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
<goal>test-compile-custom</goal>
|
||||
</goals>
|
||||
|
||||
<configuration>
|
||||
<protocPlugins>
|
||||
<protocPlugin>
|
||||
<id>dump</id>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>jprotoc</artifactId>
|
||||
<version>${jprotoc.version}</version>
|
||||
<mainClass>com.salesforce.jprotoc.dump.DumpGenerator</mainClass>
|
||||
</protocPlugin>
|
||||
</protocPlugins>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.html.HtmlEscapers;
|
||||
import com.google.protobuf.DescriptorProtos;
|
||||
import com.google.protobuf.compiler.PluginProtos;
|
||||
import com.salesforce.jprotoc.Generator;
|
||||
import com.salesforce.jprotoc.GeneratorException;
|
||||
import com.salesforce.jprotoc.ProtoTypeMap;
|
||||
import com.salesforce.jprotoc.ProtocPlugin;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Generates "simple" gRPC server stubs from gRPC service definitions. "Simple" server stubs:
|
||||
* <ul>
|
||||
* <li>Generate blocking, just-return-the-response methods for unary gRPC methods</li>
|
||||
* <li>Generate methods that accept a {@link java.util.concurrent.Flow.Publisher} and return a
|
||||
* {@link java.util.concurrent.CompletionStage} for client-streaming gRPC methods</li>
|
||||
* <li>Generate methods that return a {@code Flow.Publisher} for server-streaming gRPC methods</li>
|
||||
* <li>Generate methods that accept and return a {@code Flow.Publisher} for bidirectional-streaming gRPC methods</li>
|
||||
* </ul>
|
||||
* Callers should take care to provide an appropriate executor to the gRPC server for blocking methods; although many
|
||||
* different kinds of executors could work, this generator was designed with
|
||||
* <a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">virtual threads</a> in mind.
|
||||
* <p>
|
||||
* This generator borrows heavily and with gratitude from the
|
||||
* <a href="https://github.com/salesforce/reactive-grpc">reactive-grpc</a> generator.
|
||||
*/
|
||||
public class SimpleGrpcGenerator extends Generator {
|
||||
|
||||
private static final String CLASS_PREFIX = "Simple";
|
||||
private static final String SERVICE_JAVADOC_PREFIX = " ";
|
||||
private static final String METHOD_JAVADOC_PREFIX = " ";
|
||||
|
||||
private static final int SERVICE_NUMBER_OF_PATHS = 2;
|
||||
private static final int METHOD_NUMBER_OF_PATHS = 4;
|
||||
|
||||
public static void main(final String... args) {
|
||||
if (args.length == 0) {
|
||||
// Generate from protoc via stdin
|
||||
ProtocPlugin.generate(new SimpleGrpcGenerator());
|
||||
} else if (args.length == 1) {
|
||||
// Process from a descriptor_dump file via command line arg
|
||||
ProtocPlugin.debug(new SimpleGrpcGenerator(), args[0]);
|
||||
} else {
|
||||
System.err.println("Usage: SimpleGrpcGenerator [DESCRIPTOR_DUMP_FILE]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PluginProtos.CodeGeneratorResponse.File> generateFiles(final PluginProtos.CodeGeneratorRequest request) throws GeneratorException {
|
||||
final List<DescriptorProtos.FileDescriptorProto> protosToGenerate = request.getProtoFileList().stream()
|
||||
.filter(protoFile -> request.getFileToGenerateList().contains(protoFile.getName()))
|
||||
.toList();
|
||||
|
||||
return generateFiles(findServices(protosToGenerate, ProtoTypeMap.of(request.getProtoFileList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<PluginProtos.CodeGeneratorResponse.Feature> supportedFeatures() {
|
||||
return List.of(PluginProtos.CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL);
|
||||
}
|
||||
|
||||
private List<ServiceContext> findServices(final List<DescriptorProtos.FileDescriptorProto> fileDescriptorProtos,
|
||||
final ProtoTypeMap typeMap) {
|
||||
|
||||
return fileDescriptorProtos.stream()
|
||||
.flatMap(fileDescriptorProto -> IntStream.range(0, fileDescriptorProto.getServiceCount())
|
||||
.mapToObj(serviceNumber -> buildServiceContext(fileDescriptorProto, typeMap, serviceNumber)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static ServiceContext buildServiceContext(final DescriptorProtos.FileDescriptorProto fileDescriptorProto,
|
||||
final ProtoTypeMap typeMap,
|
||||
final int serviceNumber) {
|
||||
|
||||
final DescriptorProtos.ServiceDescriptorProto serviceProto = fileDescriptorProto.getService(serviceNumber);
|
||||
final List<DescriptorProtos.SourceCodeInfo.Location> locations =
|
||||
fileDescriptorProto.getSourceCodeInfo().getLocationList();
|
||||
|
||||
final DescriptorProtos.SourceCodeInfo.Location serviceLocation = locations.stream()
|
||||
.filter(location ->
|
||||
location.getPathCount() >= 2 &&
|
||||
location.getPath(0) == DescriptorProtos.FileDescriptorProto.SERVICE_FIELD_NUMBER &&
|
||||
location.getPath(1) == serviceNumber)
|
||||
.filter(location -> location.getPathCount() == SERVICE_NUMBER_OF_PATHS)
|
||||
.findFirst()
|
||||
.orElseGet(DescriptorProtos.SourceCodeInfo.Location::getDefaultInstance);
|
||||
|
||||
final ServiceContext serviceContext = new ServiceContext();
|
||||
serviceContext.fileName = CLASS_PREFIX + serviceProto.getName() + "Grpc.java";
|
||||
serviceContext.className = CLASS_PREFIX + serviceProto.getName() + "Grpc";
|
||||
serviceContext.serviceName = serviceProto.getName();
|
||||
serviceContext.deprecated = serviceProto.getOptions() != null && serviceProto.getOptions().getDeprecated();
|
||||
serviceContext.protoName = fileDescriptorProto.getName();
|
||||
serviceContext.packageName = extractPackageName(fileDescriptorProto);
|
||||
serviceContext.javaDoc = getJavaDoc(getComments(serviceLocation), SERVICE_JAVADOC_PREFIX);
|
||||
|
||||
for (int methodNumber = 0; methodNumber < serviceProto.getMethodCount(); methodNumber++) {
|
||||
serviceContext.methods.add(buildMethodContext(serviceProto.getMethod(methodNumber), typeMap, locations, methodNumber));
|
||||
}
|
||||
|
||||
return serviceContext;
|
||||
}
|
||||
|
||||
private static String extractPackageName(final DescriptorProtos.FileDescriptorProto proto) {
|
||||
final DescriptorProtos.FileOptions options = proto.getOptions();
|
||||
|
||||
if (options != null) {
|
||||
final String javaPackage = options.getJavaPackage();
|
||||
if (!Strings.isNullOrEmpty(javaPackage)) {
|
||||
return javaPackage;
|
||||
}
|
||||
}
|
||||
|
||||
return Strings.nullToEmpty(proto.getPackage());
|
||||
}
|
||||
|
||||
private static MethodContext buildMethodContext(final DescriptorProtos.MethodDescriptorProto methodProto,
|
||||
final ProtoTypeMap typeMap,
|
||||
final List<DescriptorProtos.SourceCodeInfo.Location> locations,
|
||||
final int methodNumber) {
|
||||
|
||||
final DescriptorProtos.SourceCodeInfo.Location methodLocation = locations.stream()
|
||||
.filter(location ->
|
||||
location.getPathCount() == METHOD_NUMBER_OF_PATHS &&
|
||||
location.getPath(METHOD_NUMBER_OF_PATHS - 1) == methodNumber)
|
||||
.findFirst()
|
||||
.orElseGet(DescriptorProtos.SourceCodeInfo.Location::getDefaultInstance);
|
||||
|
||||
final MethodContext methodContext = new MethodContext();
|
||||
methodContext.methodName = lowerCaseFirst(methodProto.getName());
|
||||
methodContext.inputType = typeMap.toJavaTypeName(methodProto.getInputType());
|
||||
methodContext.outputType = typeMap.toJavaTypeName(methodProto.getOutputType());
|
||||
methodContext.deprecated = methodProto.getOptions() != null && methodProto.getOptions().getDeprecated();
|
||||
methodContext.isManyInput = methodProto.getClientStreaming();
|
||||
methodContext.isManyOutput = methodProto.getServerStreaming();
|
||||
methodContext.methodNumber = methodNumber;
|
||||
methodContext.javaDoc = getJavaDoc(getComments(methodLocation), METHOD_JAVADOC_PREFIX);
|
||||
|
||||
if (!methodProto.getClientStreaming() && !methodProto.getServerStreaming()) {
|
||||
methodContext.simpleCallsMethodName = "unaryCall";
|
||||
methodContext.grpcCallsMethodName = "asyncUnaryCall";
|
||||
}
|
||||
|
||||
if (!methodProto.getClientStreaming() && methodProto.getServerStreaming()) {
|
||||
methodContext.simpleCallsMethodName = "serverStreamingCall";
|
||||
methodContext.grpcCallsMethodName = "asyncServerStreamingCall";
|
||||
}
|
||||
|
||||
if (methodProto.getClientStreaming() && !methodProto.getServerStreaming()) {
|
||||
methodContext.simpleCallsMethodName = "clientStreamingCall";
|
||||
methodContext.grpcCallsMethodName = "asyncClientStreamingCall";
|
||||
}
|
||||
|
||||
if (methodProto.getClientStreaming() && methodProto.getServerStreaming()) {
|
||||
methodContext.simpleCallsMethodName = "bidirectionalStreamingCall";
|
||||
methodContext.grpcCallsMethodName = "asyncBidiStreamingCall";
|
||||
}
|
||||
|
||||
return methodContext;
|
||||
}
|
||||
|
||||
private static String lowerCaseFirst(final String s) {
|
||||
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
|
||||
}
|
||||
|
||||
private List<PluginProtos.CodeGeneratorResponse.File> generateFiles(final List<ServiceContext> services) {
|
||||
return services.stream()
|
||||
.map(this::buildFile)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private PluginProtos.CodeGeneratorResponse.File buildFile(final ServiceContext serviceContext) {
|
||||
return PluginProtos.CodeGeneratorResponse.File
|
||||
.newBuilder()
|
||||
.setName(getGeneratedFilename(serviceContext))
|
||||
.setContent(applyTemplate("SimpleStub.mustache", serviceContext))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative path for a generated file. As the schema notes, this is:
|
||||
* <p>
|
||||
* <blockquote>The file name, relative to the output directory. The name must not contain "." or ".." components and
|
||||
* must be relative, not be absolute (so, the file cannot lie outside the output directory). "/" must be used as the
|
||||
* path separator, not "\".</blockquote>
|
||||
*
|
||||
* @param serviceContext the service context for which to get a generated filename
|
||||
*
|
||||
* @return the relative path to the generated file
|
||||
*
|
||||
* @see <a href="https://github.com/protocolbuffers/protobuf/blob/e8edc5d5e72fa091b0086b4a6d12af0bb66d664b/src/google/protobuf/compiler/plugin.proto#L119-L130">CodeGeneratorResponse.File.name</a>
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String getGeneratedFilename(final ServiceContext serviceContext) {
|
||||
final String dir = serviceContext.packageName != null
|
||||
? serviceContext.packageName.replace('.', '/')
|
||||
: null;
|
||||
|
||||
if (Strings.isNullOrEmpty(dir)) {
|
||||
return serviceContext.fileName;
|
||||
} else {
|
||||
return dir + "/" + serviceContext.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getComments(final DescriptorProtos.SourceCodeInfo.Location location) {
|
||||
return location.getLeadingComments().isEmpty() ? location.getTrailingComments() : location.getLeadingComments();
|
||||
}
|
||||
|
||||
private static String getJavaDoc(final String comments, final String prefix) {
|
||||
if (comments.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final StringBuilder builder = new StringBuilder("/**\n")
|
||||
.append(prefix).append(" * <pre>\n");
|
||||
Arrays.stream(HtmlEscapers.htmlEscaper().escape(comments).split("\n"))
|
||||
.map(line -> line.replace("*/", "*/").replace("*", "*"))
|
||||
.forEach(line -> builder.append(prefix).append(" * ").append(line).append("\n"));
|
||||
|
||||
builder
|
||||
.append(prefix).append(" * </pre>\n")
|
||||
.append(prefix).append(" */");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Template class for proto Service objects.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class ServiceContext {
|
||||
public String fileName;
|
||||
public String protoName;
|
||||
public String packageName;
|
||||
public String className;
|
||||
public String serviceName;
|
||||
public boolean deprecated;
|
||||
public String javaDoc;
|
||||
public List<MethodContext> methods = new ArrayList<>();
|
||||
|
||||
// Although not used directly by the generator, templates may invoke this method
|
||||
@SuppressWarnings("unused")
|
||||
public List<MethodContext> unaryRequestMethods() {
|
||||
return methods.stream().filter(m -> !m.isManyInput).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template class for proto RPC objects.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class MethodContext {
|
||||
public String methodName;
|
||||
public String inputType;
|
||||
public String outputType;
|
||||
public boolean deprecated;
|
||||
public boolean isManyInput;
|
||||
public boolean isManyOutput;
|
||||
public String simpleCallsMethodName;
|
||||
public String grpcCallsMethodName;
|
||||
public int methodNumber;
|
||||
public String javaDoc;
|
||||
|
||||
// This method mimics the upper-casing method of gRPC to ensure compatibility
|
||||
// See https://github.com/grpc/grpc-java/blob/v1.8.0/compiler/src/java_plugin/cpp/java_generator.cpp#L58
|
||||
// Although not used directly by the generator, templates may invoke this method
|
||||
@SuppressWarnings("unused")
|
||||
public String methodNameUpperUnderscore() {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int i = 0; i < methodName.length(); i++) {
|
||||
char c = methodName.charAt(i);
|
||||
s.append(Character.toUpperCase(c));
|
||||
if ((i < methodName.length() - 1) && Character.isLowerCase(c) && Character.isUpperCase(methodName.charAt(i + 1))) {
|
||||
s.append('_');
|
||||
}
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
// Although not used directly by the generator, templates may invoke this method
|
||||
@SuppressWarnings("unused")
|
||||
public String methodNamePascalCase() {
|
||||
String mn = methodName.replace("_", "");
|
||||
return Character.toUpperCase(mn.charAt(0)) + mn.substring(1);
|
||||
}
|
||||
|
||||
// Although not used directly by the generator, templates may invoke this method
|
||||
@SuppressWarnings("unused")
|
||||
public String methodNameCamelCase() {
|
||||
String mn = methodName.replace("_", "");
|
||||
return Character.toLowerCase(mn.charAt(0)) + mn.substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
simple-grpc-generator/src/main/resources/SimpleStub.mustache
Normal file
112
simple-grpc-generator/src/main/resources/SimpleStub.mustache
Normal file
@ -0,0 +1,112 @@
|
||||
{{#packageName}}
|
||||
package {{packageName}};
|
||||
{{/packageName}}
|
||||
|
||||
import static {{packageName}}.{{serviceName}}Grpc.getServiceDescriptor;
|
||||
import static io.grpc.stub.ServerCalls.asyncUnaryCall;
|
||||
import static io.grpc.stub.ServerCalls.asyncServerStreamingCall;
|
||||
import static io.grpc.stub.ServerCalls.asyncClientStreamingCall;
|
||||
import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall;
|
||||
|
||||
|
||||
{{#deprecated}}
|
||||
@java.lang.Deprecated
|
||||
{{/deprecated}}
|
||||
@javax.annotation.Generated(
|
||||
value = "by SimpleGrpc generator",
|
||||
comments = "Source: {{protoName}}")
|
||||
public final class {{className}} {
|
||||
private {{className}}() {}
|
||||
|
||||
{{#javaDoc}}
|
||||
{{{javaDoc}}}
|
||||
{{/javaDoc}}
|
||||
public static abstract class {{serviceName}}ImplBase implements io.grpc.BindableService {
|
||||
|
||||
{{#methods}}
|
||||
{{#javaDoc}}
|
||||
{{{javaDoc}}}
|
||||
{{/javaDoc}}
|
||||
{{#deprecated}}
|
||||
@java.lang.Deprecated
|
||||
{{/deprecated}}
|
||||
public {{#isManyOutput}}java.util.concurrent.Flow.Publisher<{{outputType}}>{{/isManyOutput}}{{^isManyOutput}}{{outputType}}{{/isManyOutput}} {{methodNameCamelCase}}(final {{#isManyInput}}java.util.concurrent.Flow.Publisher<{{inputType}}>{{/isManyInput}}{{^isManyInput}}{{inputType}}{{/isManyInput}} request){{^isManyOutput}} throws Exception {{/isManyOutput}} {
|
||||
throw new io.grpc.StatusRuntimeException(io.grpc.Status.UNIMPLEMENTED);
|
||||
}
|
||||
|
||||
{{/methods}}
|
||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
||||
{{#methods}}
|
||||
.addMethod(
|
||||
{{packageName}}.{{serviceName}}Grpc.get{{methodNamePascalCase}}Method(),
|
||||
{{grpcCallsMethodName}}(
|
||||
new MethodHandlers<
|
||||
{{inputType}},
|
||||
{{outputType}}>(
|
||||
this, METHOD_ID_{{methodNameUpperUnderscore}})))
|
||||
{{/methods}}
|
||||
.build();
|
||||
}
|
||||
|
||||
protected Throwable mapException(final Exception e) {
|
||||
return org.signal.grpc.simple.ServerCalls.mapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
{{#methods}}
|
||||
public static final int METHOD_ID_{{methodNameUpperUnderscore}} = {{methodNumber}};
|
||||
{{/methods}}
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
||||
private final {{serviceName}}ImplBase serviceImpl;
|
||||
private final int methodId;
|
||||
|
||||
MethodHandlers({{serviceName}}ImplBase serviceImpl, int methodId) {
|
||||
this.serviceImpl = serviceImpl;
|
||||
this.methodId = methodId;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public void invoke(final Req request, final io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
{{#methods}}
|
||||
{{^isManyInput}}
|
||||
case METHOD_ID_{{methodNameUpperUnderscore}}:
|
||||
org.signal.grpc.simple.ServerCalls.{{simpleCallsMethodName}}(({{inputType}}) request,
|
||||
(io.grpc.stub.ServerCallStreamObserver<{{outputType}}>) responseObserver,
|
||||
serviceImpl::{{methodNameCamelCase}},
|
||||
serviceImpl::mapException);
|
||||
break;
|
||||
{{/isManyInput}}
|
||||
{{/methods}}
|
||||
default:
|
||||
throw new java.lang.AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(final io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
{{#methods}}
|
||||
{{#isManyInput}}
|
||||
case METHOD_ID_{{methodNameUpperUnderscore}}:
|
||||
return (io.grpc.stub.StreamObserver<Req>) org.signal.grpc.simple.ServerCalls.{{simpleCallsMethodName}}(
|
||||
(io.grpc.stub.ServerCallStreamObserver<{{outputType}}>) responseObserver,
|
||||
serviceImpl::{{methodNameCamelCase}},
|
||||
serviceImpl::mapException);
|
||||
{{/isManyInput}}
|
||||
{{/methods}}
|
||||
default:
|
||||
throw new java.lang.AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
import com.google.protobuf.compiler.PluginProtos;
|
||||
import com.salesforce.jprotoc.ProtocPluginTesting;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SimpleGrpcGeneratorTest {
|
||||
|
||||
@Test
|
||||
void verifyGenerator() throws IOException {
|
||||
final PluginProtos.CodeGeneratorResponse response =
|
||||
ProtocPluginTesting.test(new SimpleGrpcGenerator(), "target/generated-test-sources/protobuf/java/descriptor_dump");
|
||||
|
||||
assertTrue(response.getError().isBlank());
|
||||
assertFalse(response.getFileList().isEmpty());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void getGeneratedFilename(final SimpleGrpcGenerator.ServiceContext serviceContext, final String expectedFilename) {
|
||||
assertEquals(expectedFilename, SimpleGrpcGenerator.getGeneratedFilename(serviceContext));
|
||||
}
|
||||
|
||||
private static List<Arguments> getGeneratedFilename() {
|
||||
final SimpleGrpcGenerator.ServiceContext serviceContextWithPackage = new SimpleGrpcGenerator.ServiceContext();
|
||||
serviceContextWithPackage.packageName = "com.example.test";
|
||||
serviceContextWithPackage.fileName = "Test.java";
|
||||
|
||||
final SimpleGrpcGenerator.ServiceContext serviceContextWithBlankPackage = new SimpleGrpcGenerator.ServiceContext();
|
||||
serviceContextWithBlankPackage.packageName = "";
|
||||
serviceContextWithBlankPackage.fileName = "Test.java";
|
||||
|
||||
final SimpleGrpcGenerator.ServiceContext serviceContextWithNullPackage = new SimpleGrpcGenerator.ServiceContext();
|
||||
serviceContextWithNullPackage.packageName = null;
|
||||
serviceContextWithNullPackage.fileName = "Test.java";
|
||||
|
||||
return List.of(
|
||||
Arguments.argumentSet("Service context with package",
|
||||
serviceContextWithPackage, "com/example/test/Test.java"),
|
||||
|
||||
Arguments.argumentSet("Service context with blank package",
|
||||
serviceContextWithBlankPackage, "Test.java"),
|
||||
|
||||
Arguments.argumentSet("Service context with null package",
|
||||
serviceContextWithNullPackage, "Test.java")
|
||||
);
|
||||
}
|
||||
}
|
||||
34
simple-grpc-generator/src/test/proto/test.proto
Normal file
34
simple-grpc-generator/src/test/proto/test.proto
Normal file
@ -0,0 +1,34 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package org.signal.grpc.simple.test;
|
||||
|
||||
service Test {
|
||||
rpc Unary(UnaryRequest) returns (UnaryResponse) {}
|
||||
rpc ServerStreaming(ServerStreamingRequest) returns (stream ServerStreamingResponse) {}
|
||||
rpc ClientStreaming(stream ClientStreamingRequest) returns (ClientStreamingResponse) {}
|
||||
rpc BidirectionalStreaming(stream BidirectionalStreamingRequest) returns (stream BidirectionalStreamingResponse) {}
|
||||
}
|
||||
|
||||
message UnaryRequest {
|
||||
}
|
||||
|
||||
message UnaryResponse {
|
||||
}
|
||||
|
||||
message ServerStreamingRequest {
|
||||
}
|
||||
|
||||
message ServerStreamingResponse {
|
||||
}
|
||||
|
||||
message ClientStreamingRequest {
|
||||
}
|
||||
|
||||
message ClientStreamingResponse {
|
||||
}
|
||||
|
||||
message BidirectionalStreamingRequest {
|
||||
}
|
||||
|
||||
message BidirectionalStreamingResponse {
|
||||
}
|
||||
106
simple-grpc-runtime/pom.xml
Normal file
106
simple-grpc-runtime/pom.xml
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>simple-grpc-runtime</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-inprocess</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>${os.maven.plugin.version}</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
<goal>test-compile-custom</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<protocPlugins>
|
||||
<protocPlugin>
|
||||
<id>simple</id>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc-generator</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<mainClass>org.signal.grpc.simple.SimpleGrpcGenerator</mainClass>
|
||||
</protocPlugin>
|
||||
</protocPlugins>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
/**
|
||||
* Represents a function that accepts one argument and produces a result and may throw a checked exception.
|
||||
*
|
||||
* @param <T> the type of the input to the function
|
||||
* @param <R> the type of the result of the function
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CheckedFunction<T, R> {
|
||||
R apply(T argument) throws Exception;
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusException;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.stub.ServerCallStreamObserver;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Bridges calls from the gRPC framework to application-space service implementations.
|
||||
*/
|
||||
public class ServerCalls {
|
||||
|
||||
private ServerCalls() {}
|
||||
|
||||
/**
|
||||
* Handles unary (single-request, single-response) calls from a service stub generated by {@code SimpleGrpcGenerator}.
|
||||
*
|
||||
* @param request the request object from the client
|
||||
* @param responseObserver the response observer used by the server to send signals to the client
|
||||
* @param delegate the service method that will handle the business logic of this request
|
||||
* @param exceptionMapper a function that maps exceptions thrown by the {@code delegate} function to an appropriate
|
||||
* gRPC {@link Status}
|
||||
*
|
||||
* @param <Req> the type of request object handled by the {@code delegate} function
|
||||
* @param <Resp> the type of response object returned by the {@code delegate} function
|
||||
*/
|
||||
public static <Req, Resp> void unaryCall(
|
||||
final Req request,
|
||||
final ServerCallStreamObserver<Resp> responseObserver,
|
||||
final CheckedFunction<Req, Resp> delegate,
|
||||
final Function<Exception, Throwable> exceptionMapper) {
|
||||
|
||||
try {
|
||||
final Resp response = delegate.apply(request);
|
||||
|
||||
// Don't try to respond if the server has already canceled the request
|
||||
if (!responseObserver.isCancelled()) {
|
||||
responseObserver.onNext(response);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
final Throwable mappedException = exceptionMapper.apply(e);
|
||||
|
||||
// Don't try to respond if the server has already canceled the request
|
||||
if (!responseObserver.isCancelled()) {
|
||||
responseObserver.onError(mappedException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <Req, Resp> void serverStreamingCall(
|
||||
final Req request,
|
||||
final ServerCallStreamObserver<Resp> responseObserver,
|
||||
final Function<Req, Flow.Publisher<Resp>> delegate,
|
||||
final Function<Exception, Throwable> exceptionMapper) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static <Req, Resp> StreamObserver<Req> clientStreamingCall(
|
||||
final ServerCallStreamObserver<Resp> responseObserver,
|
||||
final CheckedFunction<Flow.Publisher<Req>, Resp> delegate,
|
||||
final Function<Exception, Throwable> exceptionMapper) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static <Req, Resp> StreamObserver<Req> bidirectionalStreamingCall(
|
||||
final ServerCallStreamObserver<Resp> responseObserver,
|
||||
final Function<Flow.Publisher<Req>, Flow.Publisher<Resp>> delegate,
|
||||
final Function<Exception, Throwable> exceptionMapper) {
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a default mapping of exceptions to {@code StatusException}. Exceptions that are already instances of
|
||||
* {@link StatusException} or {@link StatusRuntimeException} are returned unchanged. If the given exception is not
|
||||
* a {@code StatusException} or {@code StatusRuntimeException} but is caused by one of those classes, then the
|
||||
* status-bearing cause is returned.
|
||||
*
|
||||
* @param e the exception to map to a gRPC {@code StatusException}
|
||||
*
|
||||
* @return a {@code StatusException} or {@code StatusRuntimeException} derived from the given exception
|
||||
*/
|
||||
public static Throwable mapException(final Exception e) {
|
||||
if (e instanceof StatusException || e instanceof StatusRuntimeException) {
|
||||
return e;
|
||||
} else {
|
||||
return Status.fromThrowable(e).asException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusException;
|
||||
import io.grpc.stub.ServerCallStreamObserver;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
class ServerCallsTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void unaryCall(final boolean cancelled) {
|
||||
final String response = "Test response";
|
||||
|
||||
@SuppressWarnings("unchecked") final ServerCallStreamObserver<String> responseObserver =
|
||||
mock(ServerCallStreamObserver.class);
|
||||
|
||||
when(responseObserver.isCancelled()).thenReturn(cancelled);
|
||||
|
||||
ServerCalls.unaryCall("Test request", responseObserver, ignored -> response, ServerCalls::mapException);
|
||||
|
||||
verify(responseObserver, atLeastOnce()).isCancelled();
|
||||
|
||||
if (!cancelled) {
|
||||
verify(responseObserver).onNext(response);
|
||||
verify(responseObserver).onCompleted();
|
||||
}
|
||||
|
||||
verifyNoMoreInteractions(responseObserver);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void unaryCallException(final boolean cancelled) {
|
||||
@SuppressWarnings("unchecked") final ServerCallStreamObserver<String> responseObserver =
|
||||
mock(ServerCallStreamObserver.class);
|
||||
|
||||
when(responseObserver.isCancelled()).thenReturn(cancelled);
|
||||
|
||||
final Function<Exception, Throwable> mapException = ignored -> Status.RESOURCE_EXHAUSTED.asException();
|
||||
|
||||
ServerCalls.unaryCall("Test request", responseObserver, ignored -> {
|
||||
throw new RuntimeException();
|
||||
}, mapException);
|
||||
|
||||
verify(responseObserver, atLeastOnce()).isCancelled();
|
||||
|
||||
if (!cancelled) {
|
||||
verify(responseObserver).onError(argThat(throwable ->
|
||||
throwable instanceof StatusException &&
|
||||
((StatusException) throwable).getStatus().getCode() == Status.Code.RESOURCE_EXHAUSTED));
|
||||
}
|
||||
|
||||
verifyNoMoreInteractions(responseObserver);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class SimpleGrpcIntegrationTest {
|
||||
|
||||
private static class CalculatorService extends SimpleCalculatorGrpc.CalculatorImplBase {
|
||||
|
||||
@Override
|
||||
public AdditionResponse add(final AdditionRequest request) {
|
||||
return AdditionResponse.newBuilder().setSum(request.getAddendList().stream()
|
||||
.mapToInt(i -> i)
|
||||
.sum())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExceptionCalculatorService extends SimpleCalculatorGrpc.CalculatorImplBase {
|
||||
|
||||
@Override
|
||||
public AdditionResponse add(final AdditionRequest request) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Throwable mapException(final Exception e) {
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
return Status.INVALID_ARGUMENT.asException();
|
||||
}
|
||||
|
||||
return super.mapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnary() throws IOException {
|
||||
final Server server = InProcessServerBuilder.forName("test")
|
||||
.directExecutor()
|
||||
.addService(new CalculatorService())
|
||||
.build()
|
||||
.start();
|
||||
|
||||
try {
|
||||
final Channel channel = InProcessChannelBuilder.forName("test")
|
||||
.directExecutor()
|
||||
.build();
|
||||
|
||||
final CalculatorGrpc.CalculatorBlockingStub stub = CalculatorGrpc.newBlockingStub(channel);
|
||||
|
||||
final AdditionRequest additionRequest = AdditionRequest.newBuilder()
|
||||
.addAddend(8)
|
||||
.addAddend(6)
|
||||
.build();
|
||||
|
||||
assertEquals(14, stub.add(additionRequest).getSum());
|
||||
} finally {
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnaryException() throws IOException {
|
||||
final Server server = InProcessServerBuilder.forName("test")
|
||||
.directExecutor()
|
||||
.addService(new ExceptionCalculatorService())
|
||||
.build()
|
||||
.start();
|
||||
|
||||
try {
|
||||
final Channel channel = InProcessChannelBuilder.forName("test")
|
||||
.directExecutor()
|
||||
.build();
|
||||
|
||||
final CalculatorGrpc.CalculatorBlockingStub stub = CalculatorGrpc.newBlockingStub(channel);
|
||||
|
||||
final StatusRuntimeException statusRuntimeException =
|
||||
assertThrows(StatusRuntimeException.class, () -> stub.add(AdditionRequest.newBuilder().build()));
|
||||
|
||||
assertEquals(Status.INVALID_ARGUMENT, statusRuntimeException.getStatus());
|
||||
} finally {
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
simple-grpc-runtime/src/test/proto/calculator.proto
Normal file
28
simple-grpc-runtime/src/test/proto/calculator.proto
Normal file
@ -0,0 +1,28 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_multiple_files = true;
|
||||
|
||||
package org.signal.grpc.simple;
|
||||
|
||||
service Calculator {
|
||||
rpc Add(AdditionRequest) returns (AdditionResponse) {}
|
||||
rpc AddStream(stream AdditionRequest) returns (AdditionResponse) {}
|
||||
rpc Factor(FactorRequest) returns (stream FactorResponse) {}
|
||||
rpc RunningAddition(stream AdditionRequest) returns (stream AdditionResponse) {}
|
||||
}
|
||||
|
||||
message AdditionRequest {
|
||||
repeated int32 addend = 1;
|
||||
}
|
||||
|
||||
message AdditionResponse {
|
||||
int64 sum = 1;
|
||||
}
|
||||
|
||||
message FactorRequest {
|
||||
uint64 number = 1;
|
||||
}
|
||||
|
||||
message FactorResponse {
|
||||
uint64 factor = 1;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user