diff --git a/src/main/java/com/google/api/generator/gapic/model/BUILD.bazel b/src/main/java/com/google/api/generator/gapic/model/BUILD.bazel index aa4d4014b4..2460f46eed 100644 --- a/src/main/java/com/google/api/generator/gapic/model/BUILD.bazel +++ b/src/main/java/com/google/api/generator/gapic/model/BUILD.bazel @@ -13,6 +13,7 @@ java_library( deps = [ "//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/utils", "@com_google_auto_value_auto_value//jar", "@com_google_auto_value_auto_value_annotations//jar", "@com_google_code_findbugs_jsr305//jar", 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 6921edaa0c..1a27957f17 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 @@ -29,8 +29,13 @@ public abstract class MethodArgument { // appeared as the last element). public abstract ImmutableList nestedTypes(); + // Returns true if this is a resource name helper tyep. + public abstract boolean isResourceNameHelper(); + public static Builder builder() { - return new AutoValue_MethodArgument.Builder().setNestedTypes(ImmutableList.of()); + return new AutoValue_MethodArgument.Builder() + .setNestedTypes(ImmutableList.of()) + .setIsResourceNameHelper(false); } @AutoValue.Builder @@ -41,6 +46,8 @@ public abstract static class Builder { public abstract Builder setNestedTypes(List nestedTypes); + public abstract Builder setIsResourceNameHelper(boolean isResourceNameHelper); + 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 0b4112e87e..293ed024a1 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 @@ -16,6 +16,8 @@ import com.google.api.generator.engine.ast.TypeNode; import com.google.api.generator.engine.ast.VaporReference; +import com.google.api.generator.gapic.utils.JavaStyle; +import com.google.api.generator.gapic.utils.ResourceNameConstants; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.util.List; @@ -45,6 +47,8 @@ public abstract class ResourceName { // The Java TypeNode of the resource name helper class to generate. public abstract TypeNode type(); + public abstract boolean isOnlyWildcard(); + // The message in which this resource was defined. Optional. // This is expected to be empty for file-level definitions. @Nullable @@ -55,7 +59,27 @@ public boolean hasParentMessageName() { } public static Builder builder() { - return new AutoValue_ResourceName.Builder(); + + return new AutoValue_ResourceName.Builder().setIsOnlyWildcard(false); + } + + public static ResourceName createWildcard(String resourceTypeString, String pakkage) { + String placeholderVarName = + JavaStyle.toLowerCamelCase( + resourceTypeString.substring(resourceTypeString.indexOf(SLASH) + 1)); + return builder() + .setVariableName(placeholderVarName) + .setPakkage(pakkage) + .setResourceTypeString(resourceTypeString) + .setPatterns(ImmutableList.of(ResourceNameConstants.WILDCARD_PATTERN)) + .setIsOnlyWildcard(true) + .setType( + TypeNode.withReference( + VaporReference.builder() + .setName("ResourceName") + .setPakkage("com.google.api.resourcenames") + .build())) + .build(); } @Override @@ -98,22 +122,32 @@ public abstract static class Builder { public abstract Builder setParentMessageName(String parentMessageName); - // Private setter. + // Private setters. abstract Builder setType(TypeNode type); + abstract Builder setIsOnlyWildcard(boolean isOnlyWildcard); + // Private accessors. abstract String pakkage(); abstract String resourceTypeString(); + abstract boolean isOnlyWildcard(); + // Private. abstract ResourceName autoBuild(); public ResourceName build() { - String typeName = resourceTypeString().substring(resourceTypeString().lastIndexOf(SLASH) + 1); - setType( - TypeNode.withReference( - VaporReference.builder().setName(typeName).setPakkage(pakkage()).build())); + if (!isOnlyWildcard()) { + String typeName = + resourceTypeString().substring(resourceTypeString().lastIndexOf(SLASH) + 1); + setType( + TypeNode.withReference( + VaporReference.builder() + .setName(String.format("%sName", typeName)) + .setPakkage(pakkage()) + .build())); + } return autoBuild(); } } 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 5147fccc72..d7d20e5265 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 @@ -20,7 +20,9 @@ import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.MethodArgument; import com.google.api.generator.gapic.model.ResourceName; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import com.google.protobuf.Descriptors.MethodDescriptor; import java.util.ArrayList; import java.util.Arrays; @@ -56,40 +58,89 @@ public static List> parseMethodSignatures( Message inputMessage = messageTypes.get(methodInputTypeName); // Example from Expand in echo.proto: - // Input: ["content,error", "content,error,info"]. - // Output: [["content", "error"], ["content", "error", "info"]]. + // stringSigs: ["content,error", "content,error,info"]. for (String stringSig : stringSigs) { - List arguments = new ArrayList<>(); - for (String argumentName : stringSig.split(METHOD_SIGNATURE_DELIMITER)) { - List argumentTypePath = - new ArrayList<>( - parseTypeFromArgumentName( - argumentName, - servicePackage, - inputMessage, - messageTypes, - resourceNames, - patternsToResourceNames, - outputArgResourceNames)); + List argumentNames = new ArrayList<>(); + Map> argumentNameToOverloads = new HashMap<>(); + // stringSig.split: ["content", "error"]. + for (String argumentName : stringSig.split(METHOD_SIGNATURE_DELIMITER)) { + // 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( + argumentName, + servicePackage, + inputMessage, + messageTypes, + resourceNames, + patternsToResourceNames, + argumentTypePathAcc, + outputArgResourceNames); int dotLastIndex = argumentName.lastIndexOf(DOT); String actualArgumentName = dotLastIndex < 0 ? argumentName : argumentName.substring(dotLastIndex + 1); + argumentNames.add(actualArgumentName); + argumentNameToOverloads.put( + actualArgumentName, + argumentTypes.stream() + .map( + type -> + MethodArgument.builder() + .setName(actualArgumentName) + .setType(type) + .setIsResourceNameHelper( + argumentTypes.size() > 1 && !type.equals(TypeNode.STRING)) + .setNestedTypes(argumentTypePathAcc) + .build()) + .collect(Collectors.toList())); + } + signatures.addAll(flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads)); + } + return signatures; + } - int typeLastIndex = argumentTypePath.size() - 1; - TypeNode argumentType = argumentTypePath.get(typeLastIndex); - argumentTypePath.remove(typeLastIndex); + @VisibleForTesting + static List> flattenMethodSignatureVariants( + List argumentNames, Map> argumentNameToOverloads) { + Preconditions.checkState( + argumentNames.size() == argumentNameToOverloads.size(), + String.format( + "Cardinality of argument names %s do not match that of overloaded types %s", + argumentNames, argumentNameToOverloads)); + for (String name : argumentNames) { + Preconditions.checkNotNull( + argumentNameToOverloads.get(name), + String.format("No corresponding overload types found for argument %s", name)); + } + return flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads, 0); + } - arguments.add( - MethodArgument.builder() - .setName(actualArgumentName) - .setType(argumentType) - .setNestedTypes(argumentTypePath) - .build()); + private static List> flattenMethodSignatureVariants( + List argumentNames, + Map> argumentNameToOverloads, + int depth) { + List> methodArgs = new ArrayList<>(); + if (depth >= argumentNames.size() - 1) { + for (MethodArgument methodArg : argumentNameToOverloads.get(argumentNames.get(depth))) { + methodArgs.add(Lists.newArrayList(methodArg)); } - signatures.add(arguments); + return methodArgs; } - return signatures; + + List> subsequentArgs = + flattenMethodSignatureVariants(argumentNames, argumentNameToOverloads, depth + 1); + for (MethodArgument methodArg : argumentNameToOverloads.get(argumentNames.get(depth))) { + for (List subsequentArg : subsequentArgs) { + // Use a new list to avoid appending all subsequent elements (in upcoming loop iterations) + // to the same list. + List appendedArgs = new ArrayList<>(subsequentArg); + appendedArgs.add(0, methodArg); + methodArgs.add(appendedArgs); + } + } + return methodArgs; } private static List parseTypeFromArgumentName( @@ -99,35 +150,33 @@ private static List parseTypeFromArgumentName( Map messageTypes, Map resourceNames, Map patternsToResourceNames, + List argumentTypePathAcc, Set outputArgResourceNames) { - return parseTypeFromArgumentName( - argumentName, - servicePackage, - inputMessage, - messageTypes, - resourceNames, - patternsToResourceNames, - outputArgResourceNames, - new ArrayList<>()); - } - private static List parseTypeFromArgumentName( - String argumentName, - String servicePackage, - Message inputMessage, - Map messageTypes, - Map resourceNames, - Map patternsToResourceNames, - Set outputArgResourceNames, - List typeAcc) { int dotIndex = argumentName.indexOf(DOT); - // TODO(miraleung): Fake out resource names here. if (dotIndex < 1) { Field field = inputMessage.fieldMap().get(argumentName); Preconditions.checkNotNull( - field, String.format("Field %s not found, %s", argumentName, inputMessage.fieldMap())); - return Arrays.asList(field.type()); + field, + String.format( + "Field %s not found from input message %s values %s", + argumentName, inputMessage.name(), inputMessage.fieldMap().keySet())); + if (!field.hasResourceReference()) { + return Arrays.asList(field.type()); + } + + // Parse the resource name tyeps. + List resourceNameArgs = + ResourceReferenceParser.parseResourceNames( + field.resourceReference(), servicePackage, 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; } + Preconditions.checkState( dotIndex < argumentName.length() - 1, String.format( @@ -137,16 +186,6 @@ private static List parseTypeFromArgumentName( // Must be a sub-message for a type's subfield to be valid. Field firstField = inputMessage.fieldMap().get(firstFieldName); - if (firstField.hasResourceReference()) { - List resourceNameArgs = - ResourceReferenceParser.parseResourceNames( - firstField.resourceReference(), - servicePackage, - resourceNames, - patternsToResourceNames); - outputArgResourceNames.addAll(resourceNameArgs); - return resourceNameArgs.stream().map(r -> r.type()).collect(Collectors.toList()); - } Preconditions.checkState( !firstField.isRepeated(), @@ -164,8 +203,7 @@ private static List parseTypeFromArgumentName( String.format( "Message type %s for field reference %s invalid", firstFieldTypeName, firstFieldName)); - List newAcc = new ArrayList<>(typeAcc); - newAcc.add(firstFieldType); + argumentTypePathAcc.add(firstFieldType); return parseTypeFromArgumentName( remainingArgumentName, servicePackage, @@ -173,8 +211,8 @@ private static List parseTypeFromArgumentName( messageTypes, resourceNames, patternsToResourceNames, - outputArgResourceNames, - newAcc); + argumentTypePathAcc, + outputArgResourceNames); } private static Map createPatternResourceNameMap( 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 e2bc978c0b..b8ac38d2b8 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 @@ -116,7 +116,7 @@ private static Optional createResourceName( protoResource.getType())); if (patterns.size() == 1 && patterns.get(0).equals(ResourceNameConstants.WILDCARD_PATTERN)) { - return Optional.empty(); + return Optional.of(ResourceName.createWildcard(protoResource.getType(), pakkage)); } // Assuming that both patterns end with the same variable name. 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 49abdb6081..5145194d0d 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 @@ -21,6 +21,7 @@ import com.google.api.pathtemplate.PathTemplate; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,6 +30,9 @@ import java.util.Set; public class ResourceReferenceParser { + private static final String EMPTY_STRING = ""; + private static final String LEFT_BRACE = "{"; + private static final String RIGHT_BRACE = "}"; private static final String SLASH = "/"; public static List parseResourceNames( @@ -42,7 +46,7 @@ public static List parseResourceNames( String.format( "No resource definition found for reference with type %s", resourceReference.resourceTypeString())); - if (!resourceReference.isChildType()) { + if (!resourceReference.isChildType() || resourceName.isOnlyWildcard()) { return Arrays.asList(resourceName); } @@ -81,14 +85,39 @@ static Optional parseParentResourceName( } String[] tokens = parentPattern.split(SLASH); - String lastToken = tokens[tokens.length - 1]; + int numTokens = tokens.length; + String lastToken = tokens[numTokens - 1]; Set variableNames = PathTemplate.create(parentPattern).vars(); String parentVariableName = null; + // Try the extracting from the conventional pattern first. + // E.g. Profile is the parent of users/{user}/profiles/{profile}/blurbs/{blurb}. for (String variableName : variableNames) { if (lastToken.contains(variableName)) { parentVariableName = variableName; } } + + // TODO(miraleung): Add unit tests that exercise these edge cases. + // Check unconventional patterns. + // Assume that non-slash separators will only ever appear in the last component of a patetrn. + // That is, they will not appear in the parent components under consideration. + if (Strings.isNullOrEmpty(parentVariableName)) { + String lowerTypeName = + resourceTypeString.substring(resourceTypeString.indexOf(SLASH) + 1).toLowerCase(); + // Check for the parent of users/{user}/profile/blurbs/legacy/{legacy_user}~{blurb}. + // We're curerntly at users/{user}/profile/blurbs. + if ((lastToken.endsWith("s") || lastToken.contains(lowerTypeName)) && numTokens > 2) { + // Not the singleton we're looking for, back up. + parentVariableName = tokens[numTokens - 2]; + } else { + // Check for the parent of users/{user}/profile/blurbs/{blurb}. + // We're curerntly at users/{user}/profile. + parentVariableName = lastToken; + } + parentVariableName = + parentVariableName.replace(LEFT_BRACE, EMPTY_STRING).replace(RIGHT_BRACE, EMPTY_STRING); + } + Preconditions.checkNotNull( parentVariableName, String.format("Could not parse variable name from pattern %s", parentPattern)); 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 c7d1e4e273..f0480808ae 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 @@ -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.api.resourcenames.ResourceName;\n" + "import com.google.longrunning.Operation;\n" + "import com.google.longrunning.OperationsClient;\n" + "import com.google.rpc.Status;\n" @@ -117,6 +118,26 @@ public void generateServiceClasses() { + " return echo(request);\n" + " }\n" + "\n" + + " public final EchoResponse echo(String name) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + + " return echo(request);\n" + + " }\n" + + "\n" + + " public final EchoResponse echo(FoobarName name) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setName(name).build();\n" + + " return echo(request);\n" + + " }\n" + + "\n" + + " public final EchoResponse echo(String parent) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setParent(parent).build();\n" + + " return echo(request);\n" + + " }\n" + + "\n" + + " public final EchoResponse echo(ResourceName parent) {\n" + + " EchoRequest request = EchoRequest.newBuilder().setParent(parent).build();\n" + + " return echo(request);\n" + + " }\n" + + "\n" + " public final EchoResponse echo(EchoRequest request) {\n" + " return echoCallable().call(request);\n" + " }\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 3694d5f68d..72b74eae47 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 @@ -1,6 +1,7 @@ package(default_visibility = ["//visibility:public"]) TESTS = [ + "MethodSignatureParserTest", "ParserTest", "ResourceNameParserTest", "ResourceReferenceParserTest", diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/MethodSignatureParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/MethodSignatureParserTest.java new file mode 100644 index 0000000000..b8ccb29d08 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/MethodSignatureParserTest.java @@ -0,0 +1,265 @@ +// 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 static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.VaporReference; +import com.google.api.generator.gapic.model.MethodArgument; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import com.google.testgapic.v1beta1.LockerProto; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; + +public class MethodSignatureParserTest { + private static final String MAIN_PACKAGE = "com.google.testgapic.v1beta1"; + + private ServiceDescriptor lockerService; + private FileDescriptor lockerServiceFileDescriptor; + + @Before + public void setUp() { + lockerServiceFileDescriptor = LockerProto.getDescriptor(); + lockerService = lockerServiceFileDescriptor.getServices().get(0); + } + + @Test + public void flattenMethodSignatures_basic() { + String fooName = "fooName"; + TypeNode fooTypeOne = + TypeNode.withReference( + VaporReference.builder().setName("FooName").setPakkage("com.google.foobar").build()); + TypeNode fooTypeTwo = + TypeNode.withReference( + VaporReference.builder().setName("FooTwoName").setPakkage("com.google.foobar").build()); + + List argumentNames = Arrays.asList(fooName); + + BiFunction methodArgFn = + (name, type) -> MethodArgument.builder().setName(name).setType(type).build(); + List fooArgs = + Arrays.asList(TypeNode.STRING, fooTypeOne, fooTypeTwo).stream() + .map(t -> methodArgFn.apply(fooName, t)) + .collect(Collectors.toList()); + Map> argumentNameToOverloads = new HashMap<>(); + argumentNameToOverloads.put(fooName, fooArgs); + + List> flattenedSignatures = + MethodSignatureParser.flattenMethodSignatureVariants( + argumentNames, argumentNameToOverloads); + + assertEquals(3, flattenedSignatures.size()); + + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(TypeNode.STRING))); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(fooTypeOne))); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(fooTypeTwo))); + } + + @Test + public void flattenMethodSignatures_oneToMany() { + String fooName = "fooName"; + String anInt = "anInt"; + + TypeNode fooTypeOne = + TypeNode.withReference( + VaporReference.builder().setName("FooName").setPakkage("com.google.foobar").build()); + TypeNode fooTypeTwo = + TypeNode.withReference( + VaporReference.builder().setName("FooTwoName").setPakkage("com.google.foobar").build()); + + List argumentNames = Arrays.asList(anInt, fooName); + + BiFunction methodArgFn = + (name, type) -> MethodArgument.builder().setName(name).setType(type).build(); + List fooArgs = + Arrays.asList(TypeNode.STRING, fooTypeOne, fooTypeTwo).stream() + .map(t -> methodArgFn.apply(fooName, t)) + .collect(Collectors.toList()); + Map> argumentNameToOverloads = new HashMap<>(); + argumentNameToOverloads.put(fooName, fooArgs); + argumentNameToOverloads.put(anInt, Arrays.asList(methodArgFn.apply(anInt, TypeNode.INT))); + + List> flattenedSignatures = + MethodSignatureParser.flattenMethodSignatureVariants( + argumentNames, argumentNameToOverloads); + + assertEquals(3, flattenedSignatures.size()); + + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(TypeNode.INT, TypeNode.STRING))); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(TypeNode.INT, fooTypeOne))); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(TypeNode.INT, fooTypeTwo))); + } + + @Test + public void flattenMethodSignatures_manyToOne() { + String fooName = "fooName"; + String anInt = "anInt"; + + TypeNode fooTypeOne = + TypeNode.withReference( + VaporReference.builder().setName("FooName").setPakkage("com.google.foobar").build()); + TypeNode fooTypeTwo = + TypeNode.withReference( + VaporReference.builder().setName("FooTwoName").setPakkage("com.google.foobar").build()); + + List argumentNames = Arrays.asList(fooName, anInt); + + BiFunction methodArgFn = + (name, type) -> MethodArgument.builder().setName(name).setType(type).build(); + List fooArgs = + Arrays.asList(TypeNode.STRING, fooTypeOne, fooTypeTwo).stream() + .map(t -> methodArgFn.apply(fooName, t)) + .collect(Collectors.toList()); + Map> argumentNameToOverloads = new HashMap<>(); + argumentNameToOverloads.put(fooName, fooArgs); + argumentNameToOverloads.put(anInt, Arrays.asList(methodArgFn.apply(anInt, TypeNode.INT))); + + List> flattenedSignatures = + MethodSignatureParser.flattenMethodSignatureVariants( + argumentNames, argumentNameToOverloads); + + assertEquals(3, flattenedSignatures.size()); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(TypeNode.STRING, TypeNode.INT))); + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(fooTypeOne, TypeNode.INT))); + + assertTrue(containsTypes(flattenedSignatures, Arrays.asList(fooTypeTwo, TypeNode.INT))); + } + + @Test + public void flattenMethodSignatures_manyToMany() { + String fooName = "fooName"; + String barName = "barName"; + String anInt = "anInt"; + String anotherInt = "anotherInt"; + + TypeNode fooTypeOne = + TypeNode.withReference( + VaporReference.builder().setName("FooName").setPakkage("com.google.foobar").build()); + TypeNode fooTypeTwo = + TypeNode.withReference( + VaporReference.builder().setName("FooTwoName").setPakkage("com.google.foobar").build()); + TypeNode barTypeOne = + TypeNode.withReference( + VaporReference.builder().setName("BarName").setPakkage("com.google.foobar").build()); + TypeNode barTypeTwo = + TypeNode.withReference( + VaporReference.builder().setName("BarTwoName").setPakkage("com.google.foobar").build()); + TypeNode barTypeThree = + TypeNode.withReference( + VaporReference.builder().setName("BarCarName").setPakkage("com.google.foobar").build()); + + List argumentNames = Arrays.asList(fooName, anInt, barName, anotherInt); + + BiFunction methodArgFn = + (name, type) -> MethodArgument.builder().setName(name).setType(type).build(); + List fooArgs = + Arrays.asList(TypeNode.STRING, fooTypeOne, fooTypeTwo).stream() + .map(t -> methodArgFn.apply(fooName, t)) + .collect(Collectors.toList()); + List barArgs = + Arrays.asList(TypeNode.STRING, barTypeOne, barTypeTwo, barTypeThree).stream() + .map(t -> methodArgFn.apply(barName, t)) + .collect(Collectors.toList()); + Map> argumentNameToOverloads = new HashMap<>(); + argumentNameToOverloads.put(fooName, fooArgs); + argumentNameToOverloads.put(anInt, Arrays.asList(methodArgFn.apply(anInt, TypeNode.INT))); + argumentNameToOverloads.put(barName, barArgs); + argumentNameToOverloads.put( + anotherInt, Arrays.asList(methodArgFn.apply(anotherInt, TypeNode.INT))); + + List> flattenedSignatures = + MethodSignatureParser.flattenMethodSignatureVariants( + argumentNames, argumentNameToOverloads); + + assertEquals(12, flattenedSignatures.size()); + + // Types 0 - 4: String, int, {String, BarName, BarTwoName, BarCarName}, int. + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(TypeNode.STRING, TypeNode.INT, TypeNode.STRING, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(TypeNode.STRING, TypeNode.INT, barTypeOne, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(TypeNode.STRING, TypeNode.INT, barTypeTwo, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(TypeNode.STRING, TypeNode.INT, barTypeThree, TypeNode.INT))); + + // Types 5 - 8: FooName, int, {String, BarName, BarTwoName, BarCarName}, int. + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeOne, TypeNode.INT, TypeNode.STRING, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeOne, TypeNode.INT, barTypeOne, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeOne, TypeNode.INT, barTypeTwo, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeOne, TypeNode.INT, barTypeThree, TypeNode.INT))); + + // Types 9 - 12: FooTwoName, int, {String, BarName, BarTwoName, BarCarName}, int. + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeTwo, TypeNode.INT, TypeNode.STRING, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeTwo, TypeNode.INT, barTypeOne, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeTwo, TypeNode.INT, barTypeTwo, TypeNode.INT))); + assertTrue( + containsTypes( + flattenedSignatures, + Arrays.asList(fooTypeTwo, TypeNode.INT, barTypeThree, TypeNode.INT))); + } + + private static boolean containsTypes( + List> flattenedSignatures, List types) { + // Brute-force search. Feel free to improve this if you've got cycles 🙃. + Function, List> typeExtractorFn = + methodArgs -> methodArgs.stream().map(m -> m.type()).collect(Collectors.toList()); + for (List args : flattenedSignatures) { + if (typeExtractorFn.apply(args).equals(types)) { + return true; + } + } + return false; + } +} diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java index 4f683cf8d7..374ccbb3e0 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java +++ b/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java @@ -60,6 +60,11 @@ public void setUp() { public void parseMessages_basic() { // TODO(miraleung): Add more tests for oneofs and other message-parsing edge cases. Map messageTypes = Parser.parseMessages(echoFileDescriptor); + + Message echoRequestMessage = messageTypes.get("EchoRequest"); + Field echoRequestNameField = echoRequestMessage.fieldMap().get("name"); + assertTrue(echoRequestNameField.hasResourceReference()); + String echoResponseName = "EchoResponse"; Field echoResponseContentField = Field.builder().setName("content").setType(TypeNode.STRING).build(); @@ -101,7 +106,7 @@ public void parseMethods_basic() { // Detailed method signature parsing tests are in a separate unit test. List> methodSignatures = echoMethod.methodSignatures(); - assertEquals(methodSignatures.size(), 3); + assertEquals(7, methodSignatures.size()); Method expandMethod = methods.get(1); assertEquals(expandMethod.name(), "Expand"); @@ -213,7 +218,7 @@ public void parseMethodSignatures_basic() { messageTypes, resourceNames, outputResourceNames); - assertEquals(methodSignatures.size(), 3); + assertEquals(7, methodSignatures.size()); // Signature contents: ["content"]. List methodArgs = methodSignatures.get(0); @@ -240,6 +245,21 @@ public void parseMethodSignatures_basic() { VaporReference.builder().setName("Severity").setPakkage(ECHO_PACKAGE).build()), ImmutableList.of(), argument); + + // Signature contents: ["name"], String variant. + methodArgs = methodSignatures.get(3); + assertEquals(1, methodArgs.size()); + argument = methodArgs.get(0); + assertMethodArgumentEquals("name", TypeNode.STRING, ImmutableList.of(), argument); + + // Signature contents: ["name"], resource helper variant. + methodArgs = methodSignatures.get(4); + assertEquals(1, methodArgs.size()); + argument = methodArgs.get(0); + TypeNode foobarNameType = + TypeNode.withReference( + VaporReference.builder().setName("FoobarName").setPakkage(ECHO_PACKAGE).build()); + assertMethodArgumentEquals("name", foobarNameType, ImmutableList.of(), argument); } @Test diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/ResourceNameParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/ResourceNameParserTest.java index af7d6b9139..4c07b1b6f0 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/ResourceNameParserTest.java +++ b/src/test/java/com/google/api/generator/gapic/protoparser/ResourceNameParserTest.java @@ -20,6 +20,8 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertThrows; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.VaporReference; import com.google.api.generator.gapic.model.ResourceName; import com.google.api.generator.gapic.utils.ResourceNameConstants; import com.google.protobuf.Descriptors.Descriptor; @@ -49,7 +51,7 @@ public void setUp() { public void parseResourceNames_basicOnePattern() { Map typeStringsToResourceNames = ResourceNameParser.parseResourceNamesFromFile(lockerServiceFileDescriptor); - assertEquals(3, typeStringsToResourceNames.size()); + assertEquals(4, typeStringsToResourceNames.size()); ResourceName resourceName = typeStringsToResourceNames.get("cloudbilling.googleapis.com/BillingAccount"); @@ -60,13 +62,14 @@ public void parseResourceNames_basicOnePattern() { assertEquals(MAIN_PACKAGE, resourceName.pakkage()); assertFalse(resourceName.hasParentMessageName()); assertThat(resourceName.parentMessageName()).isNull(); + assertFalse(resourceName.isOnlyWildcard()); } @Test public void parseResourceNames_basicTwoPatterns() { Map typeStringsToResourceNames = ResourceNameParser.parseResourceNamesFromFile(lockerServiceFileDescriptor); - assertEquals(3, typeStringsToResourceNames.size()); + assertEquals(4, typeStringsToResourceNames.size()); ResourceName resourceName = typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Folder"); @@ -77,13 +80,37 @@ public void parseResourceNames_basicTwoPatterns() { assertEquals("cloudresourcemanager.googleapis.com/Folder", resourceName.resourceTypeString()); assertEquals(MAIN_PACKAGE, resourceName.pakkage()); assertFalse(resourceName.hasParentMessageName()); + assertFalse(resourceName.isOnlyWildcard()); + } + + @Test + public void parseResourceNames_wildcard() { + Map typeStringsToResourceNames = + ResourceNameParser.parseResourceNamesFromFile(lockerServiceFileDescriptor); + assertEquals(4, typeStringsToResourceNames.size()); + + ResourceName resourceName = + typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Anything"); + assertEquals(ResourceNameConstants.WILDCARD_PATTERN, resourceName.patterns().get(0)); + assertEquals("anything", resourceName.variableName()); + assertEquals("cloudresourcemanager.googleapis.com/Anything", resourceName.resourceTypeString()); + assertEquals(MAIN_PACKAGE, resourceName.pakkage()); + assertFalse(resourceName.hasParentMessageName()); + assertTrue(resourceName.isOnlyWildcard()); + assertEquals( + TypeNode.withReference( + VaporReference.builder() + .setName("ResourceName") + .setPakkage("com.google.api.resourcenames") + .build()), + resourceName.type()); } @Test public void parseResourceNames_deletedTopic() { Map typeStringsToResourceNames = ResourceNameParser.parseResourceNamesFromFile(lockerServiceFileDescriptor); - assertEquals(3, typeStringsToResourceNames.size()); + assertEquals(4, typeStringsToResourceNames.size()); ResourceName resourceName = typeStringsToResourceNames.get("pubsub.googleapis.com/Topic"); assertEquals(1, resourceName.patterns().size()); @@ -92,6 +119,7 @@ public void parseResourceNames_deletedTopic() { assertEquals("pubsub.googleapis.com/Topic", resourceName.resourceTypeString()); assertEquals(MAIN_PACKAGE, resourceName.pakkage()); assertFalse(resourceName.hasParentMessageName()); + assertFalse(resourceName.isOnlyWildcard()); } @Test 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 ad9534d2f2..990e7dc4a7 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 @@ -69,7 +69,10 @@ public void parseParentResourceName_createFromPattern() { assertEquals(resourceNamePackage, parentResourceName.pakkage()); assertEquals( TypeNode.withReference( - VaporReference.builder().setName("Project").setPakkage(resourceNamePackage).build()), + VaporReference.builder() + .setName("ProjectName") + .setPakkage(resourceNamePackage) + .build()), parentResourceName.type()); assertEquals(patternsToResourceNames.get(parentPattern), parentResourceName); } diff --git a/src/test/java/com/google/api/generator/gapic/testdata/echo.proto b/src/test/java/com/google/api/generator/gapic/testdata/echo.proto index 6f392ce594..11a6f815be 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/echo.proto +++ b/src/test/java/com/google/api/generator/gapic/testdata/echo.proto @@ -17,6 +17,7 @@ syntax = "proto3"; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; import "google/longrunning/operations.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; @@ -28,6 +29,11 @@ option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; option java_package = "com.google.showcase.v1beta1"; option java_multiple_files = true; +option (google.api.resource_definition) = { + type: "showcase.googleapis.com/AnythingGoes" + pattern: "*" +}; + // This service is used showcase the four main types of rpcs - unary, server // side streaming, client side streaming, and bidirectional streaming. This // service also exposes methods that explicitly implement server delay, and @@ -47,6 +53,8 @@ service Echo { option (google.api.method_signature) = "content"; option (google.api.method_signature) = "error"; option (google.api.method_signature) = "content,severity"; + option (google.api.method_signature) = "name"; + option (google.api.method_signature) = "parent"; } // This method split the given content into words and will pass each word back @@ -56,8 +64,6 @@ service Echo { post: "/v1beta1/echo:expand" body: "*" }; - // TODO(landrito): change this to be `fields: ["content", "error"]` once - // github.com/dcodeIO/protobuf.js/issues/1094 has been resolved. option (google.api.method_signature) = "content,error"; } @@ -123,6 +129,12 @@ enum Severity { } message Foobar { + option (google.api.resource) = { + type: "showcase.googleapis.com/Foobar" + pattern: "foobars/{foobar}" + pattern: "*" + }; + string name = 1; string info = 2; } @@ -132,6 +144,17 @@ message Foobar { // If status is set in this message // then the status will be returned as an error. message EchoRequest { + string name = 5 [ + (google.api.resource_reference).type = "showcase.googleapis.com/Foobar", + (google.api.field_behavior) = REQUIRED + ]; + + string parent = 6 [ + (google.api.resource_reference).child_type = + "showcase.googleapis.com/AnythingGoes", + (google.api.field_behavior) = REQUIRED + ]; + oneof response { // The content to be echoed by the server. string content = 1; @@ -216,7 +239,7 @@ message WaitResponse { // The metadata for Wait operation. message WaitMetadata { // The time that this operation will complete. - google.protobuf.Timestamp end_time =1; + google.protobuf.Timestamp end_time = 1; } // The request for Block method. @@ -240,5 +263,3 @@ message BlockResponse { // here. string content = 1; } - -