Skip to content

Commit

Permalink
grpc-protoc typeNameSuffix option (#1184)
Browse files Browse the repository at this point in the history
Motivation:
If a project uses multiple protoc plugins it is possible there are
naming collisions for the resulting generated code. The protoc plugin
interface doesn't give visibility into types generated by other plugins,
and the plugins may not control the package name (specified in the
`.proto` file, maybe used by builtins and plugins if generating into a
single file).

Modifications:
- servicetalk-grpc-protoc now supports a typeNameSuffix option that
appends a postfix to service type names to avoid collisions in the
generated code.

Result:
Users can configure grpc-protoc to generate unique type names to avoid
collisions with other gRPC protoc plugins.
  • Loading branch information
Scottmitch authored Oct 15, 2020
1 parent a0bf19d commit 1e6ac5f
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 4 deletions.
32 changes: 32 additions & 0 deletions servicetalk-grpc-protoc/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
== ServiceTalk protoc plugin for gRPC

This module implements the
link:https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/compiler/plugin.proto[Protoc Plugin Interface]
and generates ServiceTalk code for service definitions contained in `.proto` files.

It is recommended to automate the usage of this plugin in your build via
link:https://github.com/google/protobuf-gradle-plugin[protobuf-gradle-plugin] or
link:https://www.xolstice.org/protobuf-maven-plugin[protobuf-maven-plugin]. Both options
are demonstrated in the
link:{source-root}/servicetalk-examples/grpc/helloworld[gRPC HelloWorld Example].

=== Options
This plugin supports the following
link:https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.compiler.command_line_interface[options]:

==== `typeNameSuffix=<value>`
Appends the <value> onto service type names generated by this plugin. This helps to avoid service type name
collisions if other protoc plugins are generating code in the same gradle project, or in the same package name.

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 'typeNameSuffix=Foo'
}
}
----
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,18 @@ final class FileDescriptor implements GenerationContext {
private final String javaPackageName;
@Nullable
private final String outerClassName;
@Nullable
private final String typeNameSuffix;
private final List<TypeSpec.Builder> serviceClassBuilders;
@Nullable
private Set<String> reservedJavaTypeName;

FileDescriptor(final FileDescriptorProto protoFile) {
FileDescriptor(final FileDescriptorProto protoFile,
@Nullable final String typeNameSuffix) {
this.protoFile = protoFile;
sanitizedProtoFileName = sanitizeFileName(protoFile.getName());
protoPackageName = protoFile.hasPackage() ? protoFile.getPackage() : null;
this.typeNameSuffix = typeNameSuffix;

if (protoFile.hasOptions()) {
final FileOptions fileOptions = protoFile.getOptions();
Expand Down Expand Up @@ -133,7 +137,9 @@ public String deconflictJavaTypeName(final String name) {

@Override
public TypeSpec.Builder newServiceClassBuilder(final ServiceDescriptorProto serviceProto) {
final String className = deconflictJavaTypeName(sanitizeClassName(serviceProto.getName()));
final String rawClassName = typeNameSuffix == null ? serviceProto.getName() :
serviceProto.getName() + typeNameSuffix;
final String className = deconflictJavaTypeName(sanitizeClassName(rawClassName));

final TypeSpec.Builder builder = TypeSpec.classBuilder(className)
.addModifiers(PUBLIC, FINAL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import java.util.Set;

import static com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest.parseFrom;
import static io.servicetalk.grpc.protoc.StringUtils.parseOptions;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

Expand All @@ -39,6 +41,19 @@
* interface to produce ServiceTalk code corresponding to .proto service definitions.
*/
public final class Main {
/**
* Supports an option to append a postfix to service type names generated by this protoc plugin. This is useful
* to avoid conflicts between multiple gRPC implementations generating code for the same .proto file in the same
* gradle project.
* <pre>
* task.plugins {
* servicetalk_grpc {
* option 'typeNameSuffix=Foo'
* }
* }
* </pre>
*/
private static final String TYPE_NAME_SUFFIX_OPTION = "typeNameSuffix";
private Main() {
// no instances
}
Expand Down Expand Up @@ -72,8 +87,12 @@ private static CodeGeneratorResponse generate(final CodeGeneratorRequest request

final Set<String> filesToGenerate = new HashSet<>(request.getFileToGenerateList());

final List<FileDescriptor> fileDescriptors =
request.getProtoFileList().stream().map(FileDescriptor::new).collect(toList());
final Map<String, String> optionsMap = request.hasParameter() ?
parseOptions(request.getParameter()) : emptyMap();
final String typeSuffixValue = optionsMap.get(TYPE_NAME_SUFFIX_OPTION);

final List<FileDescriptor> fileDescriptors = request.getProtoFileList().stream()
.map(protoFile -> new FileDescriptor(protoFile, typeSuffixValue)).collect(toList());

final Map<String, ClassName> messageTypesMap = fileDescriptors.stream()
.map(FileDescriptor::messageTypesMap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.servicetalk.grpc.protoc;

import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;

import static java.lang.Character.toLowerCase;
Expand Down Expand Up @@ -59,4 +61,43 @@ static boolean isNotNullNorEmpty(@Nullable final String v) {
static boolean isNullOrEmpty(@Nullable final String v) {
return v == null || v.isEmpty();
}

/**
* Parse options which are defined to be a comma separated list passed by protoc to the plugin.
* <pre>
* protoc --plug_out=enable_bar:outdir --plug_opt=enable_baz
* protoc --plug_out=enable_bar,enable_baz,mykey=myvalue:outdir
* </pre>
* @param parameters The options as specified by
* <a href="
* https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.compiler.command_line_interface
* ">protoc options</a>
* and
* <a href="
* https://github.com/google/protobuf-gradle-plugin#configure-what-to-generate
* ">protobuf-gradle-plugin options</a>.
* @return A map of the options parsed into &lt;key,value&gt; pairs.
*/
static Map<String, String> parseOptions(String parameters) {
Map<String, String> options = new HashMap<>();
int begin = 0;
while (begin < parameters.length() && begin >= 0) {
int delim = parameters.indexOf(',', begin);
final String option;
if (delim > begin) {
option = parameters.substring(begin, delim);
begin = delim + 1;
} else {
option = parameters.substring(begin);
begin = -1;
}
int equals = option.indexOf('=');
if (equals > 0) {
options.put(option.substring(0, equals), option.substring(equals + 1));
} else {
options.put(option, null);
}
}
return options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright © 2020 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.
*/
package io.servicetalk.grpc.protoc;

import org.junit.Test;

import java.util.Map;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;

public class StringUtilsTest {
@Test
public void emptyOptions() {
Map<String, String> options = StringUtils.parseOptions("");
assertThat(options.isEmpty(), is(true));
}

@Test
public void singleEntryNoValue() {
Map<String, String> options = StringUtils.parseOptions("foo");
assertThat(options.size(), is(1));
assertContainsNullValue(options, "foo");
}

@Test
public void singleEntryValue() {
Map<String, String> options = StringUtils.parseOptions("foo=bar");
assertThat(options.size(), is(1));
assertThat(options.get("foo"), is("bar"));
}

@Test
public void twoEntriesNoValues() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2");
assertThat(options.size(), is(2));
assertContainsNullValue(options, "foo1");
assertContainsNullValue(options, "foo2");
}

@Test
public void twoEntriesFirstValue() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2");
assertThat(options.size(), is(2));
assertThat(options.get("foo1"), is("bar1"));
assertContainsNullValue(options, "foo2");
}

@Test
public void twoEntriesSecondValue() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2=bar2");
assertThat(options.size(), is(2));
assertContainsNullValue(options, "foo1");
assertThat(options.get("foo2"), is("bar2"));
}

@Test
public void twoEntriesBothValues() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2=bar2");
assertThat(options.size(), is(2));
assertThat(options.get("foo1"), is("bar1"));
assertThat(options.get("foo2"), is("bar2"));
}

@Test
public void threeEntriesNoValues() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2,foo3");
assertThat(options.size(), is(3));
assertContainsNullValue(options, "foo1");
assertContainsNullValue(options, "foo2");
assertContainsNullValue(options, "foo3");
}

@Test
public void threeEntriesFirstValue() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2,foo3");
assertThat(options.size(), is(3));
assertThat(options.get("foo1"), is("bar1"));
assertContainsNullValue(options, "foo2");
assertContainsNullValue(options, "foo3");
}

@Test
public void threeEntriesSecondValue() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2=bar2,foo3");
assertThat(options.size(), is(3));
assertContainsNullValue(options, "foo1");
assertThat(options.get("foo2"), is("bar2"));
assertContainsNullValue(options, "foo3");
}

@Test
public void threeEntriesThirdValue() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2,foo3=bar3");
assertThat(options.size(), is(3));
assertContainsNullValue(options, "foo1");
assertContainsNullValue(options, "foo2");
assertThat(options.get("foo3"), is("bar3"));
}

@Test
public void threeEntriesFirstSecondValue() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2=bar2,foo3");
assertThat(options.size(), is(3));
assertThat(options.get("foo1"), is("bar1"));
assertThat(options.get("foo2"), is("bar2"));
assertContainsNullValue(options, "foo3");
}

@Test
public void threeEntriesFirstThirdValue() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2,foo3=bar3");
assertThat(options.size(), is(3));
assertThat(options.get("foo1"), is("bar1"));
assertContainsNullValue(options, "foo2");
assertThat(options.get("foo3"), is("bar3"));
}

@Test
public void threeEntriesSecondThirdValue() {
Map<String, String> options = StringUtils.parseOptions("foo1,foo2=bar2,foo3=bar3");
assertThat(options.size(), is(3));
assertContainsNullValue(options, "foo1");
assertThat(options.get("foo2"), is("bar2"));
assertThat(options.get("foo3"), is("bar3"));
}

@Test
public void threeEntriesValues() {
Map<String, String> options = StringUtils.parseOptions("foo1=bar1,foo2=bar2,foo3=bar3");
assertThat(options.size(), is(3));
assertThat(options.get("foo1"), is("bar1"));
assertThat(options.get("foo2"), is("bar2"));
assertThat(options.get("foo3"), is("bar3"));
}

private static void assertContainsNullValue(Map<String, String> options, String key) {
assertThat(options.containsKey(key), is(true));
assertThat(options.get(key), is(nullValue()));
}
}

0 comments on commit 1e6ac5f

Please sign in to comment.