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..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 @@ -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,31 @@ 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. + 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 74eaa5c6bd..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 @@ -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)) @@ -471,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/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/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/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/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 new file mode 100644 index 0000000000..bfa4c62297 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParser.java @@ -0,0 +1,301 @@ +// 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.api.generator.gapic.model.SourceCodeInfoLocation; +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 SourceCodeInfoLocation getLocation(Descriptor message) { + FileDescriptor file = message.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(message))); + } + + /** Gets the location of a field, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(FieldDescriptor field) { + FileDescriptor file = field.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(field))); + } + + /** Gets the location of a service, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(ServiceDescriptor service) { + FileDescriptor file = service.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(service))); + } + + /** Gets the location of a method, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(MethodDescriptor method) { + FileDescriptor file = method.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(method))); + } + + /** Gets the location of an enum type, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(EnumDescriptor enumType) { + FileDescriptor file = enumType.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumType))); + } + + /** Gets the location of an enum value, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(EnumValueDescriptor enumValue) { + FileDescriptor file = enumValue.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumValue))); + } + + /** Gets the location of a oneof, if available. */ + @Nullable + public SourceCodeInfoLocation getLocation(OneofDescriptor oneof) { + FileDescriptor file = oneof.getFile(); + if (!file.toProto().hasSourceCodeInfo()) { + return null; + } + return SourceCodeInfoLocation.create(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/engine/ast/TypeNodeTest.java b/src/test/java/com/google/api/generator/engine/ast/TypeNodeTest.java index 5e559db5a7..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 @@ -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,36 @@ 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.STRING)).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 b18cdbe0ea..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 @@ -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" @@ -154,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" @@ -164,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" @@ -188,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); + } +} 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..b8daabefeb --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/SourceCodeInfoParserTest.java @@ -0,0 +1,163 @@ +// 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.api.generator.gapic.model.SourceCodeInfoLocation; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +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() { + SourceCodeInfoLocation location = parser.getLocation(protoFile.findServiceByName("FooService")); + assertEquals( + "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.", location.getLeadingComments()); + } + + @Test + public void getMethodInfo() { + ServiceDescriptor service = protoFile.findServiceByName("FooService"); + SourceCodeInfoLocation location = parser.getLocation(service.findMethodByName("FooMethod")); + assertEquals( + "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.", location.getLeadingComments()); + } + + @Test + public void getOuterMessageInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + SourceCodeInfoLocation location = parser.getLocation(message); + assertEquals( + "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. And here is the second line of that" + + " description.", + location.getLeadingComments()); + assertEquals("A field trailing comment.", location.getTrailingComments()); + + location = parser.getLocation(message.findFieldByName("field_two")); + assertEquals("This is another field description.", location.getLeadingComments()); + assertEquals("Another field trailing comment.", location.getTrailingComments()); + } + + @Test + public void getInnerMessageInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + assertThat(message).isNotNull(); + message = message.findNestedTypeByName("BarMessage"); + + SourceCodeInfoLocation location = parser.getLocation(message); + assertEquals( + "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.", location.getLeadingComments()); + + location = parser.getLocation(message.findFieldByName("field_two")); + assertEquals("This is a block comment for field_two.", location.getLeadingComments()); + } + + @Test + public void getOuterEnumInfo() { + EnumDescriptor protoEnum = protoFile.findEnumTypeByName("OuterEnum"); + 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.", location.getLeadingComments()); + } + + @Test + public void getInnerEnumInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + EnumDescriptor protoEnum = message.findEnumTypeByName("FoodEnum"); + SourceCodeInfoLocation location = parser.getLocation(protoEnum); + assertEquals("An inner enum.", location.getLeadingComments()); + + // Enum fields. + location = parser.getLocation(protoEnum.findValueByName("RICE")); + assertEquals("😋 🍚.", location.getLeadingComments()); + location = parser.getLocation(protoEnum.findValueByName("CHOCOLATE")); + assertEquals("🤤 🍫.", location.getLeadingComments()); + } + + @Test + public void getOnoeofInfo() { + Descriptor message = protoFile.findMessageTypeByName("FooMessage"); + OneofDescriptor protoOneof = message.getOneofs().get(0); + SourceCodeInfoLocation location = parser.getLocation(protoOneof); + assertEquals("An inner oneof.", location.getLeadingComments()); + + location = parser.getLocation(protoOneof.getField(0)); + assertEquals("An InnerOneof comment for its field.", 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; +}