diff --git a/src/main/java/com/google/api/generator/gapic/composer/DefaultValueComposer.java b/src/main/java/com/google/api/generator/gapic/composer/DefaultValueComposer.java new file mode 100644 index 0000000000..f53123c4c0 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/composer/DefaultValueComposer.java @@ -0,0 +1,189 @@ +// 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.composer; + +import com.google.api.generator.engine.ast.ConcreteReference; +import com.google.api.generator.engine.ast.Expr; +import com.google.api.generator.engine.ast.MethodInvocationExpr; +import com.google.api.generator.engine.ast.NewObjectExpr; +import com.google.api.generator.engine.ast.PrimitiveValue; +import com.google.api.generator.engine.ast.StringObjectValue; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.ValueExpr; +import com.google.api.generator.gapic.model.Field; +import com.google.api.generator.gapic.model.MethodArgument; +import com.google.api.generator.gapic.model.ResourceName; +import com.google.api.generator.gapic.utils.JavaStyle; +import com.google.api.generator.gapic.utils.ResourceNameConstants; +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DefaultValueComposer { + static Expr createDefaultValue( + MethodArgument methodArg, Map resourceNames) { + if (methodArg.isResourceNameHelper()) { + Preconditions.checkState( + methodArg.field().hasResourceReference(), + String.format( + "No corresponding resource reference for argument %s found on field %s %s", + methodArg.name(), methodArg.field().type(), methodArg.field().name())); + ResourceName resourceName = + resourceNames.get(methodArg.field().resourceReference().resourceTypeString()); + Preconditions.checkNotNull( + resourceName, + String.format( + "No resource name found for reference %s", + methodArg.field().resourceReference().resourceTypeString())); + return createDefaultValue( + resourceName, resourceNames.values().stream().collect(Collectors.toList())); + } + + if (methodArg.type().equals(methodArg.field().type())) { + return createDefaultValue(methodArg.field()); + } + + return createDefaultValue( + Field.builder().setName(methodArg.name()).setType(methodArg.type()).build()); + } + + static Expr createDefaultValue(Field f) { + if (f.isRepeated()) { + TypeNode newType = + TypeNode.withReference( + ConcreteReference.withClazz(f.isMap() ? HashMap.class : ArrayList.class)); + return NewObjectExpr.builder().setType(newType).setIsGeneric(true).build(); + } + + if (f.isEnum()) { + return MethodInvocationExpr.builder() + .setStaticReferenceType(f.type()) + .setMethodName("forNumber") + .setArguments( + ValueExpr.withValue( + PrimitiveValue.builder().setType(TypeNode.INT).setValue("0").build())) + .setReturnType(f.type()) + .build(); + } + + if (f.isMessage()) { + MethodInvocationExpr newBuilderExpr = + MethodInvocationExpr.builder() + .setStaticReferenceType(f.type()) + .setMethodName("newBuilder") + .build(); + return MethodInvocationExpr.builder() + .setExprReferenceExpr(newBuilderExpr) + .setMethodName("build") + .setReturnType(f.type()) + .build(); + } + + if (f.type().equals(TypeNode.STRING)) { + return ValueExpr.withValue( + StringObjectValue.withValue(String.format("%s%s", f.name(), f.name().hashCode()))); + } + + if (TypeNode.isNumericType(f.type())) { + return ValueExpr.withValue( + PrimitiveValue.builder() + .setType(f.type()) + .setValue(String.format("%s", f.name().hashCode())) + .build()); + } + + if (f.type().equals(TypeNode.BOOLEAN)) { + return ValueExpr.withValue( + PrimitiveValue.builder().setType(f.type()).setValue("true").build()); + } + + throw new UnsupportedOperationException( + String.format( + "Default value for field %s with type %s not implemented yet.", f.name(), f.type())); + } + + static Expr createDefaultValue(ResourceName resourceName, List resnames) { + boolean hasOnePattern = resourceName.patterns().size() == 1; + if (resourceName.isOnlyWildcard()) { + List unexaminedResnames = new ArrayList<>(resnames); + for (ResourceName resname : resnames) { + if (resname.isOnlyWildcard()) { + unexaminedResnames.remove(resname); + continue; + } + unexaminedResnames.remove(resname); + return createDefaultValue(resname, unexaminedResnames); + } + // Should not get here. + Preconditions.checkState( + !unexaminedResnames.isEmpty(), + String.format( + "No default resource name found for wildcard %s", resourceName.resourceTypeString())); + } + + // The cost tradeoffs of new ctors versus distinct() don't really matter here, since this list + // will usually have a very small number of elements. + List patterns = new ArrayList<>(new HashSet<>(resourceName.patterns())); + boolean containsOnlyDeletedTopic = + patterns.size() == 1 && patterns.get(0).equals(ResourceNameConstants.DELETED_TOPIC_LITERAL); + String ofMethodName = "of"; + List patternPlaceholderTokens = new ArrayList<>(); + + if (containsOnlyDeletedTopic) { + ofMethodName = "ofDeletedTopic"; + } else { + for (String pattern : resourceName.patterns()) { + if (pattern.equals(ResourceNameConstants.WILDCARD_PATTERN) + || pattern.equals(ResourceNameConstants.DELETED_TOPIC_LITERAL)) { + continue; + } + patternPlaceholderTokens.addAll( + ResourceNameTokenizer.parseTokenHierarchy(Arrays.asList(pattern)).get(0)); + break; + } + } + + if (!hasOnePattern) { + ofMethodName = + String.format( + "of%sName", + String.join( + "", + patternPlaceholderTokens.stream() + .map(s -> JavaStyle.toUpperCamelCase(s)) + .collect(Collectors.toList()))); + } + + TypeNode resourceNameJavaType = resourceName.type(); + List argExprs = + patternPlaceholderTokens.stream() + .map( + s -> + ValueExpr.withValue( + StringObjectValue.withValue(String.format("[%s]", s.toUpperCase())))) + .collect(Collectors.toList()); + return MethodInvocationExpr.builder() + .setStaticReferenceType(resourceNameJavaType) + .setMethodName(ofMethodName) + .setArguments(argExprs) + .setReturnType(resourceNameJavaType) + .build(); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/composer/DefaultValueComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/DefaultValueComposerTest.java new file mode 100644 index 0000000000..bb0083f013 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/composer/DefaultValueComposerTest.java @@ -0,0 +1,211 @@ +// 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.composer; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.api.generator.engine.ast.Expr; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.writer.JavaWriterVisitor; +import com.google.api.generator.gapic.model.Field; +import com.google.api.generator.gapic.model.ResourceName; +import com.google.api.generator.gapic.protoparser.Parser; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.testgapic.v1beta1.LockerProto; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; + +public class DefaultValueComposerTest { + private JavaWriterVisitor writerVisitor; + + @Before + public void setUp() { + writerVisitor = new JavaWriterVisitor(); + } + + @Test + public void defaultValue_mapField() { + // Incorrect and will never happen in real usage, but proves that map detection is based on + // isMap rather than type(). + Field field = + Field.builder() + .setName("foobar") + .setType(TypeNode.STRING) + .setIsMap(true) + .setIsRepeated(true) + .build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("new HashMap<>()", writerVisitor.write()); + + writerVisitor.clear(); + + // isMap() and isRepeated() will be set by protoc simulataneoulsy, but we check this edge case + // for completeness. + field = Field.builder().setName("foobar").setType(TypeNode.STRING).setIsMap(true).build(); + expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("\"foobar-1268878963\"", writerVisitor.write()); + } + + @Test + public void defaultValue_listField() { + // Incorrect and will never happen in real usage, but proves that list detection is based on + // isRepeated rather than type(). + Field field = + Field.builder().setName("foobar").setType(TypeNode.STRING).setIsRepeated(true).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("new ArrayList<>()", writerVisitor.write()); + } + + @Test + public void defaultValue_enumField() { + // Incorrect and will never happen in real usage, but proves that enum detection is based on + // isEnum() rather than type(). + Field field = + Field.builder().setName("foobar").setType(TypeNode.STRING).setIsEnum(true).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("String.forNumber(0)", writerVisitor.write()); + } + + @Test + public void defaultValue_messageField() { + // Incorrect and will never happen in real usage, but proves that message detection is based on + // isMessage() rather than type(). + Field field = + Field.builder().setName("foobar").setType(TypeNode.STRING).setIsMessage(true).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("String.newBuilder().build()", writerVisitor.write()); + } + + @Test + public void defaultValue_stringField() { + Field field = Field.builder().setName("foobar").setType(TypeNode.STRING).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("\"foobar-1268878963\"", writerVisitor.write()); + } + + @Test + public void defaultValue_numericField() { + Field field = Field.builder().setName("foobar").setType(TypeNode.INT).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("-1268878963", writerVisitor.write()); + + writerVisitor.clear(); + field = Field.builder().setName("foobar").setType(TypeNode.DOUBLE).build(); + expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("-1268878963", writerVisitor.write()); + } + + @Test + public void defaultValue_booleanField() { + Field field = Field.builder().setName("foobar").setType(TypeNode.BOOLEAN).build(); + Expr expr = DefaultValueComposer.createDefaultValue(field); + expr.accept(writerVisitor); + assertEquals("true", writerVisitor.write()); + } + + @Test + public void defaultValue_resourceNameWithOnePattern() { + FileDescriptor lockerServiceFileDescriptor = LockerProto.getDescriptor(); + Map typeStringsToResourceNames = + Parser.parseResourceNames(lockerServiceFileDescriptor); + ResourceName resourceName = + typeStringsToResourceNames.get("cloudbilling.googleapis.com/BillingAccount"); + Expr expr = + DefaultValueComposer.createDefaultValue( + resourceName, + typeStringsToResourceNames.values().stream().collect(Collectors.toList())); + expr.accept(writerVisitor); + assertEquals("BillingAccountName.of(\"[BILLING_ACCOUNT]\")", writerVisitor.write()); + } + + @Test + public void defaultValue_resourceNameWithMultiplePatterns() { + FileDescriptor lockerServiceFileDescriptor = LockerProto.getDescriptor(); + Map typeStringsToResourceNames = + Parser.parseResourceNames(lockerServiceFileDescriptor); + ResourceName resourceName = + typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Folder"); + Expr expr = + DefaultValueComposer.createDefaultValue( + resourceName, + typeStringsToResourceNames.values().stream().collect(Collectors.toList())); + expr.accept(writerVisitor); + assertEquals( + "FolderName.ofProjectFolderName(\"[PROJECT]\", \"[FOLDER]\")", writerVisitor.write()); + } + + @Test + public void defaultValue_resourceNameWithWildcardPattern() { + FileDescriptor lockerServiceFileDescriptor = LockerProto.getDescriptor(); + Map typeStringsToResourceNames = + Parser.parseResourceNames(lockerServiceFileDescriptor); + ResourceName resourceName = + typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Anything"); + Expr expr = + DefaultValueComposer.createDefaultValue( + resourceName, + typeStringsToResourceNames.values().stream().collect(Collectors.toList())); + expr.accept(writerVisitor); + assertEquals("DocumentName.ofDocumentName(\"[DOCUMENT]\")", writerVisitor.write()); + } + + @Test + public void invalidDefaultValue_wildcardResourceNameWithOnlyDeletedTopic() { + // Edge case that should never happen in practice. + // Wildcard, but the resource names map has only other names that contain only the deleted-topic + // constant. + FileDescriptor lockerServiceFileDescriptor = LockerProto.getDescriptor(); + Map typeStringsToResourceNames = + Parser.parseResourceNames(lockerServiceFileDescriptor); + ResourceName resourceName = + typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Anything"); + ResourceName topicResourceName = typeStringsToResourceNames.get("pubsub.googleapis.com/Topic"); + typeStringsToResourceNames.clear(); + typeStringsToResourceNames.put(topicResourceName.resourceTypeString(), topicResourceName); + Expr expr = + DefaultValueComposer.createDefaultValue( + resourceName, + typeStringsToResourceNames.values().stream().collect(Collectors.toList())); + expr.accept(writerVisitor); + assertEquals("TopicName.ofDeletedTopic()", writerVisitor.write()); + } + + @Test + public void invalidDefaultValue_resourceNameWithOnlyWildcards() { + // Edge case that should never happen in practice. + // Wildcard, but the resource names map has only other names that contain only the deleted-topic + // constant. + FileDescriptor lockerServiceFileDescriptor = LockerProto.getDescriptor(); + Map typeStringsToResourceNames = + Parser.parseResourceNames(lockerServiceFileDescriptor); + ResourceName resourceName = + typeStringsToResourceNames.get("cloudresourcemanager.googleapis.com/Anything"); + assertThrows( + IllegalStateException.class, + () -> DefaultValueComposer.createDefaultValue(resourceName, Collections.emptyList())); + } +}