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/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/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" 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..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 @@ -10,6 +10,21 @@ filegroup( srcs = glob(["*_gapic.yaml"]), ) +genrule( + name = "basic_proto_descriptor", + srcs = [ + "basic.proto", + ], + outs = ["basic_proto.descriptor"], + # 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( 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; +}