From fd218de18288d1ef48ffc9fd8cb33908eb886ae3 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Tue, 15 Sep 2020 15:13:10 -0700 Subject: [PATCH 01/22] feat: add protobuf comment parser util --- .../protoparser/SourceCodeInfoParser.java | 300 ++++++++++++++++++ .../generator/gapic/protoparser/BUILD.bazel | 2 + .../protoparser/SourceCodeInfoParserTest.java | 164 ++++++++++ .../api/generator/gapic/testdata/BUILD.bazel | 10 + .../api/generator/gapic/testdata/basic.proto | 80 +++++ 5 files changed, 556 insertions(+) create mode 100644 src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java create mode 100644 src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java create mode 100644 src/test/java/com/google/api/generator/gapic/testdata/basic.proto diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java new file mode 100644 index 0000000000..ef1552c441 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java @@ -0,0 +1,300 @@ +// Copyright 2020 Google LLC +// +// 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 com.google.api.generator.gapic.protoparser; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimaps; +import com.google.protobuf.DescriptorProtos.DescriptorProto; +import com.google.protobuf.DescriptorProtos.EnumDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; +import com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.Descriptors.EnumValueDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * A helper class which provides protocol buffer source info for descriptors. + * + *

In order to make this work, the descriptors need to be produced using the flag {@code + * --include_source_info}. Note that descriptors taken from the generated java code have source info + * stripped, and won't work with this class. + * + *

This class uses internal caches to speed up access to the source info. It is not thread safe. + * If you think you need this functionality in a thread-safe context, feel free to suggest a + * refactor. + */ +public class SourceCodeInfoParser { + /** + * A map from file descriptors to the analyzed source info, stored as a multimap from a path of + * the form {@code n.m.l} to the location info. + */ + private final Map> fileToPathToLocation = + Maps.newHashMap(); + + /** A map from descriptor objects to the path to those objects in their proto file. */ + private final Map descriptorToPath = Maps.newHashMap(); + + /** Gets the location of a message, if available. */ + @Nullable + public Location getLocation(Descriptor message) { + FileDescriptor file = message.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(message)); + } + + /** Gets the location of a field, if available. */ + @Nullable + public Location getLocation(FieldDescriptor field) { + FileDescriptor file = field.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(field)); + } + + /** Gets the location of a service, if available. */ + @Nullable + public Location getLocation(ServiceDescriptor service) { + FileDescriptor file = service.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(service)); + } + + /** Gets the location of a method, if available. */ + @Nullable + public Location getLocation(MethodDescriptor method) { + FileDescriptor file = method.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(method)); + } + + /** Gets the location of an enum type, if available. */ + @Nullable + public Location getLocation(EnumDescriptor enumType) { + FileDescriptor file = enumType.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(enumType)); + } + + /** Gets the location of an enum value, if available. */ + @Nullable + public Location getLocation(EnumValueDescriptor enumValue) { + FileDescriptor file = enumValue.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(enumValue)); + } + + /** Gets the location of a oneof, if available. */ + @Nullable + public Location getLocation(OneofDescriptor oneof) { + FileDescriptor file = oneof.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return getLocation(file, buildPath(oneof)); + } + + // ----------------------------------------------------------------------------- + // Helpers + + /** + * A helper to compute the location based on a file descriptor and a path into that descriptor. + */ + private Location getLocation(FileDescriptor file, String path) { + ImmutableList cands = getCandidateLocations(file, path); + if (cands != null && cands.isEmpty()) { + return null; + } else { + return cands.get(0); // We choose the first one. + } + } + + private ImmutableList getCandidateLocations(FileDescriptor file, String path) { + ImmutableListMultimap locationMap = fileToPathToLocation.get(file); + if (locationMap == null) { + locationMap = + Multimaps.index( + file.toProto().getSourceCodeInfo().getLocationList(), + new Function() { + @Override + public String apply(Location location) { + return Joiner.on('.').join(location.getPathList()); + } + }); + fileToPathToLocation.put(file, locationMap); + } + return locationMap.get(path); + } + + private String buildPath(Descriptor message) { + String path = descriptorToPath.get(message); + if (path != null) { + return path; + } + if (message.getContainingType() != null) { + path = + String.format( + "%s.%d.%d", + buildPath(message.getContainingType()), + DescriptorProto.NESTED_TYPE_FIELD_NUMBER, + message.getContainingType().getNestedTypes().indexOf(message)); + } else { + path = + String.format( + "%d.%d", + FileDescriptorProto.MESSAGE_TYPE_FIELD_NUMBER, + message.getFile().getMessageTypes().indexOf(message)); + } + descriptorToPath.put(message, path); + return path; + } + + private String buildPath(FieldDescriptor field) { + String path = descriptorToPath.get(field); + if (path != null) { + return path; + } + if (field.isExtension()) { + if (field.getExtensionScope() == null) { + path = + String.format( + "%d.%d", + FileDescriptorProto.EXTENSION_FIELD_NUMBER, + field.getFile().getExtensions().indexOf(field)); + } else { + path = + String.format( + "%s.%d.%d", + buildPath(field.getExtensionScope()), + DescriptorProto.EXTENSION_FIELD_NUMBER, + field.getExtensionScope().getExtensions().indexOf(field)); + } + } else { + path = + String.format( + "%s.%d.%d", + buildPath(field.getContainingType()), + DescriptorProto.FIELD_FIELD_NUMBER, + field.getContainingType().getFields().indexOf(field)); + } + descriptorToPath.put(field, path); + return path; + } + + private String buildPath(ServiceDescriptor service) { + String path = descriptorToPath.get(service); + if (path != null) { + return path; + } + path = + String.format( + "%d.%d", + FileDescriptorProto.SERVICE_FIELD_NUMBER, + service.getFile().getServices().indexOf(service)); + descriptorToPath.put(service, path); + return path; + } + + private String buildPath(MethodDescriptor method) { + String path = descriptorToPath.get(method); + if (path != null) { + return path; + } + path = + String.format( + "%s.%d.%d", + buildPath(method.getService()), + ServiceDescriptorProto.METHOD_FIELD_NUMBER, + method.getService().getMethods().indexOf(method)); + descriptorToPath.put(method, path); + return path; + } + + private String buildPath(EnumDescriptor enumType) { + String path = descriptorToPath.get(enumType); + if (path != null) { + return path; + } + if (enumType.getContainingType() != null) { + path = + String.format( + "%s.%d.%d", + buildPath(enumType.getContainingType()), + DescriptorProto.ENUM_TYPE_FIELD_NUMBER, + enumType.getContainingType().getEnumTypes().indexOf(enumType)); + } else { + path = + String.format( + "%d.%d", + FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER, + enumType.getFile().getEnumTypes().indexOf(enumType)); + } + descriptorToPath.put(enumType, path); + return path; + } + + private String buildPath(EnumValueDescriptor enumValue) { + String path = descriptorToPath.get(enumValue); + if (path != null) { + return path; + } + path = + String.format( + "%s.%d.%d", + buildPath(enumValue.getType()), + EnumDescriptorProto.VALUE_FIELD_NUMBER, + enumValue.getType().getValues().indexOf(enumValue)); + descriptorToPath.put(enumValue, path); + return path; + } + + private String buildPath(OneofDescriptor oneof) { + String path = descriptorToPath.get(oneof); + if (path != null) { + return path; + } + path = + String.format( + "%s.%d.%d", + buildPath(oneof.getContainingType()), + DescriptorProto.ONEOF_DECL_FIELD_NUMBER, + oneof.getContainingType().getOneofs().indexOf(oneof)); + + descriptorToPath.put(oneof, path); + return path; + } +} diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel index d8bacd3937..cde28cbf97 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel @@ -8,6 +8,7 @@ TESTS = [ "ResourceNameParserTest", "ResourceReferenceParserTest", "ServiceConfigParserTest", + "SourceCodeInfoParserTest", ] filegroup( @@ -19,6 +20,7 @@ filegroup( name = test_name, srcs = ["{0}.java".format(test_name)], data = [ + "//src/test/java/com/google/api/generator/gapic/testdata:basic_proto_descriptor", "//src/test/java/com/google/api/generator/gapic/testdata:gapic_config_files", "//src/test/java/com/google/api/generator/gapic/testdata:service_config_files", ], diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java new file mode 100644 index 0000000000..06d0d0c753 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java @@ -0,0 +1,164 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this protoFile 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 com.google.api.generator.gapic.protoparser; + +import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public class SourceCodeInfoParserTest { + private static final String TEST_PROTO_FILE = + "src/test/java/com/google/api/generator/gapic/testdata/basic_proto.descriptor"; + + private SourceCodeInfoParser parser; + private FileDescriptor protoFile; + + @Before + public void setUp() throws Exception { + parser = new SourceCodeInfoParser(); + protoFile = buildFileDescriptor(); + } + + @Test + public void getServiceInfo() { + Location location = parser.getLocation(protoFile.findServiceByName("FooService")); + assertEquals( + " This is a service description.\n It takes up multiple lines, like so.\n", + location.getLeadingComments()); + + location = parser.getLocation(protoFile.findServiceByName("BarService")); + assertEquals(" This is another service description.\n", location.getLeadingComments()); + } + + @Test + public void getMethodInfo() { + ServiceDescriptor service = protoFile.findServiceByName("FooService"); + Location location = parser.getLocation(service.findMethodByName("FooMethod")); + assertEquals( + " FooMethod does something.\n This comment also takes up multiple lines.\n", + location.getLeadingComments()); + + service = protoFile.findServiceByName("BarService"); + location = parser.getLocation(service.findMethodByName("BarMethod")); + assertEquals(" BarMethod does another thing.\n", location.getLeadingComments()); + } + + @Test + public void getOuterMessageInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + Location location = parser.getLocation(message); + assertEquals( + " This is a message descxription.\n" + + " Lorum ipsum dolor sit amet consectetur adipiscing elit.\n", + location.getLeadingComments()); + + // Fields. + location = parser.getLocation(message.findFieldByName("field_one")); + assertEquals( + " This is a field description for field_one.\n" + + " And here is the second line of that description.\n", + location.getLeadingComments()); + assertEquals(" A field trailing comment.\n", location.getTrailingComments()); + + location = parser.getLocation(message.findFieldByName("field_two")); + assertEquals(" This is another field description.\n", location.getLeadingComments()); + assertEquals(" Another field trailing comment.\n", location.getTrailingComments()); + } + + @Test + public void getInnerMessageInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + assertThat(message).isNotNull(); + message = message.findNestedTypeByName("BarMessage"); + + Location location = parser.getLocation(message); + assertEquals( + " This is an inner message description for BarMessage.\n", location.getLeadingComments()); + + // Fields. + location = parser.getLocation(message.findFieldByName("field_three")); + assertEquals(" A third leading comment for field_three.\n", location.getLeadingComments()); + + location = parser.getLocation(message.findFieldByName("field_two")); + assertEquals("\n This is a block comment for field_two.\n", location.getLeadingComments()); + } + + @Test + public void getOuterEnumInfo() { + EnumDescriptor protoEnum = protoFile.findEnumTypeByName("OuterEnum"); + Location location = parser.getLocation(protoEnum); + assertEquals(" This is an outer enum.\n", location.getLeadingComments()); + + // Enum fields. + location = parser.getLocation(protoEnum.findValueByName("VALUE_UNSPECIFIED")); + assertEquals(" Another unspecified value.\n", location.getLeadingComments()); + } + + @Test + public void getInnerEnumInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + EnumDescriptor protoEnum = message.findEnumTypeByName("FoodEnum"); + Location location = parser.getLocation(protoEnum); + assertEquals(" An inner enum.\n", location.getLeadingComments()); + + // Enum fields. + location = parser.getLocation(protoEnum.findValueByName("RICE")); + assertEquals(" 😋 🍚.\n", location.getLeadingComments()); + location = parser.getLocation(protoEnum.findValueByName("CHOCOLATE")); + assertEquals(" 🤤 🍫.\n", location.getLeadingComments()); + } + + @Test + public void getOnoeofInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + OneofDescriptor protoOneof = message.getOneofs().get(0); + Location location = parser.getLocation(protoOneof); + assertEquals(" An inner oneof.\n", location.getLeadingComments()); + + location = parser.getLocation(protoOneof.getField(0)); + assertEquals(" An InnerOneof comment for its field.\n", location.getLeadingComments()); + } + + /** + * Parses a {@link FileDescriptorSet} from the {@link TEST_PROTO_FILE} and converts the protos to + * {@link FileDescriptor} wrappers. + * + * @return the top level target protoFile descriptor + */ + private static FileDescriptor buildFileDescriptor() throws Exception { + FileDescriptor result = null; + List protoFileList = + FileDescriptorSet.parseFrom(new FileInputStream(TEST_PROTO_FILE)).getFileList(); + List deps = new ArrayList<>(); + for (FileDescriptorProto proto : protoFileList) { + result = FileDescriptor.buildFrom(proto, deps.toArray(new FileDescriptor[0])); + deps.add(result); + } + return result; + } +} diff --git a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel index 44d2b3bf82..600b3cb18e 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel @@ -10,6 +10,16 @@ filegroup( srcs = glob(["*_gapic.yaml"]), ) +genrule( + name = "basic_proto_descriptor", + srcs = [ + "basic.proto", + ], + outs = ["basic_proto.descriptor"], + cmd = ("protoc --include_source_info --include_imports --descriptor_set_out=$@ $(SRCS)"), + message = "Generating proto descriptor", +) + proto_library( name = "showcase_proto", srcs = [ diff --git a/src/test/java/com/google/api/generator/gapic/testdata/basic.proto b/src/test/java/com/google/api/generator/gapic/testdata/basic.proto new file mode 100644 index 0000000000..bee4393336 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/testdata/basic.proto @@ -0,0 +1,80 @@ +// Copyright 2018 Google LLC +// +// 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 +// +// https://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"; + +package google.testdata; + +option java_package = "com.google.google.testdata"; + +// This is a service description. +// It takes up multiple lines, like so. +service FooService { + // FooMethod does something. + // This comment also takes up multiple lines. + rpc FooMethod(FooMessage) returns (FooMessage.BarMessage); +} + +// This is another service description. +service BarService { + // BarMethod does another thing. + rpc BarMethod(FooMessage) returns (FooMessage.BarMessage); +} + +// This is a message descxription. +// Lorum ipsum dolor sit amet consectetur adipiscing elit. +message FooMessage { + // This is a field description for field_one. + // And here is the second line of that description. + string field_one = 1; // A field trailing comment. + + // This is another field description. + string field_two = 2; + // Another field trailing comment. + + // This is an inner message description for BarMessage. + message BarMessage { + // A third leading comment for field_three. + string field_three = 1; + + /* + * This is a block comment for field_two. + */ + string field_two = 2; + } + + // An inner enum. + enum FoodEnum { + // Unspecified value. + FOOD_UNSPECIFIED = 0; + + // 😋 🍚. + RICE = 1; + + // 🤤 🍫. + CHOCOLATE = 2; + } + + // An inner oneof. + oneof InnerOneof { + // An InnerOneof comment for its field. + string field_four = 6; + } +} + +// This is an outer enum. +enum OuterEnum { + // Another unspecified value. + VALUE_UNSPECIFIED = 0; +} From 8c792e36e0e2b3a40719df1d29ab7f712655ade3 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Tue, 15 Sep 2020 15:16:29 -0700 Subject: [PATCH 02/22] fix: add basic proto build rules --- .../java/com/google/api/generator/gapic/testdata/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel index 600b3cb18e..e4569f7cb4 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel @@ -16,7 +16,7 @@ genrule( "basic.proto", ], outs = ["basic_proto.descriptor"], - cmd = ("protoc --include_source_info --include_imports --descriptor_set_out=$@ $(SRCS)"), + cmd = "protoc --include_source_info --include_imports --descriptor_set_out=$@ $(SRCS)", message = "Generating proto descriptor", ) From d6ee06813167937dcff91d721d998933573158c3 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Tue, 15 Sep 2020 15:16:58 -0700 Subject: [PATCH 03/22] feat: add header comments to ServiceClient --- .../composer/ServiceClientClassComposer.java | 2 + .../ServiceClientCommentComposer.java | 83 +++++++++++++++++++ .../api/generator/gapic/model/Service.java | 11 ++- .../ServiceClientClassComposerTest.java | 49 +++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java index 74eaa5c6bd..cf0f2367bb 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java @@ -101,6 +101,8 @@ public GapicClass generate(Service service, Map messageTypes) { ClassDefinition classDef = ClassDefinition.builder() + .setHeaderCommentStatements( + ServiceClientCommentComposer.createClassHeaderComments(service)) .setPackageString(pakkage) .setAnnotations(createClassAnnotations(types)) .setImplementsTypes(createClassImplements(types)) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java index 3ace86e832..46b7d61bbe 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java @@ -17,14 +17,62 @@ import com.google.api.generator.engine.ast.CommentStatement; import com.google.api.generator.engine.ast.JavaDocComment; import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.gapic.utils.JavaStyle; +import java.util.Arrays; +import java.util.List; class ServiceClientCommentComposer { + // Tokens. private static final String COLON = ":"; + // Constants. + private static final String SERVICE_DESCRIPTION_INTRO_STRING = + "This class provides the ability to make remote calls to the backing service through method " + + "calls that map to API methods. Sample code to get started:"; + private static final String SERVICE_DESCRIPTION_CLOSE_STRING = + "Note: close() needs to be called on the echoClient object to clean up resources such as " + + "threads. In the example above, try-with-resources is used, which automatically calls " + + "close()."; + private static final String SERVICE_DESCRIPTION_SURFACE_SUMMARY_STRING = + "The surface of this class includes several types of Java methods for each of the API's " + + "methods:"; + private static final String SERVICE_DESCRIPTION_SURFACE_CODA_STRING = + "See the individual methods for example code."; + private static final String SERVICE_DESCRIPTION_RESOURCE_NAMES_FORMATTING_STRING = + "Many parameters require resource names to be formatted in a particular way. To assist with" + + " these names, this class includes a format method for each type of name, and" + + " additionally a parse method to extract the individual identifiers contained within" + + " names that are returned."; + private static final String SERVICE_DESCRIPTION_CREDENTIALS_SUMMARY_STRING = + "To customize credentials:"; + private static final String SERVICE_DESCRIPTION_ENDPOINT_SUMMARY_STRING = + "To customize the endpoint:"; + + private static final List SERVICE_DESCRIPTION_SURFACE_DESCRIPTION = + Arrays.asList( + "A \"flattened\" method. With this type of method, the fields of the request type have" + + " been converted into function parameters. It may be the case that not all fields" + + " are available as parameters, and not every API method will have a flattened" + + " method entry point.", + "A \"request object\" method. This type of method only takes one parameter, a request" + + " object, which must be constructed before the call. Not every API method will" + + " have a request object method.", + "A \"callable\" method. This type of method takes no parameters and returns an immutable " + + "API callable object, which can be used to initiate calls to the service."); + + // Patterns. private static final String CREATE_METHOD_STUB_ARG_PATTERN = "Constructs an instance of EchoClient, using the given stub for making calls. This is for" + " advanced usage - prefer using create(%s)."; + private static final String SERVICE_DESCRIPTION_CUSTOMIZE_SUMMARY_PATTERN = + "This class can be customized by passing in a custom instance of %s to create(). For" + + " example:"; + + private static final String SERVICE_DESCRIPTION_SUMMARY_PATTERN = "Service Description: %s"; + + // Comments. static final CommentStatement CREATE_METHOD_NO_ARG_COMMENT = toSimpleComment("Constructs an instance of EchoClient with default settings."); @@ -45,6 +93,41 @@ class ServiceClientCommentComposer { "Returns the OperationsClient that can be used to query the status of a long-running" + " operation returned by another API method call."); + static List createClassHeaderComments(Service service) { + JavaDocComment.Builder classHeaderJavadocBuilder = JavaDocComment.builder(); + if (service.hasDescription()) { + classHeaderJavadocBuilder.addComment( + String.format(SERVICE_DESCRIPTION_SUMMARY_PATTERN, service.description())); + } + + // Service introduction. + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_INTRO_STRING); + // TODO(summerji): Add sample code here. + + // API surface description. + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_CLOSE_STRING); + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_SURFACE_SUMMARY_STRING); + classHeaderJavadocBuilder.addOrderedList(SERVICE_DESCRIPTION_SURFACE_DESCRIPTION); + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_SURFACE_CODA_STRING); + + // Formatting resource names. + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_RESOURCE_NAMES_FORMATTING_STRING); + + // Customization examples. + classHeaderJavadocBuilder.addParagraph( + String.format( + SERVICE_DESCRIPTION_CUSTOMIZE_SUMMARY_PATTERN, + String.format("%sSettings", JavaStyle.toUpperCamelCase(service.name())))); + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_CREDENTIALS_SUMMARY_STRING); + // TODO(summerji): Add credentials' customization sample code here. + classHeaderJavadocBuilder.addParagraph(SERVICE_DESCRIPTION_ENDPOINT_SUMMARY_STRING); + // TODO(summerji): Add endpoint customization sample code here. + + return Arrays.asList( + CommentComposer.AUTO_GENERATED_CLASS_COMMENT, + CommentStatement.withComment(classHeaderJavadocBuilder.build())); + } + static CommentStatement createCreateMethodStubArgComment(TypeNode settingsType) { return toSimpleComment( String.format(CREATE_METHOD_STUB_ARG_PATTERN, settingsType.reference().name())); diff --git a/src/main/java/com/google/api/generator/gapic/model/Service.java b/src/main/java/com/google/api/generator/gapic/model/Service.java index 22bf2e3c24..a56567af50 100644 --- a/src/main/java/com/google/api/generator/gapic/model/Service.java +++ b/src/main/java/com/google/api/generator/gapic/model/Service.java @@ -15,8 +15,10 @@ package com.google.api.generator.gapic.model; import com.google.auto.value.AutoValue; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import java.util.List; +import javax.annotation.Nullable; @AutoValue public abstract class Service { @@ -32,7 +34,12 @@ public abstract class Service { public abstract ImmutableList methods(); - // TODO(miraleung): Get comments. + @Nullable + public abstract String description(); + + public boolean hasDescription() { + return !Strings.isNullOrEmpty(description()); + } public static Builder builder() { return new AutoValue_Service.Builder().setMethods(ImmutableList.of()); @@ -52,6 +59,8 @@ public abstract static class Builder { public abstract Builder setMethods(List methods); + public abstract Builder setDescription(String description); + public abstract Service build(); } } diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index b18cdbe0ea..27e33bfa2e 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -83,6 +83,55 @@ public void generateServiceClasses() { + "import java.util.concurrent.TimeUnit;\n" + "import javax.annotation.Generated;\n" + "\n" + + "// AUTO-GENERATED DOCUMENTATION AND CLASS.\n" + + "/**\n" + + " * This class provides the ability to make remote calls to the backing service" + + " through method calls\n" + + " * that map to API methods. Sample code to get started:\n" + + " *\n" + + " *

Note: close() needs to be called on the echoClient object to clean up resources" + + " such as\n" + + " * threads. In the example above, try-with-resources is used, which automatically" + + " calls close().\n" + + " *\n" + + " *

The surface of this class includes several types of Java methods for each of" + + " the API's\n" + + " * methods:\n" + + " *\n" + + " *

    \n" + + " *
  1. A \"flattened\" method. With this type of method, the fields of the request" + + " type have been\n" + + " * converted into function parameters. It may be the case that not all fields" + + " are available as\n" + + " * parameters, and not every API method will have a flattened method entry" + + " point.\n" + + " *
  2. A \"request object\" method. This type of method only takes one parameter, a" + + " request object,\n" + + " * which must be constructed before the call. Not every API method will have a" + + " request object\n" + + " * method.\n" + + " *
  3. A \"callable\" method. This type of method takes no parameters and returns" + + " an immutable API\n" + + " * callable object, which can be used to initiate calls to the service.\n" + + " *
\n" + + " *\n" + + " *

See the individual methods for example code.\n" + + " *\n" + + " *

Many parameters require resource names to be formatted in a particular way. To" + + " assist with\n" + + " * these names, this class includes a format method for each type of name, and" + + " additionally a parse\n" + + " * method to extract the individual identifiers contained within names that are" + + " returned.\n" + + " *\n" + + " *

This class can be customized by passing in a custom instance of EchoSettings to" + + " create(). For\n" + + " * example:\n" + + " *\n" + + " *

To customize credentials:\n" + + " *\n" + + " *

To customize the endpoint:\n" + + " */\n" + "@BetaApi\n" + "@Generated(\"by gapic-generator\")\n" + "public class EchoClient implements BackgroundResource {\n" From 82aa0b6e9d9e58c5c494c27a3c5209b837593049 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Tue, 15 Sep 2020 15:27:37 -0700 Subject: [PATCH 04/22] fix: build protoc at test time --- .../com/google/api/generator/gapic/testdata/BUILD.bazel | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel index e4569f7cb4..744138900f 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel @@ -16,8 +16,13 @@ genrule( "basic.proto", ], outs = ["basic_proto.descriptor"], - cmd = "protoc --include_source_info --include_imports --descriptor_set_out=$@ $(SRCS)", + # CircleCI does not have protoc installed. + cmd = "$(location @com_google_protobuf//:protoc) " + + "--include_source_info --include_imports --descriptor_set_out=$@ $(SRCS)", message = "Generating proto descriptor", + tools = [ + "@com_google_protobuf//:protoc", + ], ) proto_library( From 2405b1ad3df5d5f8011a7c1c64c8ea2904e2f4c9 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Tue, 15 Sep 2020 16:19:50 -0700 Subject: [PATCH 05/22] fix!: wrap protobuf location and process comments --- .../gapic/model/SourceCodeInfoLocation.java | 64 +++++++++++++++++++ .../protoparser/SourceCodeInfoParser.java | 29 +++++---- .../protoparser/SourceCodeInfoParserTest.java | 57 ++++++++--------- 3 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/google/api/generator/gapic/model/SourceCodeInfoLocation.java diff --git a/src/main/java/com/google/api/generator/gapic/model/SourceCodeInfoLocation.java b/src/main/java/com/google/api/generator/gapic/model/SourceCodeInfoLocation.java new file mode 100644 index 0000000000..fca6e7c791 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/model/SourceCodeInfoLocation.java @@ -0,0 +1,64 @@ +// Copyright 2020 Google LLC +// +// 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 com.google.api.generator.gapic.model; + +import com.google.common.escape.Escaper; +import com.google.common.escape.Escapers; +import com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location; +import javax.annotation.Nonnull; + +/** + * A light wrapper around SourceCodeInfo.Location to provide cleaner protobuf comments. Please see + * additional documentation on descriptor.proto. + */ +public class SourceCodeInfoLocation { + // Not a singleton because of nested-class instantiation mechanics. + private final NewlineEscaper ESCAPER = new NewlineEscaper(); + + @Nonnull private final Location location; + + private SourceCodeInfoLocation(Location location) { + this.location = location; + } + + public static SourceCodeInfoLocation create(@Nonnull Location location) { + return new SourceCodeInfoLocation(location); + } + + public String getLeadingComments() { + return processProtobufComment(location.getLeadingComments()); + } + + public String getTrailingComments() { + return processProtobufComment(location.getTrailingComments()); + } + + public String getLeadingDetachedComments(int index) { + return processProtobufComment(location.getLeadingDetachedComments(index)); + } + + private String processProtobufComment(String s) { + return ESCAPER.escape(s).trim(); + } + + private class NewlineEscaper extends Escaper { + private final Escaper charEscaper = Escapers.builder().addEscape('\n', "").build(); + + @Override + public String escape(String sourceString) { + return charEscaper.escape(sourceString); + } + } +} diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java index ef1552c441..bfa4c62297 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java @@ -14,6 +14,7 @@ package com.google.api.generator.gapic.protoparser; +import com.google.api.generator.gapic.model.SourceCodeInfoLocation; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -60,72 +61,72 @@ public class SourceCodeInfoParser { /** Gets the location of a message, if available. */ @Nullable - public Location getLocation(Descriptor message) { + public SourceCodeInfoLocation getLocation(Descriptor message) { FileDescriptor file = message.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(message)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(message))); } /** Gets the location of a field, if available. */ @Nullable - public Location getLocation(FieldDescriptor field) { + public SourceCodeInfoLocation getLocation(FieldDescriptor field) { FileDescriptor file = field.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(field)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(field))); } /** Gets the location of a service, if available. */ @Nullable - public Location getLocation(ServiceDescriptor service) { + public SourceCodeInfoLocation getLocation(ServiceDescriptor service) { FileDescriptor file = service.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(service)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(service))); } /** Gets the location of a method, if available. */ @Nullable - public Location getLocation(MethodDescriptor method) { + public SourceCodeInfoLocation getLocation(MethodDescriptor method) { FileDescriptor file = method.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(method)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(method))); } /** Gets the location of an enum type, if available. */ @Nullable - public Location getLocation(EnumDescriptor enumType) { + public SourceCodeInfoLocation getLocation(EnumDescriptor enumType) { FileDescriptor file = enumType.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(enumType)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumType))); } /** Gets the location of an enum value, if available. */ @Nullable - public Location getLocation(EnumValueDescriptor enumValue) { + public SourceCodeInfoLocation getLocation(EnumValueDescriptor enumValue) { FileDescriptor file = enumValue.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(enumValue)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumValue))); } /** Gets the location of a oneof, if available. */ @Nullable - public Location getLocation(OneofDescriptor oneof) { + public SourceCodeInfoLocation getLocation(OneofDescriptor oneof) { FileDescriptor file = oneof.getFile(); if (!file.toProto().hasSourceCodeInfo()) { return null; } - return getLocation(file, buildPath(oneof)); + return SourceCodeInfoLocation.create(getLocation(file, buildPath(oneof))); } // ----------------------------------------------------------------------------- diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java index 06d0d0c753..b8daabefeb 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java +++ b/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java @@ -17,9 +17,9 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; +import com.google.api.generator.gapic.model.SourceCodeInfoLocation; import com.google.protobuf.DescriptorProtos.FileDescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; -import com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; @@ -46,48 +46,47 @@ public void setUp() throws Exception { @Test public void getServiceInfo() { - Location location = parser.getLocation(protoFile.findServiceByName("FooService")); + SourceCodeInfoLocation location = parser.getLocation(protoFile.findServiceByName("FooService")); assertEquals( - " This is a service description.\n It takes up multiple lines, like so.\n", + "This is a service description. It takes up multiple lines, like so.", location.getLeadingComments()); location = parser.getLocation(protoFile.findServiceByName("BarService")); - assertEquals(" This is another service description.\n", location.getLeadingComments()); + assertEquals("This is another service description.", location.getLeadingComments()); } @Test public void getMethodInfo() { ServiceDescriptor service = protoFile.findServiceByName("FooService"); - Location location = parser.getLocation(service.findMethodByName("FooMethod")); + SourceCodeInfoLocation location = parser.getLocation(service.findMethodByName("FooMethod")); assertEquals( - " FooMethod does something.\n This comment also takes up multiple lines.\n", + "FooMethod does something. This comment also takes up multiple lines.", location.getLeadingComments()); service = protoFile.findServiceByName("BarService"); location = parser.getLocation(service.findMethodByName("BarMethod")); - assertEquals(" BarMethod does another thing.\n", location.getLeadingComments()); + assertEquals("BarMethod does another thing.", location.getLeadingComments()); } @Test public void getOuterMessageInfo() { Descriptor message = protoFile.findMessageTypeByName("FooMessage"); - Location location = parser.getLocation(message); + SourceCodeInfoLocation location = parser.getLocation(message); assertEquals( - " This is a message descxription.\n" - + " Lorum ipsum dolor sit amet consectetur adipiscing elit.\n", + "This is a message descxription. Lorum ipsum dolor sit amet consectetur adipiscing elit.", location.getLeadingComments()); // Fields. location = parser.getLocation(message.findFieldByName("field_one")); assertEquals( - " This is a field description for field_one.\n" - + " And here is the second line of that description.\n", + "This is a field description for field_one. And here is the second line of that" + + " description.", location.getLeadingComments()); - assertEquals(" A field trailing comment.\n", location.getTrailingComments()); + assertEquals("A field trailing comment.", location.getTrailingComments()); location = parser.getLocation(message.findFieldByName("field_two")); - assertEquals(" This is another field description.\n", location.getLeadingComments()); - assertEquals(" Another field trailing comment.\n", location.getTrailingComments()); + assertEquals("This is another field description.", location.getLeadingComments()); + assertEquals("Another field trailing comment.", location.getTrailingComments()); } @Test @@ -96,52 +95,52 @@ public void getInnerMessageInfo() { assertThat(message).isNotNull(); message = message.findNestedTypeByName("BarMessage"); - Location location = parser.getLocation(message); + SourceCodeInfoLocation location = parser.getLocation(message); assertEquals( - " This is an inner message description for BarMessage.\n", location.getLeadingComments()); + "This is an inner message description for BarMessage.", location.getLeadingComments()); // Fields. location = parser.getLocation(message.findFieldByName("field_three")); - assertEquals(" A third leading comment for field_three.\n", location.getLeadingComments()); + assertEquals("A third leading comment for field_three.", location.getLeadingComments()); location = parser.getLocation(message.findFieldByName("field_two")); - assertEquals("\n This is a block comment for field_two.\n", location.getLeadingComments()); + assertEquals("This is a block comment for field_two.", location.getLeadingComments()); } @Test public void getOuterEnumInfo() { EnumDescriptor protoEnum = protoFile.findEnumTypeByName("OuterEnum"); - Location location = parser.getLocation(protoEnum); - assertEquals(" This is an outer enum.\n", location.getLeadingComments()); + SourceCodeInfoLocation location = parser.getLocation(protoEnum); + assertEquals("This is an outer enum.", location.getLeadingComments()); // Enum fields. location = parser.getLocation(protoEnum.findValueByName("VALUE_UNSPECIFIED")); - assertEquals(" Another unspecified value.\n", location.getLeadingComments()); + assertEquals("Another unspecified value.", location.getLeadingComments()); } @Test public void getInnerEnumInfo() { Descriptor message = protoFile.findMessageTypeByName("FooMessage"); EnumDescriptor protoEnum = message.findEnumTypeByName("FoodEnum"); - Location location = parser.getLocation(protoEnum); - assertEquals(" An inner enum.\n", location.getLeadingComments()); + SourceCodeInfoLocation location = parser.getLocation(protoEnum); + assertEquals("An inner enum.", location.getLeadingComments()); // Enum fields. location = parser.getLocation(protoEnum.findValueByName("RICE")); - assertEquals(" 😋 🍚.\n", location.getLeadingComments()); + assertEquals("😋 🍚.", location.getLeadingComments()); location = parser.getLocation(protoEnum.findValueByName("CHOCOLATE")); - assertEquals(" 🤤 🍫.\n", location.getLeadingComments()); + assertEquals("🤤 🍫.", location.getLeadingComments()); } @Test public void getOnoeofInfo() { Descriptor message = protoFile.findMessageTypeByName("FooMessage"); OneofDescriptor protoOneof = message.getOneofs().get(0); - Location location = parser.getLocation(protoOneof); - assertEquals(" An inner oneof.\n", location.getLeadingComments()); + SourceCodeInfoLocation location = parser.getLocation(protoOneof); + assertEquals("An inner oneof.", location.getLeadingComments()); location = parser.getLocation(protoOneof.getField(0)); - assertEquals(" An InnerOneof comment for its field.\n", location.getLeadingComments()); + assertEquals("An InnerOneof comment for its field.", location.getLeadingComments()); } /** From a59fd3f354bca8845fac53dcbf99a3bbffd883fc Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Wed, 16 Sep 2020 13:22:12 -0700 Subject: [PATCH 06/22] feat: add comment parsing to methods and fields --- .../api/generator/gapic/model/Field.java | 9 ++ .../api/generator/gapic/model/Method.java | 9 +- .../generator/gapic/model/MethodArgument.java | 10 ++ .../generator/gapic/model/ResourceName.java | 10 ++ .../protoparser/MethodSignatureParser.java | 42 +++++--- .../generator/gapic/protoparser/Parser.java | 41 ++++++- .../protoparser/ResourceReferenceParser.java | 5 + .../ServiceClientClassComposerTest.java | 102 ++++++++++++++++++ .../ResourceReferenceParserTest.java | 5 + 9 files changed, 213 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/model/Field.java b/src/main/java/com/google/api/generator/gapic/model/Field.java index 8f6d10b2af..57d836979e 100644 --- a/src/main/java/com/google/api/generator/gapic/model/Field.java +++ b/src/main/java/com/google/api/generator/gapic/model/Field.java @@ -31,6 +31,13 @@ public abstract class Field { @Nullable public abstract ResourceReference resourceReference(); + @Nullable + public abstract String description(); + + public boolean hasDescription() { + return description() != null; + } + public boolean hasResourceReference() { return type().equals(TypeNode.STRING) && resourceReference() != null; } @@ -51,6 +58,8 @@ public abstract static class Builder { public abstract Builder setResourceReference(ResourceReference resourceReference); + public abstract Builder setDescription(String description); + public abstract Field build(); } } diff --git a/src/main/java/com/google/api/generator/gapic/model/Method.java b/src/main/java/com/google/api/generator/gapic/model/Method.java index eb77f15a1c..30e94c9ad5 100644 --- a/src/main/java/com/google/api/generator/gapic/model/Method.java +++ b/src/main/java/com/google/api/generator/gapic/model/Method.java @@ -42,6 +42,9 @@ public enum Stream { @Nullable public abstract LongrunningOperation lro(); + @Nullable + public abstract String description(); + // Example from Expand in echo.proto: Thet TypeNodes that map to // [["content", "error"], ["content", "error", "info"]]. public abstract ImmutableList> methodSignatures(); @@ -50,7 +53,9 @@ public boolean hasLro() { return lro() != null; } - // TODO(miraleung): Parse annotations, comments. + public boolean hasDescription() { + return description() != null; + } public static Builder builder() { return new AutoValue_Method.Builder() @@ -84,6 +89,8 @@ public abstract static class Builder { public abstract Builder setLro(LongrunningOperation lro); + public abstract Builder setDescription(String description); + public abstract Builder setMethodSignatures(List> methodSignature); public abstract Builder setIsPaged(boolean isPaged); diff --git a/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java b/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java index 1a27957f17..33ecfb2cdc 100644 --- a/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java +++ b/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.util.List; +import javax.annotation.Nullable; @AutoValue public abstract class MethodArgument { @@ -32,6 +33,13 @@ public abstract class MethodArgument { // Returns true if this is a resource name helper tyep. public abstract boolean isResourceNameHelper(); + @Nullable + public abstract String description(); + + public boolean hasDescription() { + return description() != null; + } + public static Builder builder() { return new AutoValue_MethodArgument.Builder() .setNestedTypes(ImmutableList.of()) @@ -48,6 +56,8 @@ public abstract static class Builder { public abstract Builder setIsResourceNameHelper(boolean isResourceNameHelper); + public abstract Builder setDescription(String description); + public abstract MethodArgument build(); } } diff --git a/src/main/java/com/google/api/generator/gapic/model/ResourceName.java b/src/main/java/com/google/api/generator/gapic/model/ResourceName.java index 45e8e1c814..14770b7366 100644 --- a/src/main/java/com/google/api/generator/gapic/model/ResourceName.java +++ b/src/main/java/com/google/api/generator/gapic/model/ResourceName.java @@ -54,10 +54,17 @@ public abstract class ResourceName { @Nullable public abstract String parentMessageName(); + @Nullable + public abstract String description(); + public boolean hasParentMessageName() { return parentMessageName() != null; } + public boolean hasDescription() { + return description() != null; + } + public String resourceTypeName() { return resourceTypeString().substring(resourceTypeString().indexOf(SLASH) + 1); } @@ -92,6 +99,7 @@ public boolean equals(Object o) { } ResourceName other = (ResourceName) o; + // Exclude the description from the resource name because it's just a comment. return variableName().equals(other.variableName()) && pakkage().equals(other.pakkage()) && resourceTypeString().equals(other.resourceTypeString()) @@ -125,6 +133,8 @@ public abstract static class Builder { public abstract Builder setParentMessageName(String parentMessageName); + public abstract Builder setDescription(String description); + // Private setters. abstract Builder setType(TypeNode type); diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/MethodSignatureParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/MethodSignatureParser.java index d7d20e5265..baef34d268 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/MethodSignatureParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/MethodSignatureParser.java @@ -25,7 +25,6 @@ import com.google.common.collect.Lists; import com.google.protobuf.Descriptors.MethodDescriptor; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,8 +67,8 @@ public static List> parseMethodSignatures( // For resource names, this will be empty. List argumentTypePathAcc = new ArrayList<>(); // There should be more than one type returned only when we encounter a reousrce name. - List argumentTypes = - parseTypeFromArgumentName( + Map argumentTypes = + parseTypeAndCommentFromArgumentName( argumentName, servicePackage, inputMessage, @@ -84,14 +83,15 @@ public static List> parseMethodSignatures( argumentNames.add(actualArgumentName); argumentNameToOverloads.put( actualArgumentName, - argumentTypes.stream() + argumentTypes.entrySet().stream() .map( - type -> + e -> MethodArgument.builder() .setName(actualArgumentName) - .setType(type) + .setDescription(e.getValue()) // May be null. + .setType(e.getKey()) .setIsResourceNameHelper( - argumentTypes.size() > 1 && !type.equals(TypeNode.STRING)) + argumentTypes.size() > 1 && !e.getKey().equals(TypeNode.STRING)) .setNestedTypes(argumentTypePathAcc) .build()) .collect(Collectors.toList())); @@ -143,7 +143,7 @@ private static List> flattenMethodSignatureVariants( return methodArgs; } - private static List parseTypeFromArgumentName( + private static Map parseTypeAndCommentFromArgumentName( String argumentName, String servicePackage, Message inputMessage, @@ -153,6 +153,8 @@ private static List parseTypeFromArgumentName( List argumentTypePathAcc, Set outputArgResourceNames) { + // Comment values may be null. + Map typeToComment = new HashMap<>(); int dotIndex = argumentName.indexOf(DOT); if (dotIndex < 1) { Field field = inputMessage.fieldMap().get(argumentName); @@ -162,19 +164,27 @@ private static List parseTypeFromArgumentName( "Field %s not found from input message %s values %s", argumentName, inputMessage.name(), inputMessage.fieldMap().keySet())); if (!field.hasResourceReference()) { - return Arrays.asList(field.type()); + typeToComment.put(field.type(), field.description()); + return typeToComment; } // Parse the resource name tyeps. List resourceNameArgs = ResourceReferenceParser.parseResourceNames( - field.resourceReference(), servicePackage, resourceNames, patternsToResourceNames); + field.resourceReference(), + servicePackage, + field.description(), + resourceNames, + patternsToResourceNames); outputArgResourceNames.addAll(resourceNameArgs); - List allFieldTypes = new ArrayList<>(); - allFieldTypes.add(TypeNode.STRING); - allFieldTypes.addAll( - resourceNameArgs.stream().map(r -> r.type()).collect(Collectors.toList())); - return allFieldTypes; + typeToComment.put( + TypeNode.STRING, + resourceNameArgs.isEmpty() ? null : resourceNameArgs.get(0).description()); + typeToComment.putAll( + resourceNameArgs.stream() + .collect( + Collectors.toMap(r -> r.type(), r -> r.hasDescription() ? r.description() : ""))); + return typeToComment; } Preconditions.checkState( @@ -204,7 +214,7 @@ private static List parseTypeFromArgumentName( "Message type %s for field reference %s invalid", firstFieldTypeName, firstFieldName)); argumentTypePathAcc.add(firstFieldType); - return parseTypeFromArgumentName( + return parseTypeAndCommentFromArgumentName( remainingArgumentName, servicePackage, firstFieldMessage, diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index 275ec8d5cb..c8485ad4f2 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java @@ -29,6 +29,7 @@ import com.google.api.generator.gapic.model.ResourceName; import com.google.api.generator.gapic.model.ResourceReference; import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.gapic.model.SourceCodeInfoLocation; import com.google.api.generator.gapic.utils.ResourceNameConstants; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -55,6 +56,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -64,6 +66,9 @@ public class Parser { private static final String COLON = ":"; private static final String DEFAULT_PORT = "443"; + // Allow other parsers to access this. + protected static final SourceCodeInfoParser SOURCE_CODE_INFO_PARSER = new SourceCodeInfoParser(); + static class GapicParserException extends RuntimeException { public GapicParserException(String errorMessage) { super(errorMessage); @@ -147,7 +152,17 @@ public static List parseService( List oauthScopes = Arrays.asList(serviceOptions.getExtension(ClientProto.oauthScopes).split(COMMA)); - return Service.builder() + Service.Builder serviceBuilder = Service.builder(); + if (fileDescriptor.toProto().hasSourceCodeInfo()) { + SourceCodeInfoLocation protoServiceLocation = + SOURCE_CODE_INFO_PARSER.getLocation(s); + if (!Objects.isNull(protoServiceLocation) + && !Strings.isNullOrEmpty(protoServiceLocation.getLeadingComments())) { + serviceBuilder.setDescription(protoServiceLocation.getLeadingComments()); + } + } + + return serviceBuilder .setName(s.getName()) .setDefaultHost(defaultHost) .setOauthScopes(oauthScopes) @@ -244,8 +259,18 @@ static List parseMethods( for (MethodDescriptor protoMethod : serviceDescriptor.getMethods()) { // Parse the method. TypeNode inputType = TypeParser.parseType(protoMethod.getInputType()); + Method.Builder methodBuilder = Method.builder(); + if (protoMethod.getFile().toProto().hasSourceCodeInfo()) { + SourceCodeInfoLocation protoMethodLocation = + SOURCE_CODE_INFO_PARSER.getLocation(protoMethod); + if (!Objects.isNull(protoMethodLocation) + && !Strings.isNullOrEmpty(protoMethodLocation.getLeadingComments())) { + methodBuilder.setDescription(protoMethodLocation.getLeadingComments()); + } + } + methods.add( - Method.builder() + methodBuilder .setName(protoMethod.getName()) .setInputType(inputType) .setOutputType(TypeParser.parseType(protoMethod.getOutputType())) @@ -358,7 +383,17 @@ private static Field parseField(FieldDescriptor fieldDescriptor, Descriptor mess } } - return Field.builder() + Field.Builder fieldBuilder = Field.builder(); + if (fieldDescriptor.getFile().toProto().hasSourceCodeInfo()) { + SourceCodeInfoLocation protoFieldLocation = + SOURCE_CODE_INFO_PARSER.getLocation(fieldDescriptor); + if (!Objects.isNull(protoFieldLocation) + && !Strings.isNullOrEmpty(protoFieldLocation.getLeadingComments())) { + fieldBuilder.setDescription(protoFieldLocation.getLeadingComments()); + } + } + + return fieldBuilder .setName(fieldDescriptor.getName()) .setType(TypeParser.parseType(fieldDescriptor)) .setIsRepeated(fieldDescriptor.isRepeated()) diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java index f81d198260..a8731deea2 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; public class ResourceReferenceParser { private static final String EMPTY_STRING = ""; @@ -39,6 +40,7 @@ public class ResourceReferenceParser { public static List parseResourceNames( ResourceReference resourceReference, String servicePackage, + @Nullable String description, Map resourceNames, Map patternsToResourceNames) { ResourceName resourceName = resourceNames.get(resourceReference.resourceTypeString()); @@ -62,6 +64,7 @@ public static List parseResourceNames( servicePackage, resourceName.pakkage(), resourceName.resourceTypeString(), + description, patternsToResourceNames); // Prevent duplicates. if (parentResourceNameOpt.isPresent() @@ -80,6 +83,7 @@ static Optional parseParentResourceName( String servicePackage, String resourcePackage, String resourceTypeString, + @Nullable String description, Map patternsToResourceNames) { Optional parentPatternOpt = parseParentPattern(pattern); if (!parentPatternOpt.isPresent()) { @@ -143,6 +147,7 @@ static Optional parseParentResourceName( .setPakkage(pakkage) .setResourceTypeString(parentResourceTypeString) .setPatterns(Arrays.asList(parentPattern)) + .setDescription(description) .build(); patternsToResourceNames.put(parentPattern, parentResourceName); diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index 27e33bfa2e..5a0a3f0eb1 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -203,27 +203,63 @@ public void generateServiceClasses() { + " return operationsClient;\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param content\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String content) {\n" + " EchoRequest request = EchoRequest.newBuilder().setContent(content).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param error\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(Status error) {\n" + " EchoRequest request = EchoRequest.newBuilder().setError(error).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param content\n" + + " * @param severity\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String content, Severity severity) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder().setContent(content).setSeverity(severity).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param name\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String name) {\n" + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param name\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(FoobarName name) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -232,11 +268,25 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param parent\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String parent) {\n" + " EchoRequest request = EchoRequest.newBuilder().setParent(parent).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param parent\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(ResourceName parent) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -245,65 +295,117 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(EchoRequest request) {\n" + " return echoCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable echoCallable() {\n" + " return stub.echoCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final ServerStreamingCallable expandCallable()" + " {\n" + " return stub.expandCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final ClientStreamingCallable collectCallable()" + " {\n" + " return stub.collectCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final BidiStreamingCallable chatCallable() {\n" + " return stub.chatCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final BidiStreamingCallable chatAgainCallable()" + " {\n" + " return stub.chatAgainCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final PagedExpandResponse pagedExpand(PagedExpandRequest request) {\n" + " return pagedExpandCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable\n" + " pagedExpandPagedCallable() {\n" + " return stub.pagedExpandPagedCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable" + " pagedExpandCallable() {\n" + " return stub.pagedExpandCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final OperationFuture waitAsync(WaitRequest" + " request) {\n" + " return waitOperationCallable().futureCall(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final OperationCallable" + " waitOperationCallable() {\n" + " return stub.waitOperationCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable waitCallable() {\n" + " return stub.waitCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final BlockResponse block(BlockRequest request) {\n" + " return blockCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable blockCallable() {\n" + " return stub.blockCallable();\n" + " }\n" diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParserTest.java index 990e7dc4a7..c2cae61f88 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParserTest.java +++ b/src/test/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParserTest.java @@ -48,6 +48,7 @@ public void setUp() { public void parseParentResourceName_createFromPattern() { String resourceNamePackage = String.format("%s.common", MAIN_PACKAGE); String domainName = "cloudbilling.googleapis.com"; + String description = "This is the resource name description"; String resourceTypeString = String.format("%s/BillingAccount", domainName); String parentResourceTypeString = String.format("%s/Project", domainName); Map patternsToResourceNames = new HashMap<>(); @@ -59,6 +60,7 @@ public void parseParentResourceName_createFromPattern() { MAIN_PACKAGE, resourceNamePackage, resourceTypeString, + description, patternsToResourceNames); assertTrue(parentResourceNameOpt.isPresent()); @@ -67,6 +69,7 @@ public void parseParentResourceName_createFromPattern() { assertEquals(Arrays.asList(parentPattern), parentResourceName.patterns()); assertEquals(parentResourceTypeString, parentResourceName.resourceTypeString()); assertEquals(resourceNamePackage, parentResourceName.pakkage()); + assertEquals(description, parentResourceName.description()); assertEquals( TypeNode.withReference( VaporReference.builder() @@ -93,6 +96,7 @@ public void parseParentResourceName_parentResourceNameExists() { ResourceReferenceParser.parseParentResourceName( "projects/{project}/folders/{folder}/documents/{document}", MAIN_PACKAGE, + null, MAIN_PACKAGE, "cloudresourcemanager.googleapis.com/Document", patternsToResourceNames); @@ -109,6 +113,7 @@ public void parseParentResourceName_badPattern() { ResourceReferenceParser.parseParentResourceName( "projects/{project}/billingAccounts", MAIN_PACKAGE, + null, MAIN_PACKAGE, "cloudbilling.googleapis.com/Feature", new HashMap()); From 66dec11135abf9c0f4bff1089c17178dc6d83b94 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Wed, 16 Sep 2020 13:25:13 -0700 Subject: [PATCH 07/22] fix: test --- .../ServiceClientClassComposerTest.java | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index 5a0a3f0eb1..27e33bfa2e 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -203,63 +203,27 @@ public void generateServiceClasses() { + " return operationsClient;\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param content\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(String content) {\n" + " EchoRequest request = EchoRequest.newBuilder().setContent(content).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param error\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(Status error) {\n" + " EchoRequest request = EchoRequest.newBuilder().setError(error).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param content\n" - + " * @param severity\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(String content, Severity severity) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder().setContent(content).setSeverity(severity).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param name\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(String name) {\n" + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param name\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(FoobarName name) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -268,25 +232,11 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param parent\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(String parent) {\n" + " EchoRequest request = EchoRequest.newBuilder().setParent(parent).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param parent\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(ResourceName parent) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -295,117 +245,65 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param request The request object containing all of the parameters for the API" - + " call.\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final EchoResponse echo(EchoRequest request) {\n" + " return echoCallable().call(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final UnaryCallable echoCallable() {\n" + " return stub.echoCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final ServerStreamingCallable expandCallable()" + " {\n" + " return stub.expandCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final ClientStreamingCallable collectCallable()" + " {\n" + " return stub.collectCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final BidiStreamingCallable chatCallable() {\n" + " return stub.chatCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final BidiStreamingCallable chatAgainCallable()" + " {\n" + " return stub.chatAgainCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param request The request object containing all of the parameters for the API" - + " call.\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final PagedExpandResponse pagedExpand(PagedExpandRequest request) {\n" + " return pagedExpandCallable().call(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final UnaryCallable\n" + " pagedExpandPagedCallable() {\n" + " return stub.pagedExpandPagedCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final UnaryCallable" + " pagedExpandCallable() {\n" + " return stub.pagedExpandCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param request The request object containing all of the parameters for the API" - + " call.\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final OperationFuture waitAsync(WaitRequest" + " request) {\n" + " return waitOperationCallable().futureCall(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final OperationCallable" + " waitOperationCallable() {\n" + " return stub.waitOperationCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final UnaryCallable waitCallable() {\n" + " return stub.waitCallable();\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /**\n" - + " * Sample code:\n" - + " *\n" - + " * @param request The request object containing all of the parameters for the API" - + " call.\n" - + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" - + " */\n" + " public final BlockResponse block(BlockRequest request) {\n" + " return blockCallable().call(request);\n" + " }\n" + "\n" - + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" - + " /** Sample code: */\n" + " public final UnaryCallable blockCallable() {\n" + " return stub.blockCallable();\n" + " }\n" From 83254508b44778f886888dcb70d0207347812b11 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Wed, 16 Sep 2020 13:26:22 -0700 Subject: [PATCH 08/22] feat: add protobuf comments to ServiceClient --- .../composer/ServiceClientClassComposer.java | 9 ++ .../ServiceClientCommentComposer.java | 55 ++++++++++ .../ServiceClientClassComposerTest.java | 102 ++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java index cf0f2367bb..84064723b9 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java @@ -565,6 +565,8 @@ private static List createMethodVariants( javaMethods.add( MethodDefinition.builder() + .setHeaderCommentStatements( + ServiceClientCommentComposer.createRpcMethodHeaderComment(method, signature)) .setScope(ScopeNode.PUBLIC) .setIsFinal(true) .setReturnType(methodOutputType) @@ -595,6 +597,8 @@ private static List createMethodVariants( .build(); javaMethods.add( MethodDefinition.builder() + .setHeaderCommentStatements( + ServiceClientCommentComposer.createRpcMethodHeaderComment(method)) .setScope(ScopeNode.PUBLIC) .setIsFinal(true) .setReturnType(methodOutputType) @@ -608,6 +612,7 @@ private static List createMethodVariants( private static MethodDefinition createLroAsyncMethod( String serviceName, Method method, Map types) { + // TODO(miraleung): Create variants as well. Preconditions.checkState( method.hasLro(), String.format("Method %s does not have an LRO", method.name())); String methodName = JavaStyle.toLowerCamelCase(method.name()); @@ -642,6 +647,8 @@ private static MethodDefinition createLroAsyncMethod( .build(); return MethodDefinition.builder() + .setHeaderCommentStatements( + ServiceClientCommentComposer.createRpcMethodHeaderComment(method)) .setScope(ScopeNode.PUBLIC) .setIsFinal(true) .setReturnType(returnType) @@ -713,6 +720,8 @@ private static MethodDefinition createCallableMethod( .build(); return MethodDefinition.builder() + .setHeaderCommentStatements( + ServiceClientCommentComposer.createRpcCallableMethodHeaderComment(method)) .setScope(ScopeNode.PUBLIC) .setIsFinal(true) .setName(methodName) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java index 46b7d61bbe..d79bf7f03e 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientCommentComposer.java @@ -17,14 +17,20 @@ import com.google.api.generator.engine.ast.CommentStatement; import com.google.api.generator.engine.ast.JavaDocComment; import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.gapic.model.Method; +import com.google.api.generator.gapic.model.MethodArgument; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.gapic.utils.JavaStyle; import java.util.Arrays; +import java.util.Collections; import java.util.List; class ServiceClientCommentComposer { // Tokens. private static final String COLON = ":"; + private static final String EMPTY_STRING = ""; + private static final String API_EXCEPTION_TYPE_NAME = "com.google.api.gax.rpc.ApiException"; + private static final String EXCEPTION_CONDITION = "if the remote call fails"; // Constants. private static final String SERVICE_DESCRIPTION_INTRO_STRING = @@ -49,6 +55,8 @@ class ServiceClientCommentComposer { private static final String SERVICE_DESCRIPTION_ENDPOINT_SUMMARY_STRING = "To customize the endpoint:"; + private static final String METHOD_DESCRIPTION_SAMPLE_CODE_SUMMARY_STRING = "Sample code:"; + private static final List SERVICE_DESCRIPTION_SURFACE_DESCRIPTION = Arrays.asList( "A \"flattened\" method. With this type of method, the fields of the request type have" @@ -133,6 +141,53 @@ static CommentStatement createCreateMethodStubArgComment(TypeNode settingsType) String.format(CREATE_METHOD_STUB_ARG_PATTERN, settingsType.reference().name())); } + static List createRpcMethodHeaderComment( + Method method, List methodArguments) { + JavaDocComment.Builder methodJavadocBuilder = JavaDocComment.builder(); + + if (method.hasDescription()) { + methodJavadocBuilder.addComment(method.description()); + } + + methodJavadocBuilder.addParagraph(METHOD_DESCRIPTION_SAMPLE_CODE_SUMMARY_STRING); + // TODO(summerji): Add sample code here. + + if (methodArguments.isEmpty()) { + methodJavadocBuilder.addParam( + "request", "The request object containing all of the parameters for the API call."); + } else { + for (MethodArgument argument : methodArguments) { + methodJavadocBuilder.addParam( + argument.name(), argument.hasDescription() ? argument.description() : EMPTY_STRING); + } + } + + methodJavadocBuilder.setThrows(API_EXCEPTION_TYPE_NAME, EXCEPTION_CONDITION); + + return Arrays.asList( + CommentComposer.AUTO_GENERATED_METHOD_COMMENT, + CommentStatement.withComment(methodJavadocBuilder.build())); + } + + static List createRpcMethodHeaderComment(Method method) { + return createRpcMethodHeaderComment(method, Collections.emptyList()); + } + + static List createRpcCallableMethodHeaderComment(Method method) { + JavaDocComment.Builder methodJavadocBuilder = JavaDocComment.builder(); + + if (method.hasDescription()) { + methodJavadocBuilder.addComment(method.description()); + } + + methodJavadocBuilder.addParagraph(METHOD_DESCRIPTION_SAMPLE_CODE_SUMMARY_STRING); + // TODO(summerji): Add sample code here. + + return Arrays.asList( + CommentComposer.AUTO_GENERATED_METHOD_COMMENT, + CommentStatement.withComment(methodJavadocBuilder.build())); + } + private static CommentStatement toSimpleComment(String comment) { return CommentStatement.withComment(JavaDocComment.withComment(comment)); } diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index 27e33bfa2e..5a0a3f0eb1 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -203,27 +203,63 @@ public void generateServiceClasses() { + " return operationsClient;\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param content\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String content) {\n" + " EchoRequest request = EchoRequest.newBuilder().setContent(content).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param error\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(Status error) {\n" + " EchoRequest request = EchoRequest.newBuilder().setError(error).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param content\n" + + " * @param severity\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String content, Severity severity) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder().setContent(content).setSeverity(severity).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param name\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String name) {\n" + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param name\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(FoobarName name) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -232,11 +268,25 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param parent\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(String parent) {\n" + " EchoRequest request = EchoRequest.newBuilder().setParent(parent).build();\n" + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param parent\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(ResourceName parent) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" @@ -245,65 +295,117 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final EchoResponse echo(EchoRequest request) {\n" + " return echoCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable echoCallable() {\n" + " return stub.echoCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final ServerStreamingCallable expandCallable()" + " {\n" + " return stub.expandCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final ClientStreamingCallable collectCallable()" + " {\n" + " return stub.collectCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final BidiStreamingCallable chatCallable() {\n" + " return stub.chatCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final BidiStreamingCallable chatAgainCallable()" + " {\n" + " return stub.chatAgainCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final PagedExpandResponse pagedExpand(PagedExpandRequest request) {\n" + " return pagedExpandCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable\n" + " pagedExpandPagedCallable() {\n" + " return stub.pagedExpandPagedCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable" + " pagedExpandCallable() {\n" + " return stub.pagedExpandCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final OperationFuture waitAsync(WaitRequest" + " request) {\n" + " return waitOperationCallable().futureCall(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final OperationCallable" + " waitOperationCallable() {\n" + " return stub.waitOperationCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable waitCallable() {\n" + " return stub.waitCallable();\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /**\n" + + " * Sample code:\n" + + " *\n" + + " * @param request The request object containing all of the parameters for the API" + + " call.\n" + + " * @throws com.google.api.gax.rpc.ApiException if the remote call fails\n" + + " */\n" + " public final BlockResponse block(BlockRequest request) {\n" + " return blockCallable().call(request);\n" + " }\n" + "\n" + + " // AUTO-GENERATED DOCUMENTATION AND METHOD.\n" + + " /** Sample code: */\n" + " public final UnaryCallable blockCallable() {\n" + " return stub.blockCallable();\n" + " }\n" From 5acc151d8fb0253a7224e16fe889f57479cc60b5 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Wed, 16 Sep 2020 16:45:49 -0700 Subject: [PATCH 09/22] fix: solidify codegen method order with TypeNode/MethodArg and Comparable --- .../api/generator/engine/ast/TypeNode.java | 34 ++++++++++++- .../composer/ServiceClientClassComposer.java | 21 +++++++- .../generator/gapic/model/MethodArgument.java | 11 +++- .../generator/engine/ast/TypeNodeTest.java | 30 +++++++++++ .../ServiceClientClassComposerTest.java | 30 +++++------ .../api/generator/gapic/model/BUILD.bazel | 2 + .../gapic/model/MethodArgumentTest.java | 51 +++++++++++++++++++ 7 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 src/test/java/com/google/api/generator/gapic/model/MethodArgumentTest.java diff --git a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java index adc16f1f26..d16c8801c7 100644 --- a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java +++ b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java @@ -23,7 +23,7 @@ import javax.annotation.Nullable; @AutoValue -public abstract class TypeNode implements AstNode { +public abstract class TypeNode implements AstNode, Comparable { static final Reference EXCEPTION_REFERENCE = ConcreteReference.withClazz(Exception.class); public static final Reference WILDCARD_REFERENCE = ConcreteReference.wildcard(); @@ -88,6 +88,38 @@ public enum TypeKind { @Nullable public abstract Reference reference(); + @Override + public int compareTo(TypeNode other) { + // Ascending order of name. + if (isPrimitiveType()) { + if (other.isPrimitiveType()) { + return typeKind().name().compareTo(other.typeKind().name()); + } + // b is a reference type or null, so a < b. + return -1; + } + + if (this.equals(TypeNode.NULL)) { + // Can't self-compare, so we don't need to check whether the other one is TypeNode.NULL. + return other.isPrimitiveType() ? 1 : -1; + } + + if (other.isPrimitiveType() || other.equals(TypeNode.NULL)) { + return 1; + } + + // Both are reference types. + // TODO(miraleung): Replace this with a proper reference Comaparator. + System.out.println( + String.format( + "DEL: %s comapre to %s: %d", + reference().fullName(), + other.reference().fullName(), + reference().fullName().compareTo(other.reference().fullName()))); + + return reference().fullName().compareTo(other.reference().fullName()); + } + public static Builder builder() { return new AutoValue_TypeNode.Builder().setIsArray(false); } diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java index cf0f2367bb..4c7ed0987f 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java @@ -473,7 +473,26 @@ private static List createMethodVariants( Preconditions.checkNotNull( inputMessage, String.format("Message %s not found", methodInputTypeName)); - for (List signature : method.methodSignatures()) { + // Make the method signature order deterministic, which helps with unit testing and per-version + // diffs. + List> sortedMethodSignatures = + method.methodSignatures().stream() + .sorted( + (s1, s2) -> { + if (s1.size() != s2.size()) { + return s1.size() - s2.size(); + } + for (int i = 0; i < s1.size(); i++) { + int compareVal = s1.get(i).compareTo(s2.get(i)); + if (compareVal != 0) { + return compareVal; + } + } + return 0; + }) + .collect(Collectors.toList()); + + for (List signature : sortedMethodSignatures) { // Get the argument list. List arguments = signature.stream() diff --git a/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java b/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java index 1a27957f17..685f7b4685 100644 --- a/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java +++ b/src/main/java/com/google/api/generator/gapic/model/MethodArgument.java @@ -20,7 +20,7 @@ import java.util.List; @AutoValue -public abstract class MethodArgument { +public abstract class MethodArgument implements Comparable { public abstract String name(); public abstract TypeNode type(); @@ -32,6 +32,15 @@ public abstract class MethodArgument { // Returns true if this is a resource name helper tyep. public abstract boolean isResourceNameHelper(); + @Override + public int compareTo(MethodArgument other) { + int compareVal = type().compareTo(other.type()); + if (compareVal == 0) { + compareVal = name().compareTo(other.name()); + } + return compareVal; + } + public static Builder builder() { return new AutoValue_MethodArgument.Builder() .setNestedTypes(ImmutableList.of()) diff --git a/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java b/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java index 5e559db5a7..392ecc37f8 100644 --- a/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java +++ b/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java @@ -14,6 +14,7 @@ package com.google.api.generator.engine.ast; +import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertThrows; @@ -112,6 +113,35 @@ public void type_wildcardUpperBoundGenerics() { .build()); } + @Test + public void compareTypes() { + // Primitive and primitive. + // Can't compare objects to themselves, so this test is omitted. + assertThat(TypeNode.INT.compareTo(TypeNode.BOOLEAN)).isGreaterThan(0); + assertThat(TypeNode.BOOLEAN.compareTo(TypeNode.INT)).isLessThan(0); + + // Primitive and null. + assertThat(TypeNode.INT.compareTo(TypeNode.NULL)).isLessThan(0); + assertThat(TypeNode.NULL.compareTo(TypeNode.INT)).isGreaterThan(0); + + // Primitive and reference. + assertThat(TypeNode.INT.compareTo(TypeNode.INT_OBJECT)).isLessThan(0); + assertThat(TypeNode.INT.compareTo(TypeNode.STRING)).isLessThan(0); + assertThat(TypeNode.INT_OBJECT.compareTo(TypeNode.INT)).isGreaterThan(0); + + // Reference and null. + // No test for null against null because we can't compare objects to themselves. + assertThat(TypeNode.INT_OBJECT.compareTo(TypeNode.NULL)).isGreaterThan(0); + assertThat(TypeNode.NULL.compareTo(TypeNode.BOOLEAN_OBJECT)).isLessThan(0); + + // Reference and reference. Sorted alphabetically by package. + assertThat(TypeNode.BOOLEAN_OBJECT.compareTo(TypeNode.INT_OBJECT)).isLessThan(0); + assertThat( + TypeNode.BOOLEAN_OBJECT.compareTo( + TypeNode.withReference(ConcreteReference.withClazz(Arrays.class)))) + .isLessThan(0); + } + @Test public void invalidType_topLevelWildcard() { assertThrows( diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index 27e33bfa2e..ca3c8fd44c 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -203,8 +203,11 @@ public void generateServiceClasses() { + " return operationsClient;\n" + " }\n" + "\n" - + " public final EchoResponse echo(String content) {\n" - + " EchoRequest request = EchoRequest.newBuilder().setContent(content).build();\n" + + " public final EchoResponse echo(ResourceName parent) {\n" + + " EchoRequest request =\n" + + " EchoRequest.newBuilder()\n" + + " .setParent(Strings.isNullOrEmpty(parent) ? null : parent.toString())\n" + + " .build();\n" + " return echo(request);\n" + " }\n" + "\n" @@ -213,22 +216,21 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" - + " public final EchoResponse echo(String content, Severity severity) {\n" + + " public final EchoResponse echo(FoobarName name) {\n" + " EchoRequest request =\n" - + " EchoRequest.newBuilder().setContent(content).setSeverity(severity).build();\n" + + " EchoRequest.newBuilder()\n" + + " .setName(Strings.isNullOrEmpty(name) ? null : name.toString())\n" + + " .build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " public final EchoResponse echo(String name) {\n" - + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + + " public final EchoResponse echo(String content) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setContent(content).build();\n" + " return echo(request);\n" + " }\n" + "\n" - + " public final EchoResponse echo(FoobarName name) {\n" - + " EchoRequest request =\n" - + " EchoRequest.newBuilder()\n" - + " .setName(Strings.isNullOrEmpty(name) ? null : name.toString())\n" - + " .build();\n" + + " public final EchoResponse echo(String name) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + " return echo(request);\n" + " }\n" + "\n" @@ -237,11 +239,9 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" - + " public final EchoResponse echo(ResourceName parent) {\n" + + " public final EchoResponse echo(String content, Severity severity) {\n" + " EchoRequest request =\n" - + " EchoRequest.newBuilder()\n" - + " .setParent(Strings.isNullOrEmpty(parent) ? null : parent.toString())\n" - + " .build();\n" + + " EchoRequest.newBuilder().setContent(content).setSeverity(severity).build();\n" + " return echo(request);\n" + " }\n" + "\n" diff --git a/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel index 705c6b9cc7..6c919246db 100644 --- a/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel @@ -2,6 +2,7 @@ package(default_visibility = ["//visibility:public"]) TESTS = [ "GapicServiceConfigTest", + "MethodArgumentTest", "MethodTest", ] @@ -20,6 +21,7 @@ filegroup( deps = [ "//:service_config_java_proto", "//src/main/java/com/google/api/generator:autovalue", + "//src/main/java/com/google/api/generator/engine/ast", "//src/main/java/com/google/api/generator/gapic/model", "//src/main/java/com/google/api/generator/gapic/protoparser", "//src/test/java/com/google/api/generator/gapic/testdata:showcase_java_proto", diff --git a/src/test/java/com/google/api/generator/gapic/model/MethodArgumentTest.java b/src/test/java/com/google/api/generator/gapic/model/MethodArgumentTest.java new file mode 100644 index 0000000000..810758267a --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/model/MethodArgumentTest.java @@ -0,0 +1,51 @@ +// Copyright 2020 Google LLC +// +// 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 com.google.api.generator.gapic.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.generator.engine.ast.TypeNode; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.junit.Test; + +public class MethodArgumentTest { + @Test + public void compareMethodArguments() { + BiFunction methodArgFn = + (name, type) -> MethodArgument.builder().setName(name).setType(type).build(); + + // Cursory sanity-check of type-only differences, since these are already covered in the + // TypeNode test. + assertThat( + methodArgFn + .apply("foo", TypeNode.INT) + .compareTo(methodArgFn.apply("foo", TypeNode.BOOLEAN))) + .isGreaterThan(0); + assertThat( + methodArgFn + .apply("foo", TypeNode.INT) + .compareTo(methodArgFn.apply("foo", TypeNode.INT_OBJECT))) + .isLessThan(0); + + // Non-type-based differences. + Function simpleMethodArgFn = + (name) -> methodArgFn.apply(name, TypeNode.INT); + assertThat(simpleMethodArgFn.apply("foo").compareTo(simpleMethodArgFn.apply("bar"))) + .isGreaterThan(0); + assertThat(simpleMethodArgFn.apply("bar").compareTo(simpleMethodArgFn.apply("foo"))) + .isLessThan(0); + } +} From 97e780ab8b85acaf81c7bdb45dc2f5b04d5dea58 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Wed, 16 Sep 2020 16:48:28 -0700 Subject: [PATCH 10/22] fix: clean up tests --- .../java/com/google/api/generator/engine/ast/TypeNode.java | 7 ------- .../com/google/api/generator/engine/ast/TypeNodeTest.java | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java index d16c8801c7..26c9b658c5 100644 --- a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java +++ b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java @@ -110,13 +110,6 @@ public int compareTo(TypeNode other) { // Both are reference types. // TODO(miraleung): Replace this with a proper reference Comaparator. - System.out.println( - String.format( - "DEL: %s comapre to %s: %d", - reference().fullName(), - other.reference().fullName(), - reference().fullName().compareTo(other.reference().fullName()))); - return reference().fullName().compareTo(other.reference().fullName()); } diff --git a/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java b/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java index 392ecc37f8..982849447b 100644 --- a/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java +++ b/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java @@ -136,6 +136,7 @@ public void compareTypes() { // Reference and reference. Sorted alphabetically by package. assertThat(TypeNode.BOOLEAN_OBJECT.compareTo(TypeNode.INT_OBJECT)).isLessThan(0); + assertThat(TypeNode.BOOLEAN_OBJECT.compareTo(TypeNode.STRING)).isLessThan(0); assertThat( TypeNode.BOOLEAN_OBJECT.compareTo( TypeNode.withReference(ConcreteReference.withClazz(Arrays.class)))) From c6ca60ba5edd2bf1635648d5bd82a87c36f22390 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Thu, 17 Sep 2020 23:26:31 -0700 Subject: [PATCH 11/22] fix: ServiceClient member variables and method calls --- .../composer/ServiceClientClassComposer.java | 18 ++++++++++++------ .../ServiceClientClassComposerTest.java | 9 ++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java index 14a20fb5fd..d8c761f398 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceClientClassComposer.java @@ -68,6 +68,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.annotation.Generated; @@ -134,7 +135,7 @@ private static List createClassMethods( boolean hasLroClient) { List methods = new ArrayList<>(); methods.addAll(createStaticCreatorMethods(service, types)); - methods.addAll(createConstructorMethods(service, types)); + methods.addAll(createConstructorMethods(service, types, hasLroClient)); methods.addAll(createGetterMethods(service, types, hasLroClient)); methods.addAll(createServiceMethods(service, messageTypes, types)); methods.addAll(createBackgroundResourceMethods(service, types)); @@ -274,7 +275,7 @@ private static List createStaticCreatorMethods( } private static List createConstructorMethods( - Service service, Map types) { + Service service, Map types, boolean hasLroClient) { List methods = new ArrayList<>(); String thisClientName = String.format("%sClient", service.name()); String settingsName = String.format("%sSettings", service.name()); @@ -342,7 +343,9 @@ private static List createConstructorMethods( .setReturnType(operationsClientVarExpr.type()) .build()) .build(); - ctorAssignmentExprs.add(operationsClientAssignExpr); + if (hasLroClient) { + ctorAssignmentExprs.add(operationsClientAssignExpr); + } methods.add( MethodDefinition.constructorBuilder() @@ -370,7 +373,9 @@ private static List createConstructorMethods( .setVariableExpr(stubVarExpr.toBuilder().setExprReferenceExpr(thisExpr).build()) .setValueExpr(stubVarExpr) .build()); - ctorAssignmentExprs.add(operationsClientAssignExpr); + if (hasLroClient) { + ctorAssignmentExprs.add(operationsClientAssignExpr); + } AnnotationNode betaAnnotation = AnnotationNode.builder() .setType(types.get("BetaApi")) @@ -533,8 +538,8 @@ private static List createMethodVariants( if (argument.isResourceNameHelper()) { MethodInvocationExpr isNullCheckExpr = MethodInvocationExpr.builder() - .setStaticReferenceType(types.get("Strings")) - .setMethodName("isNullOrEmpty") + .setStaticReferenceType(types.get("Objects")) + .setMethodName("isNull") .setArguments(Arrays.asList(argVarExpr)) .setReturnType(TypeNode.BOOLEAN) .build(); @@ -893,6 +898,7 @@ private static Map createConcreteTypes() { InterruptedException.class, IOException.class, MoreExecutors.class, + Objects.class, Operation.class, OperationFuture.class, OperationCallable.class, diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java index 64cb53b7a3..85019df561 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceClientClassComposerTest.java @@ -73,13 +73,13 @@ public void generateServiceClasses() { + "import com.google.api.gax.rpc.ServerStreamingCallable;\n" + "import com.google.api.gax.rpc.UnaryCallable;\n" + "import com.google.api.resourcenames.ResourceName;\n" - + "import com.google.common.base.Strings;\n" + "import com.google.longrunning.Operation;\n" + "import com.google.longrunning.OperationsClient;\n" + "import com.google.rpc.Status;\n" + "import com.google.showcase.v1beta1.stub.EchoStub;\n" + "import com.google.showcase.v1beta1.stub.EchoStubSettings;\n" + "import java.io.IOException;\n" + + "import java.util.Objects;\n" + "import java.util.concurrent.TimeUnit;\n" + "import javax.annotation.Generated;\n" + "\n" @@ -213,7 +213,7 @@ public void generateServiceClasses() { + " public final EchoResponse echo(ResourceName parent) {\n" + " EchoRequest request =\n" + " EchoRequest.newBuilder()\n" - + " .setParent(Strings.isNullOrEmpty(parent) ? null : parent.toString())\n" + + " .setParent(Objects.isNull(parent) ? null : parent.toString())\n" + " .build();\n" + " return echo(request);\n" + " }\n" @@ -239,9 +239,8 @@ public void generateServiceClasses() { + " */\n" + " public final EchoResponse echo(FoobarName name) {\n" + " EchoRequest request =\n" - + " EchoRequest.newBuilder()\n" - + " .setName(Strings.isNullOrEmpty(name) ? null : name.toString())\n" - + " .build();\n" + + " EchoRequest.newBuilder().setName(Objects.isNull(name) ? null :" + + " name.toString()).build();\n" + " return echo(request);\n" + " }\n" + "\n" From ac89e1236dd7d49be8345283aa6d33300defd468 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Thu, 17 Sep 2020 23:29:39 -0700 Subject: [PATCH 12/22] fix: ServiceStubSettings builder type --- .../composer/ServiceStubSettingsClassComposer.java | 4 +--- .../ServiceStubSettingsClassComposerTest.java | 11 ++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposer.java index ae2228afd3..a2a509fb3e 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposer.java @@ -1144,9 +1144,7 @@ private static ClassDefinition createNestedBuilderClass( ConcreteReference.builder() .setClazz(StubSettings.Builder.class) .setGenerics( - Arrays.asList( - types.get(getServiceStubTypeName(service.name())), types.get(className)) - .stream() + Arrays.asList(types.get(thisClassName), types.get(className)).stream() .map(t -> t.reference()) .collect(Collectors.toList())) .build()); diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposerTest.java index bc8b293421..2696dcb7da 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceStubSettingsClassComposerTest.java @@ -471,7 +471,8 @@ private static List parseServices( + " }\n" + "\n" + " /** Builder for EchoStubSettings. */\n" - + " public static class Builder extends StubSettings.Builder {\n" + + " public static class Builder extends StubSettings.Builder" + + " {\n" + " private final ImmutableList>" + " unaryMethodSettingsBuilders;\n" + " private final UnaryCallSettings.Builder" @@ -1239,8 +1240,8 @@ private static List parseServices( + " }\n" + "\n" + " /** Builder for LoggingServiceV2StubSettings. */\n" - + " public static class Builder extends StubSettings.Builder {\n" + + " public static class Builder extends" + + " StubSettings.Builder {\n" + " private final ImmutableList>" + " unaryMethodSettingsBuilders;\n" + " private final UnaryCallSettings.Builder" @@ -1997,8 +1998,8 @@ private static List parseServices( + " }\n" + "\n" + " /** Builder for PublisherStubSettings. */\n" - + " public static class Builder extends StubSettings.Builder" - + " {\n" + + " public static class Builder extends StubSettings.Builder {\n" + " private final ImmutableList>" + " unaryMethodSettingsBuilders;\n" + " private final UnaryCallSettings.Builder createTopicSettings;\n" From b5e65856b590d9bc35411911216db614e1bc2ae0 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Thu, 17 Sep 2020 23:34:12 -0700 Subject: [PATCH 13/22] fix: ServiceSettings Builder construction --- .../gapic/composer/ServiceSettingsClassComposer.java | 4 ++-- .../gapic/composer/ServiceSettingsClassComposerTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposer.java index 070e860fe5..accb22d978 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposer.java @@ -230,8 +230,8 @@ private static MethodDefinition createCreatorMethod( TypeNode.withReference( VaporReference.builder() .setName("Builder") - .setEnclosingClassName(String.format("%sStubSettings", service.name())) - .setPakkage(String.format("%s.stub", service.pakkage())) + .setEnclosingClassName(String.format("%sSettings", service.name())) + .setPakkage(service.pakkage()) .build()); Expr returnMethodExpr = NewObjectExpr.builder() diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposerTest.java index 110d54f7d5..d6f15a7676 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceSettingsClassComposerTest.java @@ -162,7 +162,7 @@ public void generateServiceClasses() { + "\n" + " public static final EchoSettings create(EchoStubSettings stub) throws IOException" + " {\n" - + " return new EchoStubSettings.Builder(stub.toBuilder()).build();\n" + + " return new EchoSettings.Builder(stub.toBuilder()).build();\n" + " }\n" + "\n" + " /** Returns a builder for the default ExecutorProvider for this service. */\n" From 1e10b6bd5f9edbc4ec0a4f9512e940e10f9d3535 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Thu, 17 Sep 2020 23:35:14 -0700 Subject: [PATCH 14/22] fix: ServiceStub callable types --- .../generator/gapic/composer/ServiceStubClassComposer.java | 2 +- .../gapic/composer/ServiceStubClassComposerTest.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/composer/ServiceStubClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/ServiceStubClassComposer.java index eefcb890b6..6afbf9256b 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/ServiceStubClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/ServiceStubClassComposer.java @@ -159,7 +159,7 @@ private static MethodDefinition createCallableGetterHelper( (isLroCallable ? "Operation" : isPaged ? "Paged" : "")); List genericRefs = new ArrayList<>(); genericRefs.add(method.inputType().reference()); - if (method.hasLro()) { + if (method.hasLro() && isLroCallable) { genericRefs.add(method.lro().responseType().reference()); genericRefs.add(method.lro().metadataType().reference()); } else if (isPaged) { diff --git a/src/test/java/com/google/api/generator/gapic/composer/ServiceStubClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/ServiceStubClassComposerTest.java index cd40a257e0..217d4375b7 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/ServiceStubClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/ServiceStubClassComposerTest.java @@ -71,6 +71,7 @@ public void generateServiceClasses() { + "import com.google.api.gax.rpc.OperationCallable;\n" + "import com.google.api.gax.rpc.ServerStreamingCallable;\n" + "import com.google.api.gax.rpc.UnaryCallable;\n" + + "import com.google.longrunning.Operation;\n" + "import com.google.longrunning.stub.OperationsStub;\n" + "import com.google.showcase.v1beta1.BlockRequest;\n" + "import com.google.showcase.v1beta1.BlockResponse;\n" @@ -120,8 +121,8 @@ public void generateServiceClasses() { + " chatAgainCallable()\");\n" + " }\n" + "\n" - + " public UnaryCallable pagedExpandPagedCallable()" - + " {\n" + + " public UnaryCallable" + + " pagedExpandPagedCallable() {\n" + " throw new UnsupportedOperationException(\"Not implemented:" + " pagedExpandPagedCallable()\");\n" + " }\n" @@ -138,7 +139,7 @@ public void generateServiceClasses() { + " waitOperationCallable()\");\n" + " }\n" + "\n" - + " public UnaryCallable waitCallable() {\n" + + " public UnaryCallable waitCallable() {\n" + " throw new UnsupportedOperationException(\"Not implemented: waitCallable()\");\n" + " }\n" + "\n" From 006aff477afd9756a39a272e49b4f5231fdd841f Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Thu, 17 Sep 2020 23:37:07 -0700 Subject: [PATCH 15/22] feat: java_gapic_library rule impl --- WORKSPACE | 16 +-- dependencies.properties | 8 +- repositories.bzl | 18 --- rules_java_gapic/java_gapic.bzl | 105 ++++++++++++++++++ .../api/generator/gapic/composer/BUILD.bazel | 6 +- .../generator/gapic/protowriter/Writer.java | 7 +- .../api/generator/gapic/composer/BUILD.bazel | 2 +- 7 files changed, 125 insertions(+), 37 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 162b2b5777..2304711751 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -21,6 +21,7 @@ com_google_api_generator_properties( file = "//:dependencies.properties", ) +load("@com_google_api_generator_properties//:dependencies.properties.bzl", "PROPERTIES") load("//:repositories.bzl", "com_google_api_generator_repositories") com_google_api_generator_repositories() @@ -47,27 +48,22 @@ switched_rules_by_language( java = True, ) -# TODO(miraleung): When the gax-java Bazel build PRs are submitted, do the following: -# - Rename the next rule. -# - Use these rules in build files: -# - "@com_google_api_gax_java//gax", -# - "@com_google_api_gax_java//gax-grpc:gax_grpc", -_gax_java_version = "1.58.2" +_gax_java_version = PROPERTIES["version.com_google_gax_java"] http_archive( - name = "com_google_api_gax_java_temp", + name = "com_google_api_gax_java", strip_prefix = "gax-java-%s" % _gax_java_version, urls = ["https://github.com/googleapis/gax-java/archive/v%s.zip" % _gax_java_version], ) -load("@com_google_api_gax_java_temp//:repository_rules.bzl", "com_google_api_gax_java_properties") +load("@com_google_api_gax_java//:repository_rules.bzl", "com_google_api_gax_java_properties") com_google_api_gax_java_properties( name = "com_google_api_gax_java_properties", - file = "@com_google_api_gax_java_temp//:dependencies.properties", + file = "@com_google_api_gax_java//:dependencies.properties", ) -load("@com_google_api_gax_java_temp//:repositories.bzl", "com_google_api_gax_java_repositories") +load("@com_google_api_gax_java//:repositories.bzl", "com_google_api_gax_java_repositories") com_google_api_gax_java_repositories() diff --git a/dependencies.properties b/dependencies.properties index feeea4cce5..382d6d21ac 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -11,7 +11,8 @@ version.com_google_protobuf=3.12.2 # Version of google-java-format is downgraded from 1.8 to 1.7, because 1.8 supports java 11 minimum, while our JRE is java 8. version.google_java_format=1.7 version.com_google_api_common_java=1.9.3 -version.com_google_gax_java=1.57.0 +# TODO(miraleung): Update this. +version.com_google_gax_java=1.58.3 version.io_grpc_java=1.30.2 # Common deps. @@ -20,7 +21,7 @@ maven.com_google_code_findbugs_jsr305=com.google.code.findbugs:jsr305:3.0.0 maven.com_google_auto_value_auto_value=com.google.auto.value:auto-value:1.7.2 maven.com_google_auto_value_auto_value_annotations=com.google.auto.value:auto-value-annotations:1.7.2 maven.com_google_protobuf_protobuf_java=com.google.protobuf:protobuf-java:3.12.2 -maven.javax_annotation_api=javax.annotation:javax.annotation-api:1.3.2 +maven.javax_annotation_javax_annotation_api=javax.annotation:javax.annotation-api:1.3.2 maven.javax_validation_javax_validation_api=javax.validation:validation-api:2.0.1.Final # Gapic YAML parsing for batching settings. @@ -32,4 +33,5 @@ maven.org_threeten_threetenbp=org.threeten:threetenbp:1.3.3 # Testing. maven.junit_junit=junit:junit:4.13 maven.org_mockito_mockito_core=org.mockito:mockito-core:2.21.0 -maven.com_google_truth_truth=com.google.truth:truth:0.42 +# Keep in sync with gax-java. +maven.com_google_truth_truth=com.google.truth:truth:1.0 diff --git a/repositories.bzl b/repositories.bzl index 3c207aae8d..18c2e37184 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -99,24 +99,6 @@ def com_google_api_generator_repositories(): server_urls = ["https://repo.maven.apache.org/maven2/"], ) - # TODO(miraleung): Remove these gax imports when gax-java's Bazel build PRs have been submitted. - _gax_java_version = PROPERTIES["version.com_google_gax_java"] - - # Use the Maven artifact because a full bazel-build requires pulling in many transitive deps. - _maybe( - jvm_maven_import_external, - name = "com_google_api_gax_java", - artifact = "com.google.api:gax:%s" % _gax_java_version, - server_urls = ["https://repo.maven.apache.org/maven2/"], - ) - - _maybe( - jvm_maven_import_external, - name = "com_google_api_gax_grpc", - artifact = "com.google.api:gax-grpc:%s" % _gax_java_version, - server_urls = ["https://repo.maven.apache.org/maven2/"], - ) - # grpc-proto doesn't have releases, so we use hashes instead. _io_grpc_proto_prefix = "0020624375a8ee4c7dd9b3e513e443b90bc28990" # Aug. 20, 2020. _maybe( diff --git a/rules_java_gapic/java_gapic.bzl b/rules_java_gapic/java_gapic.bzl index 6bdb1a437d..86f38d9b0f 100644 --- a/rules_java_gapic/java_gapic.bzl +++ b/rules_java_gapic/java_gapic.bzl @@ -14,6 +14,59 @@ load("@com_google_api_codegen//rules_gapic:gapic.bzl", "proto_custom_library", "unzipped_srcjar") +def _java_gapic_postprocess_srcjar_impl(ctx): + gapic_srcjar = ctx.file.gapic_srcjar + output_srcjar_name = ctx.label.name + output_main = ctx.outputs.main + output_test = ctx.outputs.test + formatter = ctx.executable.formatter + + output_dir_name = ctx.label.name + output_dir_path = "%s/%s" % (output_main.dirname, output_dir_name) + + script = """ + unzip -q {gapic_srcjar} + # Sync'd to the output file name in Writer.java. + unzip -q temp-codegen.srcjar -d {output_dir_path} + # This may fail if there are spaces and/or too many files (exceed max length of command length). + {formatter} --replace $(find {output_dir_path} -type f -printf "%p ") + zip -r -j {output_srcjar_name}.srcjar {output_dir_path}/src/main/* {output_dir_path}/proto/src/main/* + zip -r -j {output_srcjar_name}-tests.srcjar {output_dir_path}/src/test/* + mv {output_srcjar_name}.srcjar {output_main} + mv {output_srcjar_name}-tests.srcjar {output_test} + """.format( + gapic_srcjar = gapic_srcjar.path, + output_srcjar_name = output_srcjar_name, + output_main = output_main.path, + formatter = formatter, + output_dir_name = output_dir_name, + output_dir_path = output_dir_path, + output_test = output_test.path, + ) + + ctx.actions.run_shell( + inputs = [gapic_srcjar], + tools = [formatter], + command = script, + outputs = [output_main, output_test], + ) + +_java_gapic_postprocess_srcjar = rule( + attrs = { + "gapic_srcjar": attr.label(mandatory = True, allow_single_file = True), + "formatter": attr.label( + default = Label("//:google_java_format_binary"), + executable = True, + cfg = "host", + ), + }, + outputs = { + "main": "%{name}.srcjar", + "test": "%{name}-test.srcjar", + }, + implementation = _java_gapic_postprocess_srcjar_impl, +) + def java_gapic_library( name, srcs, @@ -53,3 +106,55 @@ def java_gapic_library( output_suffix = output_suffix, **kwargs ) + + _java_gapic_postprocess_srcjar( + name = srcjar_name, + gapic_srcjar = "%s.srcjar" % raw_srcjar_name, + **kwargs + ) + + # General additional deps. + actual_deps = deps + [ + "@com_google_googleapis//google/rpc:rpc_java_proto", + "@com_google_googleapis//google/longrunning:longrunning_java_proto", + "@com_google_protobuf//:protobuf_java", + "@com_google_api_api_common//jar", + "@com_google_api_gax_java//gax:gax", + "@com_google_api_gax_java//gax-grpc:gax_grpc", + "@com_google_guava_guava//jar", + "@io_grpc_grpc_java//core:core", + "@io_grpc_grpc_java//protobuf:protobuf", + "@com_google_code_findbugs_jsr305//jar", + "@org_threeten_threetenbp//jar", + "@io_opencensus_opencensus_api//jar", + "@com_google_auth_google_auth_library_credentials//jar", + "@com_google_auth_google_auth_library_oauth2_http//jar", + "@com_google_http_client_google_http_client//jar", + "@javax_annotation_javax_annotation_api//jar", + ] + + native.java_library( + name = name, + srcs = ["%s.srcjar" % srcjar_name], + deps = actual_deps, + **kwargs + ) + + # Test deps. + actual_test_deps = test_deps + actual_deps + [ + "@com_google_api_gax_java//gax-grpc:gax_grpc_testlib", + "@com_google_api_gax_java//gax:gax_testlib", + "@com_google_code_gson_gson//jar", + "@io_grpc_grpc_java//auth:auth", + "@io_grpc_grpc_netty_shaded//jar", + "@io_grpc_grpc_java//stub:stub", + "@io_opencensus_opencensus_contrib_grpc_metrics//jar", + "@junit_junit//jar", + ] + + native.java_library( + name = "%s_test" % name, + srcs = ["%s-test.srcjar" % srcjar_name], + deps = [":%s" % name] + actual_test_deps, + **kwargs + ) diff --git a/src/main/java/com/google/api/generator/gapic/composer/BUILD.bazel b/src/main/java/com/google/api/generator/gapic/composer/BUILD.bazel index af94a71b49..0e19bf3dec 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/BUILD.bazel +++ b/src/main/java/com/google/api/generator/gapic/composer/BUILD.bazel @@ -17,8 +17,8 @@ java_library( "//src/main/java/com/google/api/generator/gapic/model", "//src/main/java/com/google/api/generator/gapic/utils", "@com_google_api_api_common//jar", - "@com_google_api_gax_grpc//jar", - "@com_google_api_gax_java//jar", + "@com_google_api_gax_java//gax", + "@com_google_api_gax_java//gax-grpc:gax_grpc", "@com_google_code_findbugs_jsr305//jar", "@com_google_googleapis//google/api:api_java_proto", "@com_google_googleapis//google/longrunning:longrunning_java_proto", @@ -30,7 +30,7 @@ java_library( "@io_grpc_grpc_java//api", "@io_grpc_grpc_java//protobuf", "@io_grpc_grpc_java//stub", - "@javax_annotation_api//jar", + "@javax_annotation_javax_annotation_api//jar", "@junit_junit//jar", "@org_threeten_threetenbp//jar", ], diff --git a/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java b/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java index fac6a6a0f6..55c61c9e07 100644 --- a/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java +++ b/src/main/java/com/google/api/generator/gapic/protowriter/Writer.java @@ -81,9 +81,12 @@ public static CodeGeneratorResponse writeCode(List clazzes, String o private static String getPath(String pakkage, String className) { String path = pakkage.replaceAll("\\.", "/"); if (className.startsWith("Mock") || className.endsWith("Test")) { - path = "test/" + path; + path = "src/test/java/" + path; + } else { + path = "src/main/java/" + path; } - // Resource name helpers go into the protobuf package. + + // Resource name helpers go into the protobuf package. Denote this with "proto/src/main/*". if (className.endsWith("Name")) { path = "proto/" + path; } diff --git a/src/test/java/com/google/api/generator/gapic/composer/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/composer/BUILD.bazel index e1601e6536..e291e47611 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/composer/BUILD.bazel @@ -52,7 +52,7 @@ java_proto_library( "//src/main/java/com/google/api/generator/gapic/model", "//src/main/java/com/google/api/generator/gapic/protoparser", "//src/test/java/com/google/api/generator/gapic/testdata:showcase_java_proto", - "@com_google_api_gax_java//jar", + "@com_google_api_gax_java//gax", "@com_google_googleapis//google/logging/v2:logging_java_proto", "@com_google_googleapis//google/pubsub/v1:pubsub_java_proto", "@com_google_googleapis//google/rpc:rpc_java_proto", From f25274f8a3810febe8bc5edf3dc843e922847525 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 13:05:15 -0700 Subject: [PATCH 16/22] fix: remove debugging comments --- rules_java_gapic/java_gapic.bzl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rules_java_gapic/java_gapic.bzl b/rules_java_gapic/java_gapic.bzl index 86f38d9b0f..d9ea0eb48a 100644 --- a/rules_java_gapic/java_gapic.bzl +++ b/rules_java_gapic/java_gapic.bzl @@ -1,4 +1,4 @@ -# Copyright 2019 Google LLC +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -89,8 +89,6 @@ def java_gapic_library( if service_yaml: file_args_dict[service_yaml] = "gapic-service-config" - #file_args = ["%s=%s" % (k, v) for k, v in file_args_dict.items()] - srcjar_name = name + "_srcjar" raw_srcjar_name = srcjar_name + "_raw" output_suffix = ".srcjar" From 76d26d8572e59cfa24847b2308ed4a97e1312d39 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 14:23:27 -0700 Subject: [PATCH 17/22] feat: add gradle assembly Bazel rules --- rules_java_gapic/BUILD.bazel | 20 + rules_java_gapic/java_gapic_pkg.bzl | 426 ++++++++++++++++++ .../resources/gradle/assembly.gradle.tmpl | 29 ++ .../resources/gradle/client.gradle.tmpl | 61 +++ .../gradle/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53638 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + rules_java_gapic/resources/gradle/gradlew | 160 +++++++ rules_java_gapic/resources/gradle/gradlew.bat | 90 ++++ .../resources/gradle/grpc.gradle.tmpl | 49 ++ .../resources/gradle/proto.gradle.tmpl | 53 +++ .../resources/gradle/settings.gradle.tmpl | 7 + 11 files changed, 901 insertions(+) create mode 100644 rules_java_gapic/java_gapic_pkg.bzl create mode 100644 rules_java_gapic/resources/gradle/assembly.gradle.tmpl create mode 100644 rules_java_gapic/resources/gradle/client.gradle.tmpl create mode 100644 rules_java_gapic/resources/gradle/gradle/wrapper/gradle-wrapper.jar create mode 100644 rules_java_gapic/resources/gradle/gradle/wrapper/gradle-wrapper.properties create mode 100755 rules_java_gapic/resources/gradle/gradlew create mode 100644 rules_java_gapic/resources/gradle/gradlew.bat create mode 100644 rules_java_gapic/resources/gradle/grpc.gradle.tmpl create mode 100644 rules_java_gapic/resources/gradle/proto.gradle.tmpl create mode 100644 rules_java_gapic/resources/gradle/settings.gradle.tmpl diff --git a/rules_java_gapic/BUILD.bazel b/rules_java_gapic/BUILD.bazel index e69de29bb2..1d3f0b6c03 100644 --- a/rules_java_gapic/BUILD.bazel +++ b/rules_java_gapic/BUILD.bazel @@ -0,0 +1,20 @@ +load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") + +package(default_visibility = ["//visibility:public"]) + +exports_files(glob(["resources/**/*"])) + +pkg_tar( + name = "gradlew", + extension = "tar.gz", + files = { + "resources/gradle/gradle/wrapper/gradle-wrapper.jar": "gradle/wrapper/gradle-wrapper.jar", + "resources/gradle/gradle/wrapper/gradle-wrapper.properties": "gradle/wrapper/gradle-wrapper.properties", + "resources/gradle/gradlew": "gradlew", + "resources/gradle/gradlew.bat": "gradlew.bat", + }, + mode = "644", + modes = { + "gradlew": "555", + }, +) diff --git a/rules_java_gapic/java_gapic_pkg.bzl b/rules_java_gapic/java_gapic_pkg.bzl new file mode 100644 index 0000000000..51da072f5d --- /dev/null +++ b/rules_java_gapic/java_gapic_pkg.bzl @@ -0,0 +1,426 @@ +# Copyright 2019 Google LLC +# +# 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 +# +# https://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. + +load("@com_google_api_gax_java_properties//:dependencies.properties.bzl", "PROPERTIES") + +def _wrapPropertyNamesInBraces(properties): + wrappedProperties = {} + for k, v in properties.items(): + wrappedProperties["{{%s}}" % k] = v + return wrappedProperties + +_PROPERTIES = _wrapPropertyNamesInBraces(PROPERTIES) + +# ======================================================================== +# General packaging helpers. +# ======================================================================== + +def _construct_package_dir_paths(attr_package_dir, out_pkg, label_name): + if attr_package_dir: + package_dir = attr_package_dir + package_dir_expr = "../{}/".format(package_dir) + tar_cd_suffix = ".." + tar_prefix = attr_package_dir + else: + package_dir = label_name + package_dir_expr = "./" + tar_cd_suffix = "." + tar_prefix = "." + + # We need to include label in the path to eliminate possible output files duplicates + # (labels are guaranteed to be unique by bazel itself) + package_dir_path = "%s/%s/%s" % (out_pkg.dirname, label_name, package_dir) + return struct( + package_dir = package_dir, + package_dir_expr = package_dir_expr, + package_dir_path = package_dir_path, + package_dir_sibling_parent = out_pkg, + package_dir_sibling_basename = label_name, + tar_cd_suffix = tar_cd_suffix, + tar_prefix = tar_prefix, + ) + +def _put_dep_in_a_bucket(dep, dep_bucket, processed_deps): + if processed_deps.get(dep): + return + dep_bucket.append(dep) + processed_deps[dep] = True + +def _gapic_pkg_tar_impl(ctx): + deps = [] + for dep in ctx.attr.deps: + for f in dep.files.to_list(): + deps.append(f) + + paths = _construct_package_dir_paths( + ctx.attr.package_dir, + ctx.outputs.pkg, + ctx.label.name, + ) + + script = """ + mkdir -p {package_dir_path} + for dep in {deps}; do + tar -xzpf $dep -C {package_dir_path} + done + cd {package_dir_path}/{tar_cd_suffix} + tar -zchpf {tar_prefix}/{package_dir}.tar.gz {tar_prefix}/* + cd - + mv {package_dir_path}/{package_dir}.tar.gz {pkg} + rm -rf {package_dir_path} + """.format( + deps = " ".join(["'%s'" % d.path for d in deps]), + package_dir_path = paths.package_dir_path, + package_dir = paths.package_dir, + pkg = ctx.outputs.pkg.path, + tar_cd_suffix = paths.tar_cd_suffix, + tar_prefix = paths.tar_prefix, + ) + + ctx.actions.run_shell( + inputs = deps, + command = script, + outputs = [ctx.outputs.pkg], + ) + +# The Bazel's native gapic_pkg_tar rule behaves weirdly when package_dir parameter +# is specified (at least on some Linux machines it does not put all the files +# under the package_dir). As a workaround for that bug we provide the custom +# implementation of the gapic_pkg_tar rule. +gapic_pkg_tar = rule( + attrs = { + "deps": attr.label_list(mandatory = True), + "package_dir": attr.string(mandatory = False, default = ""), + "extension": attr.string(mandatory = False, default = "tar.gz"), + }, + outputs = {"pkg": "%{name}.%{extension}"}, + implementation = _gapic_pkg_tar_impl, +) + +# ======================================================================== +# Java Gapic package helpers. +# ======================================================================== +def _construct_extra_deps(scope_to_deps, versions_map): + label_name_to_maven_artifact = { + "policy_proto": "maven.com_google_api_grpc_proto_google_iam_v1", + "iam_policy_proto": "maven.com_google_api_grpc_proto_google_iam_v1", + "iam_java_proto": "maven.com_google_api_grpc_proto_google_iam_v1", + "iam_java_grpc": "maven.com_google_api_grpc_grpc_google_iam_v1", + "iam_policy_java_grpc": "maven.com_google_api_grpc_grpc_google_iam_v1", + } + extra_deps = {} + for scope, deps in scope_to_deps.items(): + for dep in deps: + pkg_dependency = _get_gapic_pkg_dependency_name(dep) + if pkg_dependency: + key = "{{%s}}" % pkg_dependency + if not extra_deps.get(key): + extra_deps[key] = "%s project(':%s')" % (scope, pkg_dependency) + elif _is_java_dependency(dep): + for f in dep[JavaInfo].transitive_deps.to_list(): + maven_artifact = label_name_to_maven_artifact.get(f.owner.name) + if not maven_artifact: + continue + key = "{{%s}}" % maven_artifact + if not extra_deps.get(key): + extra_deps[key] = "%s '%s'" % (scope, versions_map[key]) + + return "\n ".join(extra_deps.values()) + +def _is_java_dependency(dep): + return JavaInfo in dep + +def _is_source_dependency(dep): + return _is_java_dependency(dep) and hasattr(dep[JavaInfo], "source_jars") and dep.label.package != "jar" + +def _is_proto_dependency(dep): + return ProtoInfo in dep + +def _get_gapic_pkg_dependency_name(dep): + files_list = dep.files.to_list() + if not files_list or len(files_list) != 1: + return None + for ext in (".tar.gz", ".gz", ".tgz"): + if files_list[0].basename.endswith(ext): + return files_list[0].basename[:-len(ext)] + return None + +# ======================================================================== +# Java Gapic package rules. +# ======================================================================== + +def _java_gapic_build_configs_pkg_impl(ctx): + expanded_templates = [] + paths = _construct_package_dir_paths(ctx.attr.package_dir, ctx.outputs.pkg, ctx.label.name) + + substitutions = dict(ctx.attr.static_substitutions) + substitutions["{{extra_deps}}"] = _construct_extra_deps({ + "compile": ctx.attr.deps, + "testCompile": ctx.attr.test_deps, + }, substitutions) + + for template in ctx.attr.templates.items(): + expanded_template = ctx.actions.declare_file( + "%s/%s" % (paths.package_dir_sibling_basename, template[1]), + sibling = paths.package_dir_sibling_parent, + ) + expanded_templates.append(expanded_template) + ctx.actions.expand_template( + template = template[0].files.to_list()[0], + substitutions = substitutions, + output = expanded_template, + ) + + # Note the script is more complicated than it intuitively should be because of the limitations + # inherent to bazel execution environment: no absolute paths allowed, the generated artifacts + # must ensure uniqueness within a build. The template output directory manipulations are + # to modify default 555 file permissions on any generated by bazel file (exectuable read-only, + # which is not at all what we need for build files). There is no bazel built-in way to change + # the generated files permissions, also the actual files accessible by the script are symlinks + # and `chmod`, when applied to a directory, does not change the attributes of symlink targets + # inside the directory. Chaning the symlink target's permissions is also not an option, because + # they are on a read-only file system. + script = """ + mkdir -p {package_dir_path} + for templ in {templates}; do + cp $templ {package_dir_path}/ + done + chmod 644 {package_dir_path}/* + cd {package_dir_path}/{tar_cd_suffix} + tar -zchpf {tar_prefix}/{package_dir}.tar.gz {tar_prefix}/* + cd - + mv {package_dir_path}/{package_dir}.tar.gz {pkg} + """.format( + templates = " ".join(["'%s'" % f.path for f in expanded_templates]), + package_dir_path = paths.package_dir_path, + package_dir = paths.package_dir, + pkg = ctx.outputs.pkg.path, + tar_cd_suffix = paths.tar_cd_suffix, + tar_prefix = paths.tar_prefix, + ) + + ctx.actions.run_shell( + inputs = expanded_templates, + command = script, + outputs = [ctx.outputs.pkg], + ) + +java_gapic_build_configs_pkg = rule( + attrs = { + "deps": attr.label_list(mandatory = True), + "test_deps": attr.label_list(mandatory = False, allow_empty = True), + "package_dir": attr.string(mandatory = False), + "templates": attr.label_keyed_string_dict(mandatory = False, allow_files = True), + "static_substitutions": attr.string_dict(mandatory = False, allow_empty = True, default = {}), + }, + outputs = {"pkg": "%{name}.tar.gz"}, + implementation = _java_gapic_build_configs_pkg_impl, +) + +def _java_gapic_srcs_pkg_impl(ctx): + srcs = [] + proto_srcs = [] + for src_dep in ctx.attr.deps: + if _is_source_dependency(src_dep): + srcs.extend(src_dep[JavaInfo].source_jars) + if _is_proto_dependency(src_dep): + proto_srcs.extend(src_dep[ProtoInfo].check_deps_sources.to_list()) + + test_srcs = [] + for test_src_dep in ctx.attr.test_deps: + if _is_source_dependency(test_src_dep): + test_srcs.extend(test_src_dep[JavaInfo].source_jars) + + paths = _construct_package_dir_paths(ctx.attr.package_dir, ctx.outputs.pkg, ctx.label.name) + + # Note the script is more complicated than it intuitively should be because of limitations + # inherent to bazel execution environment: no absolute paths allowed, the generated artifacts + # must ensure uniqueness within a build. + script = """ + for src in {srcs}; do + mkdir -p {package_dir_path}/src/main/java + unzip -q -o $src -d {package_dir_path}/src/main/java + rm -r -f {package_dir_path}/src/main/java/META-INF + done + for proto_src in {proto_srcs}; do + mkdir -p {package_dir_path}/src/main/proto + cp -f --parents $proto_src {package_dir_path}/src/main/proto + done + for test_src in {test_srcs}; do + mkdir -p {package_dir_path}/src/test/java + unzip -q -o $test_src -d {package_dir_path}/src/test/java + rm -r -f {package_dir_path}/src/test/java/META-INF + done + cd {package_dir_path}/{tar_cd_suffix} + tar -zchpf {tar_prefix}/{package_dir}.tar.gz {tar_prefix}/* + cd - + mv {package_dir_path}/{package_dir}.tar.gz {pkg} + """.format( + srcs = " ".join(["'%s'" % f.path for f in srcs]), + proto_srcs = " ".join(["'%s'" % f.path for f in proto_srcs]), + test_srcs = " ".join(["'%s'" % f.path for f in test_srcs]), + package_dir_path = paths.package_dir_path, + package_dir = paths.package_dir, + pkg = ctx.outputs.pkg.path, + tar_cd_suffix = paths.tar_cd_suffix, + tar_prefix = paths.tar_prefix, + ) + + ctx.actions.run_shell( + inputs = srcs + proto_srcs + test_srcs, + command = script, + outputs = [ctx.outputs.pkg], + ) + +java_gapic_srcs_pkg = rule( + attrs = { + "deps": attr.label_list(mandatory = True), + "test_deps": attr.label_list(mandatory = False, allow_empty = True), + "package_dir": attr.string(mandatory = True), + }, + outputs = {"pkg": "%{name}.tar.gz"}, + implementation = _java_gapic_srcs_pkg_impl, +) + +def java_gapic_assembly_gradle_pkg( + name, + deps, + assembly_name = None, + **kwargs): + package_dir = name + if assembly_name: + package_dir = "google-cloud-%s-%s" % (assembly_name, name) + + # Rename to avoid target conflicts with the monolith. + proto_target = "proto-%s" % package_dir + proto_target_dep = [] + grpc_target = "grpc-%s" % package_dir + grpc_target_dep = [] + client_target = "gapic-%s" % package_dir + client_target_dep = [] + + client_deps = [] + client_test_deps = [] + grpc_deps = [] + proto_deps = [] + + processed_deps = {} #there is no proper Set in Starlark + for dep in deps: + # Use contains instead of endswith since microgenerator testing may use differently-named targets. + if "_java_gapic" in dep: + _put_dep_in_a_bucket(dep, client_deps, processed_deps) + _put_dep_in_a_bucket("%s_test" % dep, client_test_deps, processed_deps) + elif dep.endswith("_java_grpc"): + _put_dep_in_a_bucket(dep, grpc_deps, processed_deps) + else: + _put_dep_in_a_bucket(dep, proto_deps, processed_deps) + + if proto_deps: + _java_gapic_gradle_pkg( + name = proto_target, + template_label = Label("//rules_java_gapic:resources/gradle/proto.gradle.tmpl"), + deps = proto_deps, + **kwargs + ) + proto_target_dep = [":%s" % proto_target] + + if grpc_deps: + _java_gapic_gradle_pkg( + name = grpc_target, + template_label = Label("//rules_java_gapic:resources/gradle/grpc.gradle.tmpl"), + deps = proto_target_dep + grpc_deps, + **kwargs + ) + grpc_target_dep = ["%s" % grpc_target] + + if client_deps: + _java_gapic_gradle_pkg( + name = client_target, + template_label = Label("//rules_java_gapic:resources/gradle/client.gradle.tmpl"), + deps = proto_target_dep + client_deps, + test_deps = grpc_target_dep + client_test_deps, + **kwargs + ) + client_target_dep = ["%s" % client_target] + + _java_gapic_assembly_gradle_pkg( + name = name, + assembly_name = package_dir, + deps = proto_target_dep + grpc_target_dep + client_target_dep, + ) + +def _java_gapic_gradle_pkg( + name, + template_label, + deps, + test_deps = None, + project_deps = None, + test_project_deps = None, + **kwargs): + resource_target_name = "%s-resources" % name + + static_substitutions = dict(_PROPERTIES) + static_substitutions["{{name}}"] = name + + java_gapic_build_configs_pkg( + name = resource_target_name, + deps = deps, + test_deps = test_deps, + package_dir = name, + templates = { + template_label: "build.gradle", + }, + static_substitutions = static_substitutions, + ) + + srcs_gapic_pkg_target_name = "%s-srcs_pkg" % name + java_gapic_srcs_pkg( + name = srcs_gapic_pkg_target_name, + deps = deps, + test_deps = test_deps, + package_dir = name, + **kwargs + ) + + gapic_pkg_tar( + name = name, + extension = "tar.gz", + deps = [ + resource_target_name, + srcs_gapic_pkg_target_name, + ], + **kwargs + ) + +def _java_gapic_assembly_gradle_pkg(name, assembly_name, deps, visibility = None): + resource_target_name = "%s-resources" % assembly_name + java_gapic_build_configs_pkg( + name = resource_target_name, + deps = deps, + templates = { + Label("//rules_java_gapic:resources/gradle/assembly.gradle.tmpl"): "build.gradle", + Label("//rules_java_gapic:resources/gradle/settings.gradle.tmpl"): "settings.gradle", + }, + ) + + gapic_pkg_tar( + name = name, + extension = "tar.gz", + deps = [ + Label("//rules_java_gapic:gradlew"), + resource_target_name, + ] + deps, + package_dir = assembly_name, + visibility = visibility, + ) diff --git a/rules_java_gapic/resources/gradle/assembly.gradle.tmpl b/rules_java_gapic/resources/gradle/assembly.gradle.tmpl new file mode 100644 index 0000000000..3c0e59ee69 --- /dev/null +++ b/rules_java_gapic/resources/gradle/assembly.gradle.tmpl @@ -0,0 +1,29 @@ +buildscript { + repositories { + mavenLocal() + maven { + url 'https://plugins.gradle.org/m2/' + } + mavenCentral() + } +} + +subprojects { + apply plugin: 'java' + apply plugin: 'maven' + + sourceCompatibility = 1.7 + targetCompatibility = 1.7 + + test { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + exceptionFormat = 'full' + } + } + + repositories { + mavenLocal() + mavenCentral() + } +} diff --git a/rules_java_gapic/resources/gradle/client.gradle.tmpl b/rules_java_gapic/resources/gradle/client.gradle.tmpl new file mode 100644 index 0000000000..137f38d6f2 --- /dev/null +++ b/rules_java_gapic/resources/gradle/client.gradle.tmpl @@ -0,0 +1,61 @@ +buildscript { + repositories { + mavenCentral() + } +} + +apply plugin: 'java' + +description = 'GAPIC library for {{name}}' +group = 'com.google.cloud' +version = (findProperty('version') == 'unspecified') ? '0.0.0-SNAPSHOT' : version +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +repositories { + mavenCentral() + mavenLocal() +} + +compileJava.options.encoding = 'UTF-8' +javadoc.options.encoding = 'UTF-8' + +dependencies { + compile 'com.google.api:gax:{{version.gax}}' + testCompile 'com.google.api:gax:{{version.gax}}:testlib' + compile 'com.google.api:gax-grpc:{{version.gax_grpc}}' + testCompile 'com.google.api:gax-grpc:{{version.gax_grpc}}:testlib' + testCompile 'io.grpc:grpc-netty-shaded:{{version.io_grpc}}' + testCompile '{{maven.junit_junit}}' + {{extra_deps}} +} + +task smokeTest(type: Test) { + filter { + includeTestsMatching "*SmokeTest" + setFailOnNoMatchingTests false + } +} + +test { + exclude "**/*SmokeTest*" +} + +sourceSets { + main { + java { + srcDir 'src/main/java' + } + } +} + +clean { + delete 'all-jars' +} + +task allJars(type: Copy) { + dependsOn test, jar + into 'all-jars' + // Replace with `from configurations.testRuntime, jar` to include test dependencies + from configurations.runtime, jar +} diff --git a/rules_java_gapic/resources/gradle/gradle/wrapper/gradle-wrapper.jar b/rules_java_gapic/resources/gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 GIT binary patch literal 53638 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%0AB1q=av0)K=-*!|(RDoglJ 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/rules_java_gapic/resources/gradle/gradlew.bat b/rules_java_gapic/resources/gradle/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/rules_java_gapic/resources/gradle/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rules_java_gapic/resources/gradle/grpc.gradle.tmpl b/rules_java_gapic/resources/gradle/grpc.gradle.tmpl new file mode 100644 index 0000000000..d5dfa3f3de --- /dev/null +++ b/rules_java_gapic/resources/gradle/grpc.gradle.tmpl @@ -0,0 +1,49 @@ +buildscript { + repositories { + mavenCentral() + } +} + +apply plugin: 'java' + +description = 'GRPC library for {{name}}' +group = 'com.google.api.grpc' +version = (findProperty('version') == 'unspecified') ? '0.0.0-SNAPSHOT' : version +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + compile 'io.grpc:grpc-stub:{{version.io_grpc}}' + compile 'io.grpc:grpc-protobuf:{{version.io_grpc}}' + {{extra_deps}} +} + +sourceSets { + main { + java { + srcDir 'src/main/java' + } + } +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives javadocJar, sourcesJar +} + +compileJava.options.encoding = 'UTF-8' +javadoc.options.encoding = 'UTF-8' diff --git a/rules_java_gapic/resources/gradle/proto.gradle.tmpl b/rules_java_gapic/resources/gradle/proto.gradle.tmpl new file mode 100644 index 0000000000..ad5e20fc33 --- /dev/null +++ b/rules_java_gapic/resources/gradle/proto.gradle.tmpl @@ -0,0 +1,53 @@ +buildscript { + repositories { + mavenCentral() + } +} + +apply plugin: 'java' + +description = 'PROTO library for {{name}}' +group = 'com.google.api.grpc' +version = (findProperty('version') == 'unspecified') ? '0.0.0-SNAPSHOT' : version +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + compile 'com.google.protobuf:protobuf-java:{{version.com_google_protobuf}}' + compile '{{maven.com_google_api_api_common}}' + compile '{{maven.com_google_api_grpc_proto_google_common_protos}}' + {{extra_deps}} +} + +sourceSets { + main { + resources { + srcDir 'src/main/proto' + } + java { + srcDir 'src/main/java' + } + } +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives javadocJar, sourcesJar +} + +compileJava.options.encoding = 'UTF-8' +javadoc.options.encoding = 'UTF-8' diff --git a/rules_java_gapic/resources/gradle/settings.gradle.tmpl b/rules_java_gapic/resources/gradle/settings.gradle.tmpl new file mode 100644 index 0000000000..ef8c6b79a5 --- /dev/null +++ b/rules_java_gapic/resources/gradle/settings.gradle.tmpl @@ -0,0 +1,7 @@ +include rootDir.listFiles().findAll { + it.isDirectory() && !(it =~ ".*/\\..*") && !(it =~ "^\\..*") +}.collect { + it.getName() +}. toArray(new String[0]) + + From c56271f9bc97b8551f1e2ddbd0bf1ba5fd0d815c Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 14:33:08 -0700 Subject: [PATCH 18/22] feat: add java_gapic_test Bazel rule --- rules_java_gapic/java_gapic.bzl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rules_java_gapic/java_gapic.bzl b/rules_java_gapic/java_gapic.bzl index d9ea0eb48a..3a4c494113 100644 --- a/rules_java_gapic/java_gapic.bzl +++ b/rules_java_gapic/java_gapic.bzl @@ -156,3 +156,17 @@ def java_gapic_library( deps = [":%s" % name] + actual_test_deps, **kwargs ) + +def java_gapic_test(name, runtime_deps, test_classes, **kwargs): + for test_class in test_classes: + native.java_test( + name = test_class, + test_class = test_class, + runtime_deps = runtime_deps, + **kwargs + ) + native.test_suite( + name = name, + tests = test_classes, + **kwargs + ) From 98ccc00d364c715c2550830b9497ad37071e42d1 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 16:03:34 -0700 Subject: [PATCH 19/22] fix: use Java packages for resname codegen --- .../generator/gapic/protoparser/Parser.java | 40 +++++++++++++++++-- .../gapic/protoparser/ResourceNameParser.java | 29 +++++++++++--- .../protoparser/ResourceReferenceParser.java | 1 + 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index c8485ad4f2..340716d3de 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java @@ -230,6 +230,7 @@ public static Map updateResourceNamesInMessages( } public static Map parseResourceNames(CodeGeneratorRequest request) { + @VisibleForTesting String javaPackage = parseServiceJavaPackage(request); Map fileDescriptors = getFilesToGenerate(request); Map resourceNames = new HashMap<>(); for (String fileToGenerate : request.getFileToGenerateList()) { @@ -238,14 +239,19 @@ public static Map parseResourceNames(CodeGeneratorRequest fileDescriptors.get(fileToGenerate), "Missing file descriptor for [%s]", fileToGenerate); - resourceNames.putAll(parseResourceNames(fileDescriptor)); + resourceNames.putAll(parseResourceNames(fileDescriptor, javaPackage)); } return resourceNames; } - // Convenience wrapper for package-external unit tests. + // Convenience wrapper for package-external unit tests. DO NOT ADD NEW FUNCTIONALITY HERE. public static Map parseResourceNames(FileDescriptor fileDescriptor) { - return ResourceNameParser.parseResourceNames(fileDescriptor); + return parseResourceNames(fileDescriptor, TypeParser.getPackage(fileDescriptor)); + } + + public static Map parseResourceNames( + FileDescriptor fileDescriptor, String javaPackage) { + return ResourceNameParser.parseResourceNames(fileDescriptor, javaPackage); } @VisibleForTesting @@ -428,4 +434,32 @@ private static Map getFilesToGenerate(CodeGeneratorReque } return fileDescriptors; } + + private static String parseServiceJavaPackage(CodeGeneratorRequest request) { + Map javaPackageCount = new HashMap<>(); + Map fileDescriptors = getFilesToGenerate(request); + for (String fileToGenerate : request.getFileToGenerateList()) { + FileDescriptor fileDescriptor = + Preconditions.checkNotNull( + fileDescriptors.get(fileToGenerate), + "Missing file descriptor for [%s]", + fileToGenerate); + + String javaPackage = fileDescriptor.getOptions().getJavaPackage(); + if (Strings.isNullOrEmpty(javaPackage)) { + continue; + } + if (javaPackageCount.containsKey(javaPackage)) { + javaPackageCount.put(javaPackage, javaPackageCount.get(javaPackage) + 1); + } else { + javaPackageCount.put(javaPackage, 1); + } + } + + String finalJavaPackage = + javaPackageCount.entrySet().stream().max(Map.Entry.comparingByValue()).get().getKey(); + Preconditions.checkState( + !Strings.isNullOrEmpty(finalJavaPackage), "No service Java package found"); + return finalJavaPackage; + } } diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceNameParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceNameParser.java index b3e042752a..4a66becd4e 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceNameParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceNameParser.java @@ -35,15 +35,35 @@ public class ResourceNameParser { /** Returns a map of resource types (strings) to ResourceName POJOs. */ - public static Map parseResourceNames(FileDescriptor fileDescriptor) { - Map resourceNames = parseResourceNamesFromFile(fileDescriptor); - String pakkage = TypeParser.getPackage(fileDescriptor); + public static Map parseResourceNames( + FileDescriptor fileDescriptor, String javaPackage) { + Map resourceNames = + parseResourceNamesFromFile(fileDescriptor, javaPackage); + String pakkage = fileDescriptor.getOptions().getJavaPackage(); + if (Strings.isNullOrEmpty(pakkage)) { + pakkage = javaPackage; + } resourceNames.putAll(parseResourceNamesFromMessages(fileDescriptor.getMessageTypes(), pakkage)); return resourceNames; } + // Convenience wrapper for uni tests. DO NOT ADD ANY NEW FUNCTIONALITY HERE. + @VisibleForTesting + public static Map parseResourceNames(FileDescriptor fileDescriptor) { + String pakkage = TypeParser.getPackage(fileDescriptor); + return parseResourceNames(fileDescriptor, pakkage); + } + + // Convenience wrapper for uni tests. DO NOT ADD ANY NEW FUNCTIONALITY HERE. @VisibleForTesting static Map parseResourceNamesFromFile(FileDescriptor fileDescriptor) { + String pakkage = TypeParser.getPackage(fileDescriptor); + return parseResourceNamesFromFile(fileDescriptor, pakkage); + } + + @VisibleForTesting + static Map parseResourceNamesFromFile( + FileDescriptor fileDescriptor, String javaPackage) { Map typeStringToResourceNames = new HashMap<>(); FileOptions fileOptions = fileDescriptor.getOptions(); if (fileOptions.getExtensionCount(ResourceProto.resourceDefinition) <= 0) { @@ -51,9 +71,8 @@ static Map parseResourceNamesFromFile(FileDescriptor fileD } List protoResources = fileOptions.getExtension(ResourceProto.resourceDefinition); - String pakkage = TypeParser.getPackage(fileDescriptor); for (ResourceDescriptor protoResource : protoResources) { - Optional resourceNameModelOpt = createResourceName(protoResource, pakkage); + Optional resourceNameModelOpt = createResourceName(protoResource, javaPackage); if (!resourceNameModelOpt.isPresent()) { continue; } diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java index a8731deea2..7970598d34 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/ResourceReferenceParser.java @@ -141,6 +141,7 @@ static Optional parseParentResourceName( "%s/%s", resourceTypeString.substring(0, resourceTypeString.indexOf(SLASH)), JavaStyle.toUpperCamelCase(parentVariableName)); + ResourceName parentResourceName = ResourceName.builder() .setVariableName(parentVariableName) From 5eed26a8d8918d0c768f36ae67f6fb5d91815444 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 16:25:51 -0700 Subject: [PATCH 20/22] fix: build resnames separately and extract into proto/ dir --- rules_java_gapic/java_gapic.bzl | 26 ++++++++++++++++++++++---- rules_java_gapic/java_gapic_pkg.bzl | 5 +++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/rules_java_gapic/java_gapic.bzl b/rules_java_gapic/java_gapic.bzl index 3a4c494113..3f147a9179 100644 --- a/rules_java_gapic/java_gapic.bzl +++ b/rules_java_gapic/java_gapic.bzl @@ -19,6 +19,7 @@ def _java_gapic_postprocess_srcjar_impl(ctx): output_srcjar_name = ctx.label.name output_main = ctx.outputs.main output_test = ctx.outputs.test + output_resource_name = ctx.outputs.resource_name formatter = ctx.executable.formatter output_dir_name = ctx.label.name @@ -30,17 +31,20 @@ def _java_gapic_postprocess_srcjar_impl(ctx): unzip -q temp-codegen.srcjar -d {output_dir_path} # This may fail if there are spaces and/or too many files (exceed max length of command length). {formatter} --replace $(find {output_dir_path} -type f -printf "%p ") - zip -r -j {output_srcjar_name}.srcjar {output_dir_path}/src/main/* {output_dir_path}/proto/src/main/* + zip -r -j {output_srcjar_name}.srcjar {output_dir_path}/src/main/* + zip -r -j {output_srcjar_name}-resource-name.srcjar {output_dir_path}/proto/src/main/* zip -r -j {output_srcjar_name}-tests.srcjar {output_dir_path}/src/test/* mv {output_srcjar_name}.srcjar {output_main} + mv {output_srcjar_name}-resource-name.srcjar {output_resource_name} mv {output_srcjar_name}-tests.srcjar {output_test} """.format( gapic_srcjar = gapic_srcjar.path, output_srcjar_name = output_srcjar_name, - output_main = output_main.path, formatter = formatter, output_dir_name = output_dir_name, output_dir_path = output_dir_path, + output_main = output_main.path, + output_resource_name = output_resource_name.path, output_test = output_test.path, ) @@ -48,7 +52,7 @@ def _java_gapic_postprocess_srcjar_impl(ctx): inputs = [gapic_srcjar], tools = [formatter], command = script, - outputs = [output_main, output_test], + outputs = [output_main, output_resource_name, output_test], ) _java_gapic_postprocess_srcjar = rule( @@ -62,6 +66,7 @@ _java_gapic_postprocess_srcjar = rule( }, outputs = { "main": "%{name}.srcjar", + "resource_name": "%{name}-resource-name.srcjar", "test": "%{name}-test.srcjar", }, implementation = _java_gapic_postprocess_srcjar_impl, @@ -111,8 +116,21 @@ def java_gapic_library( **kwargs ) + resource_name_name = "%s_resource_name" % name + resource_name_deps = [resource_name_name] + native.java_library( + name = resource_name_name, + srcs = ["%s-resource-name.srcjar" % srcjar_name], + deps = [ + "@com_google_api_api_common//jar", + "@com_google_guava_guava//jar", + "@javax_annotation_javax_annotation_api//jar", + ], + **kwargs + ) + # General additional deps. - actual_deps = deps + [ + actual_deps = deps + resource_name_deps + [ "@com_google_googleapis//google/rpc:rpc_java_proto", "@com_google_googleapis//google/longrunning:longrunning_java_proto", "@com_google_protobuf//:protobuf_java", diff --git a/rules_java_gapic/java_gapic_pkg.bzl b/rules_java_gapic/java_gapic_pkg.bzl index 51da072f5d..20e9127b3f 100644 --- a/rules_java_gapic/java_gapic_pkg.bzl +++ b/rules_java_gapic/java_gapic_pkg.bzl @@ -316,11 +316,16 @@ def java_gapic_assembly_gradle_pkg( proto_deps = [] processed_deps = {} #there is no proper Set in Starlark + print("DEL: ALL DEPS: ", deps) for dep in deps: + print("DEL: Saw dep ", dep) + # Use contains instead of endswith since microgenerator testing may use differently-named targets. if "_java_gapic" in dep: + print("DEL: Doing java_gapic, dep: ", dep) _put_dep_in_a_bucket(dep, client_deps, processed_deps) _put_dep_in_a_bucket("%s_test" % dep, client_test_deps, processed_deps) + _put_dep_in_a_bucket("%s_resource_name" % dep, proto_deps, processed_deps) elif dep.endswith("_java_grpc"): _put_dep_in_a_bucket(dep, grpc_deps, processed_deps) else: From 3b238ff284de59847eb5e379ef972c79f5464b4f Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 16:27:27 -0700 Subject: [PATCH 21/22] fix: remove debug printf --- rules_java_gapic/java_gapic_pkg.bzl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rules_java_gapic/java_gapic_pkg.bzl b/rules_java_gapic/java_gapic_pkg.bzl index 20e9127b3f..ad9cb74bcc 100644 --- a/rules_java_gapic/java_gapic_pkg.bzl +++ b/rules_java_gapic/java_gapic_pkg.bzl @@ -316,13 +316,9 @@ def java_gapic_assembly_gradle_pkg( proto_deps = [] processed_deps = {} #there is no proper Set in Starlark - print("DEL: ALL DEPS: ", deps) for dep in deps: - print("DEL: Saw dep ", dep) - # Use contains instead of endswith since microgenerator testing may use differently-named targets. if "_java_gapic" in dep: - print("DEL: Doing java_gapic, dep: ", dep) _put_dep_in_a_bucket(dep, client_deps, processed_deps) _put_dep_in_a_bucket("%s_test" % dep, client_test_deps, processed_deps) _put_dep_in_a_bucket("%s_resource_name" % dep, proto_deps, processed_deps) From 4563c4ae153da6c12505f80b748a5f450b0f9772 Mon Sep 17 00:00:00 2001 From: Mira Leung Date: Fri, 18 Sep 2020 19:01:05 -0700 Subject: [PATCH 22/22] fix: empty change to force re-run