diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/ProtoRegistry.java b/gapic-generator-java/src/main/java/com/google/api/generator/ProtoRegistry.java index 6f08ccfa3b..ae9c8cc76e 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/ProtoRegistry.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/ProtoRegistry.java @@ -17,6 +17,7 @@ import com.google.api.AnnotationsProto; import com.google.api.ClientProto; import com.google.api.FieldBehaviorProto; +import com.google.api.FieldInfoProto; import com.google.api.ResourceProto; import com.google.api.RoutingProto; import com.google.cloud.ExtendedOperationsProto; @@ -31,6 +32,7 @@ public static void registerAllExtensions(ExtensionRegistry extensionRegistry) { ClientProto.registerAllExtensions(extensionRegistry); ResourceProto.registerAllExtensions(extensionRegistry); FieldBehaviorProto.registerAllExtensions(extensionRegistry); + FieldInfoProto.registerAllExtensions(extensionRegistry); ExtendedOperationsProto.registerAllExtensions(extensionRegistry); RoutingProto.registerAllExtensions(extensionRegistry); } diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java index 191e834a40..2647647443 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java @@ -42,6 +42,7 @@ import com.google.api.generator.engine.ast.MethodDefinition; import com.google.api.generator.engine.ast.MethodInvocationExpr; import com.google.api.generator.engine.ast.NewObjectExpr; +import com.google.api.generator.engine.ast.Reference; import com.google.api.generator.engine.ast.ReferenceConstructorExpr; import com.google.api.generator.engine.ast.RelationalOperationExpr; import com.google.api.generator.engine.ast.ScopeNode; @@ -58,6 +59,7 @@ import com.google.api.generator.gapic.composer.comment.StubCommentComposer; import com.google.api.generator.gapic.composer.store.TypeStore; import com.google.api.generator.gapic.composer.utils.PackageChecker; +import com.google.api.generator.gapic.model.Field; import com.google.api.generator.gapic.model.GapicClass; import com.google.api.generator.gapic.model.GapicClass.Kind; import com.google.api.generator.gapic.model.GapicContext; @@ -70,8 +72,10 @@ import com.google.api.generator.gapic.model.Transport; import com.google.api.generator.gapic.utils.JavaStyle; import com.google.api.pathtemplate.PathTemplate; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.longrunning.Operation; @@ -84,9 +88,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Generated; import javax.annotation.Nullable; @@ -136,6 +142,7 @@ private static TypeStore createStaticTypes() { OperationCallable.class, OperationSnapshot.class, RequestParamsExtractor.class, + UUID.class, ServerStreamingCallable.class, TimeUnit.class, TypeRegistry.class, @@ -277,7 +284,8 @@ protected Expr createTransportSettingsInitExpr( Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr, - List classStatements) { + List classStatements, + ImmutableMap messageTypes) { MethodInvocationExpr callSettingsBuilderExpr = MethodInvocationExpr.builder() .setStaticReferenceType(getTransportContext().transportCallSettingsType()) @@ -318,6 +326,15 @@ protected Expr createTransportSettingsInitExpr( .build(); } + if (method.hasAutoPopulatedFields() && shouldGenerateRequestMutator(method, messageTypes)) { + callSettingsBuilderExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(callSettingsBuilderExpr) + .setMethodName("setRequestMutator") + .setArguments(createRequestMutatorClassInstance(method, messageTypes)) + .build(); + } + callSettingsBuilderExpr = MethodInvocationExpr.builder() .setExprReferenceExpr(callSettingsBuilderExpr) @@ -760,7 +777,8 @@ protected List createConstructorMethods( javaStyleMethodNameToTransportSettingsVarExprs.get( JavaStyle.toLowerCamelCase(m.name())), protoMethodNameToDescriptorVarExprs.get(m.name()), - classStatements)) + classStatements, + context.messages())) .collect(Collectors.toList())); secondCtorStatements.addAll( secondCtorExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())); @@ -1233,12 +1251,197 @@ protected TypeNode getTransportOperationsStubType(Service service) { return transportOpeationsStubType; } + protected static LambdaExpr createRequestMutatorClassInstance( + Method method, ImmutableMap messageTypes) { + List bodyStatements = new ArrayList<>(); + VariableExpr requestVarExpr = createRequestVarExpr(method); + + Reference requestBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(method.inputType().reference().name()) + .setName("Builder") + .setPakkage(method.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(requestBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(true) + .build(); + + MethodInvocationExpr setRequestBuilderInvocationExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestVarExpr) + .setMethodName("toBuilder") + .setReturnType(requestBuilderType) + .build(); + + Expr requestBuilderExpr = + AssignmentExpr.builder() + .setVariableExpr(requestBuilderVarExpr) + .setValueExpr(setRequestBuilderInvocationExpr) + .build(); + + bodyStatements.add(ExprStatement.withExpr(requestBuilderExpr)); + + VariableExpr returnBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + MethodInvocationExpr.Builder returnExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(returnBuilderVarExpr) + .setMethodName("build"); + + createRequestMutatorBody(method, messageTypes, bodyStatements, returnBuilderVarExpr); + + return LambdaExpr.builder() + .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) + .setBody(bodyStatements) + .setReturnExpr(returnExpr.build()) + .build(); + } + + @VisibleForTesting + static List createRequestMutatorBody( + Method method, + ImmutableMap messageTypes, + List bodyStatements, + VariableExpr returnBuilderVarExpr) { + + Message methodRequestMessage = messageTypes.get(method.inputType().reference().fullName()); + method.autoPopulatedFields().stream() + // Map each field name to its corresponding Field object, if present + .map( + fieldName -> + methodRequestMessage.fields().stream() + .filter(field -> field.name().equals(fieldName)) + .findFirst()) + .filter(Optional::isPresent) // Keep only the existing Fields + .map(Optional::get) // Extract the Field from the Optional + .filter(Field::canBeAutoPopulated) // Filter fields that can be autopopulated + .forEach( + matchedField -> { + // Create statements for each autopopulated Field + bodyStatements.add( + createAutoPopulatedRequestStatement( + method, matchedField.name(), returnBuilderVarExpr)); + }); + return bodyStatements; + } + + @VisibleForTesting + static Statement createAutoPopulatedRequestStatement( + Method method, String fieldName, VariableExpr returnBuilderVarExpr) { + + VariableExpr requestVarExpr = createRequestVarExpr(method); + + // Expected expression: request.getRequestId() + MethodInvocationExpr getAutoPopulatedFieldInvocationExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestVarExpr) + .setMethodName(String.format("get%s", JavaStyle.toUpperCamelCase(fieldName))) + .setReturnType(TypeNode.STRING) + .build(); + + VariableExpr stringsVar = + VariableExpr.withVariable( + Variable.builder() + .setType(TypeNode.withReference(ConcreteReference.withClazz(Strings.class))) + .setName("Strings") + .build()); + + // Expected expression: Strings.isNullOrEmpty(request.getRequestId()) + MethodInvocationExpr isNullOrEmptyFieldInvocationExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(stringsVar) + .setMethodName("isNullOrEmpty") + .setReturnType(TypeNode.BOOLEAN) + .setArguments(getAutoPopulatedFieldInvocationExpr) + .build(); + + // Note: Currently, autopopulation is only for UUID. + VariableExpr uuidVarExpr = + VariableExpr.withVariable( + Variable.builder() + .setType( + TypeNode.withReference( + ConcreteReference.builder().setClazz(UUID.class).build())) + .setName("UUID") + .build()); + + // Expected expression: UUID.randomUUID() + MethodInvocationExpr autoPopulatedFieldsArgsHelper = + MethodInvocationExpr.builder() + .setExprReferenceExpr(uuidVarExpr) + .setMethodName("randomUUID") + .setReturnType( + TypeNode.withReference(ConcreteReference.builder().setClazz(UUID.class).build())) + .build(); + + // Expected expression: UUID.randomUUID().toString() + MethodInvocationExpr autoPopulatedFieldsArgsToString = + MethodInvocationExpr.builder() + .setExprReferenceExpr(autoPopulatedFieldsArgsHelper) + .setMethodName("toString") + .setReturnType(TypeNode.STRING) + .build(); + + // Expected expression: requestBuilder().setField(UUID.randomUUID().toString()) + MethodInvocationExpr setAutoPopulatedFieldInvocationExpr = + MethodInvocationExpr.builder() + .setArguments(autoPopulatedFieldsArgsToString) + .setExprReferenceExpr(returnBuilderVarExpr) + .setMethodName(String.format("set%s", JavaStyle.toUpperCamelCase(fieldName))) + .setReturnType(method.inputType()) + .build(); + + return IfStatement.builder() + .setConditionExpr(isNullOrEmptyFieldInvocationExpr) + .setBody(Arrays.asList(ExprStatement.withExpr(setAutoPopulatedFieldInvocationExpr))) + .build(); + } + + /** + * The Request Mutator should only be generated if the field exists in the Message and is properly + * configured in the Message(see {@link Field#canBeAutoPopulated()}) + */ + @VisibleForTesting + static Boolean shouldGenerateRequestMutator( + Method method, ImmutableMap messageTypes) { + if (method.inputType().reference() == null + || method.inputType().reference().fullName() == null) { + return false; + } + String methodRequestName = method.inputType().reference().fullName(); + + Message methodRequestMessage = messageTypes.get(methodRequestName); + if (methodRequestMessage == null || methodRequestMessage.fields() == null) { + return false; + } + return method.autoPopulatedFields().stream().anyMatch(shouldAutoPopulate(methodRequestMessage)); + } + + /** + * The field has to exist in the Message and properly configured in the Message(see {@link + * Field#canBeAutoPopulated()}) + */ + private static Predicate shouldAutoPopulate(Message methodRequestMessage) { + return fieldName -> + methodRequestMessage.fields().stream() + .anyMatch(field -> field.name().equals(fieldName) && field.canBeAutoPopulated()); + } + protected LambdaExpr createRequestParamsExtractorClassInstance( Method method, List classStatements) { List bodyStatements = new ArrayList<>(); - VariableExpr requestVarExpr = - VariableExpr.withVariable( - Variable.builder().setType(method.inputType()).setName("request").build()); + VariableExpr requestVarExpr = createRequestVarExpr(method); TypeNode returnType = TypeNode.withReference( ConcreteReference.builder() @@ -1499,4 +1702,9 @@ private MethodInvocationExpr createRequestFieldGetterExpr( } return requestFieldGetterExprBuilder.build(); } + + private static VariableExpr createRequestVarExpr(Method method) { + return VariableExpr.withVariable( + Variable.builder().setType(method.inputType()).setName("request").build()); + } } diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/Field.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/Field.java index 997ea54e5a..39213909de 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/Field.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/model/Field.java @@ -72,6 +72,17 @@ public boolean hasResourceReference() { return type().equals(TypeNode.STRING) && resourceReference() != null; } + // Check that the field format is of UUID, it is not annotated as required, and is of type String. + // Unless + // those three conditions are met, do not autopopulate the field. + // In the future, if additional formats are supported for autopopulation, this will need to be + // refactored to support those formats. + public boolean canBeAutoPopulated() { + return Format.UUID4.equals(fieldInfoFormat()) + && !isRequired() + && TypeNode.STRING.equals(type()); + } + @Override public boolean equals(Object o) { if (!(o instanceof Field)) { diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index 9f1b395940..e960ad3f6d 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java @@ -1025,10 +1025,13 @@ private static Field parseField( if (fieldOptions.hasExtension(FieldInfoProto.fieldInfo)) { fieldInfoFormat = fieldOptions.getExtension(FieldInfoProto.fieldInfo).getFormat(); } - if (fieldOptions.getExtensionCount(FieldBehaviorProto.fieldBehavior) > 0) { - if (fieldOptions - .getExtension(FieldBehaviorProto.fieldBehavior) - .contains(FieldBehavior.REQUIRED)) ; + + // Cannot directly check fieldOptions.hasExtension(FieldBehaviorProto.fieldBehavior) because the + // default is null + if (fieldOptions.getExtensionCount(FieldBehaviorProto.fieldBehavior) > 0 + && fieldOptions + .getExtension(FieldBehaviorProto.fieldBehavior) + .contains(FieldBehavior.REQUIRED)) { isRequired = true; } diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposerTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposerTest.java new file mode 100644 index 0000000000..77205f0122 --- /dev/null +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposerTest.java @@ -0,0 +1,529 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.generator.gapic.composer.common; + +import static com.google.api.generator.gapic.composer.common.AbstractTransportServiceStubClassComposer.shouldGenerateRequestMutator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.api.FieldInfo.Format; +import com.google.api.generator.engine.ast.LambdaExpr; +import com.google.api.generator.engine.ast.Reference; +import com.google.api.generator.engine.ast.Statement; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.VaporReference; +import com.google.api.generator.engine.ast.Variable; +import com.google.api.generator.engine.ast.VariableExpr; +import com.google.api.generator.engine.writer.JavaWriterVisitor; +import com.google.api.generator.gapic.model.Field; +import com.google.api.generator.gapic.model.Message; +import com.google.api.generator.gapic.model.Method; +import com.google.api.generator.test.utils.LineFormatter; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public class AbstractTransportServiceStubClassComposerTest { + private JavaWriterVisitor writerVisitor; + + @Before + public void setUp() { + writerVisitor = new JavaWriterVisitor(); + } + + @Test + public void shouldGenerateRequestMutator_fieldConfiguredCorrectly() { + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + assertTrue(shouldGenerateRequestMutator(METHOD, messageTypes)); + } + + @Test + public void shouldNotGenerateRequestMutator_fieldConfiguredIncorrectly() { + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.IPV6) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + assertFalse(shouldGenerateRequestMutator(METHOD, messageTypes)); + } + + // TODO: add unit tests where the field is not found in the messageTypes map + @Test + public void createAutoPopulatedRequestStatement_sampleField() { + Reference RequestBuilderRef = + VaporReference.builder() + .setName("EchoRequest") + .setPakkage("com.google.example.examples.library.v1") + .build(); + + TypeNode testType = TypeNode.withReference(RequestBuilderRef); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType(testType) + .setOutputType(TypeNode.STRING) + .build(); + + Reference RequestVarBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(METHOD.inputType().reference().name()) + .setName("Builder") + .setPakkage(METHOD.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(RequestVarBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + Statement autoPopulatedFieldStatement = + AbstractTransportServiceStubClassComposer.createAutoPopulatedRequestStatement( + METHOD, "sampleField", requestBuilderVarExpr); + + autoPopulatedFieldStatement.accept(writerVisitor); + String expected = + LineFormatter.lines( + "if (Strings.isNullOrEmpty(request.getSampleField())) {\n", + "requestBuilder.setSampleField(UUID.randomUUID().toString());\n", + "}\n"); + assertEquals(expected, writerVisitor.write()); + } + + @Test + public void createRequestMutatorBody_TestField() { + List bodyStatements = new ArrayList<>(); + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + Reference RequestBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(METHOD.inputType().reference().name()) + .setName("Builder") + .setPakkage(METHOD.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(RequestBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + List listOfAutoPopulatedStatements = + AbstractTransportServiceStubClassComposer.createRequestMutatorBody( + METHOD, messageTypes, bodyStatements, requestBuilderVarExpr); + + for (Statement statement : listOfAutoPopulatedStatements) { + statement.accept(writerVisitor); + } + + String expected = + LineFormatter.lines( + "if (Strings.isNullOrEmpty(request.getTestField())) {\n", + "requestBuilder.setTestField(UUID.randomUUID().toString());\n", + "}\n"); + assertEquals(expected, writerVisitor.write()); + } + + @Test + public void createRequestMutatorBody_TestFieldNotString_shouldReturnNull() { + List bodyStatements = new ArrayList<>(); + + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.BOOLEAN) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + Reference RequestBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(METHOD.inputType().reference().name()) + .setName("Builder") + .setPakkage(METHOD.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(RequestBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + List listOfAutoPopulatedStatements = + AbstractTransportServiceStubClassComposer.createRequestMutatorBody( + METHOD, messageTypes, bodyStatements, requestBuilderVarExpr); + + for (Statement statement : listOfAutoPopulatedStatements) { + statement.accept(writerVisitor); + } + + String expected = LineFormatter.lines(""); + assertEquals(expected, writerVisitor.write()); + } + + @Test + public void createRequestMutatorBody_TestFieldFormatNotUUID_shouldReturnNull() { + List bodyStatements = new ArrayList<>(); + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.IPV4_OR_IPV6) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + Reference RequestBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(METHOD.inputType().reference().name()) + .setName("Builder") + .setPakkage(METHOD.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(RequestBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + List listOfAutoPopulatedStatements = + AbstractTransportServiceStubClassComposer.createRequestMutatorBody( + METHOD, messageTypes, bodyStatements, requestBuilderVarExpr); + + for (Statement statement : listOfAutoPopulatedStatements) { + statement.accept(writerVisitor); + } + + String expected = LineFormatter.lines(""); + assertEquals(expected, writerVisitor.write()); + } + + @Test + public void createRequestMutatorBody_TestFieldIncorrectName_shouldReturnNull() { + List bodyStatements = new ArrayList<>(); + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestIncorrectField") + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + Reference RequestBuilderRef = + VaporReference.builder() + .setEnclosingClassNames(METHOD.inputType().reference().name()) + .setName("Builder") + .setPakkage(METHOD.inputType().reference().pakkage()) + .build(); + + TypeNode requestBuilderType = TypeNode.withReference(RequestBuilderRef); + + VariableExpr requestBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("requestBuilder").setType(requestBuilderType).build()) + .setIsDecl(false) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + List listOfAutoPopulatedStatements = + AbstractTransportServiceStubClassComposer.createRequestMutatorBody( + METHOD, messageTypes, bodyStatements, requestBuilderVarExpr); + + for (Statement statement : listOfAutoPopulatedStatements) { + statement.accept(writerVisitor); + } + + String expected = LineFormatter.lines(""); + assertEquals(expected, writerVisitor.write()); + } + + @Test + public void createRequestMutator_TestField() { + String ECHO_PACKAGE = "com.google.showcase.v1beta1"; + List autoPopulatedFieldList = new ArrayList<>(); + autoPopulatedFieldList.add("TestField"); + + Method METHOD = + Method.builder() + .setName("TestMethod") + .setInputType( + TypeNode.withReference( + VaporReference.builder() + .setName("SampleRequest") + .setPakkage(ECHO_PACKAGE) + .build())) + .setOutputType(TypeNode.STRING) + .setAutoPopulatedFields(autoPopulatedFieldList) + .build(); + + Field FIELD = + Field.builder() + .setName("TestField") + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + List fieldList = new ArrayList<>(); + fieldList.add(FIELD); + + Message MESSAGE = + Message.builder() + .setFullProtoName("com.google.showcase.v1beta1.SampleRequest") + .setName("SampleRequest") + .setType(TypeNode.STRING) + .setFields(fieldList) + .build(); + + ImmutableMap messageTypes = + ImmutableMap.of("com.google.showcase.v1beta1.SampleRequest", MESSAGE); + + LambdaExpr requestMutator = + AbstractTransportServiceStubClassComposer.createRequestMutatorClassInstance( + METHOD, messageTypes); + + requestMutator.accept(writerVisitor); + + String expected = + LineFormatter.lines( + "request -> {\n", + "SampleRequest.Builder requestBuilder = request.toBuilder();\n", + "if (Strings.isNullOrEmpty(request.getTestField())) {\n", + "requestBuilder.setTestField(UUID.randomUUID().toString());\n", + "}\n", + "return requestBuilder.build();\n", + "}"); + assertEquals(expected, writerVisitor.write()); + } +} diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/defaultvalue/DefaultValueComposerTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/defaultvalue/DefaultValueComposerTest.java index 43e5411b30..3345645c8c 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/defaultvalue/DefaultValueComposerTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/defaultvalue/DefaultValueComposerTest.java @@ -569,6 +569,11 @@ public void createSimpleMessage_containsMessagesEnumsAndResourceName() { + "FoobarName.ofProjectFoobarName(\"[PROJECT]\", \"[FOOBAR]\").toString())" + ".setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\", \"[FOOBAR]\").toString())" + ".setRequestId(\"requestId693933066\")" + + ".setSecondRequestId(\"secondRequestId344404470\")" + + ".setThirdRequestId(true)" + + ".setFourthRequestId(\"fourthRequestId-2116417776\")" + + ".setFifthRequestId(\"fifthRequestId959024147\")" + + ".setSixthRequestId(\"sixthRequestId1005218260\")" + ".setSeverity(Severity.forNumber(0))" + ".setFoobar(Foobar.newBuilder().build()).build()", writerVisitor.write()); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClient.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClient.golden index f67a50d9c0..f1bf7437b4 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClient.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClient.golden @@ -518,6 +518,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -548,6 +553,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -624,6 +634,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -652,6 +667,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -683,6 +703,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -1092,6 +1117,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); @@ -1122,6 +1152,11 @@ public class EchoClient implements BackgroundResource { * .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) * .setRequestId("requestId693933066") + * .setSecondRequestId("secondRequestId344404470") + * .setThirdRequestId(true) + * .setFourthRequestId("fourthRequestId-2116417776") + * .setFifthRequestId("fifthRequestId959024147") + * .setSixthRequestId("sixthRequestId1005218260") * .setSeverity(Severity.forNumber(0)) * .setFoobar(Foobar.newBuilder().build()) * .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClientTest.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClientTest.golden index 009d8688df..a2a88d6bf3 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClientTest.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/EchoClientTest.golden @@ -109,6 +109,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -459,6 +464,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -485,6 +495,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -519,6 +534,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -545,6 +565,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -579,6 +604,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -605,6 +635,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -864,6 +899,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); @@ -878,6 +918,11 @@ public class EchoClientTest { Assert.assertEquals(request.getName(), actualRequest.getName()); Assert.assertEquals(request.getParent(), actualRequest.getParent()); Assert.assertEquals(request.getRequestId(), actualRequest.getRequestId()); + Assert.assertEquals(request.getSecondRequestId(), actualRequest.getSecondRequestId()); + Assert.assertEquals(request.getThirdRequestId(), actualRequest.getThirdRequestId()); + Assert.assertEquals(request.getFourthRequestId(), actualRequest.getFourthRequestId()); + Assert.assertEquals(request.getFifthRequestId(), actualRequest.getFifthRequestId()); + Assert.assertEquals(request.getSixthRequestId(), actualRequest.getSixthRequestId()); Assert.assertEquals(request.getContent(), actualRequest.getContent()); Assert.assertEquals(request.getError(), actualRequest.getError()); Assert.assertEquals(request.getSeverity(), actualRequest.getSeverity()); @@ -899,6 +944,11 @@ public class EchoClientTest { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcEchoStub.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcEchoStub.golden index 940ab7d4c4..c74e415fef 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcEchoStub.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcEchoStub.golden @@ -14,6 +14,7 @@ import com.google.api.gax.rpc.ClientStreamingCallable; import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; +import com.google.common.base.Strings; import com.google.longrunning.Operation; import com.google.longrunning.stub.GrpcOperationsStub; import com.google.showcase.v1beta1.BlockRequest; @@ -30,6 +31,7 @@ import com.google.showcase.v1beta1.WaitResponse; import io.grpc.MethodDescriptor; import io.grpc.protobuf.ProtoUtils; import java.io.IOException; +import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Generated; @@ -183,6 +185,17 @@ public class GrpcEchoStub extends EchoStub { GrpcCallSettings echoTransportSettings = GrpcCallSettings.newBuilder() .setMethodDescriptor(echoMethodDescriptor) + .setRequestMutator( + request -> { + EchoRequest.Builder requestBuilder = request.toBuilder(); + if (Strings.isNullOrEmpty(request.getRequestId())) { + requestBuilder.setRequestId(UUID.randomUUID().toString()); + } + if (Strings.isNullOrEmpty(request.getSecondRequestId())) { + requestBuilder.setSecondRequestId(UUID.randomUUID().toString()); + } + return requestBuilder.build(); + }) .build(); GrpcCallSettings expandTransportSettings = GrpcCallSettings.newBuilder() diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChat.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChat.golden index e17e9367c8..e67e1d5045 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChat.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChat.golden @@ -44,6 +44,11 @@ public class AsyncChat { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChatAgain.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChatAgain.golden index 02e38bd9db..16fdf6e73a 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChatAgain.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncChatAgain.golden @@ -44,6 +44,11 @@ public class AsyncChatAgain { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollect.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollect.golden index cd8c21c72d..756c28f9b2 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollect.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollect.golden @@ -62,6 +62,11 @@ public class AsyncCollect { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollideName.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollideName.golden index 5ca5cd0bcc..8b22c05f7d 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollideName.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncCollideName.golden @@ -43,6 +43,11 @@ public class AsyncCollideName { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncEcho.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncEcho.golden index 087faaebf5..685c2e16cb 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncEcho.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/AsyncEcho.golden @@ -43,6 +43,11 @@ public class AsyncEcho { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncCollideName.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncCollideName.golden index f299524598..e4beee3fcd 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncCollideName.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncCollideName.golden @@ -42,6 +42,11 @@ public class SyncCollideName { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncEcho.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncEcho.golden index a0918f576d..29c754d9ae 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncEcho.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/samples/echoclient/SyncEcho.golden @@ -42,6 +42,11 @@ public class SyncEcho { .setName(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setParent(FoobarName.ofProjectFoobarName("[PROJECT]", "[FOOBAR]").toString()) .setRequestId("requestId693933066") + .setSecondRequestId("secondRequestId344404470") + .setThirdRequestId(true) + .setFourthRequestId("fourthRequestId-2116417776") + .setFifthRequestId("fifthRequestId959024147") + .setSixthRequestId("sixthRequestId1005218260") .setSeverity(Severity.forNumber(0)) .setFoobar(Foobar.newBuilder().build()) .build(); diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonEchoStub.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonEchoStub.golden index 18904bdfbe..d58ce093b9 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonEchoStub.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonEchoStub.golden @@ -23,6 +23,7 @@ import com.google.api.gax.rpc.ClientStreamingCallable; import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.longrunning.Operation; import com.google.protobuf.TypeRegistry; @@ -42,6 +43,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Generated; @@ -408,6 +410,17 @@ public class HttpJsonEchoStub extends EchoStub { HttpJsonCallSettings.newBuilder() .setMethodDescriptor(echoMethodDescriptor) .setTypeRegistry(typeRegistry) + .setRequestMutator( + request -> { + EchoRequest.Builder requestBuilder = request.toBuilder(); + if (Strings.isNullOrEmpty(request.getRequestId())) { + requestBuilder.setRequestId(UUID.randomUUID().toString()); + } + if (Strings.isNullOrEmpty(request.getSecondRequestId())) { + requestBuilder.setSecondRequestId(UUID.randomUUID().toString()); + } + return requestBuilder.build(); + }) .build(); HttpJsonCallSettings expandTransportSettings = HttpJsonCallSettings.newBuilder() diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientCallableMethodSampleComposerTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientCallableMethodSampleComposerTest.java index 44a921f3b0..47be643ffa 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientCallableMethodSampleComposerTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientCallableMethodSampleComposerTest.java @@ -580,6 +580,11 @@ public void valid_composeStreamCallableMethod_bidiStream() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", @@ -713,6 +718,11 @@ public void valid_composeStreamCallableMethod_clientStream() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", @@ -820,6 +830,11 @@ public void valid_composeRegularCallableMethod_unaryRpc() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposerTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposerTest.java index 265882ac3f..affffb9c09 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposerTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposerTest.java @@ -224,6 +224,11 @@ public void composeClassHeaderSample_firstMethodHasNoSignatures() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposerTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposerTest.java index 9839af8f31..6d473be885 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposerTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposerTest.java @@ -336,6 +336,11 @@ public void valid_composeDefaultSample_pureUnaryReturnVoid() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", @@ -399,6 +404,11 @@ public void valid_composeDefaultSample_pureUnaryReturnResponse() { " .setParent(FoobarName.ofProjectFoobarName(\"[PROJECT]\"," + " \"[FOOBAR]\").toString())\n", " .setRequestId(\"requestId693933066\")\n", + " .setSecondRequestId(\"secondRequestId344404470\")\n", + " .setThirdRequestId(true)\n", + " .setFourthRequestId(\"fourthRequestId-2116417776\")\n", + " .setFifthRequestId(\"fifthRequestId959024147\")\n", + " .setSixthRequestId(\"sixthRequestId1005218260\")\n", " .setSeverity(Severity.forNumber(0))\n", " .setFoobar(Foobar.newBuilder().build())\n", " .build();\n", diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/model/FieldTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/model/FieldTest.java new file mode 100644 index 0000000000..89ba60eabd --- /dev/null +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/model/FieldTest.java @@ -0,0 +1,76 @@ +// Copyright 2024 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 org.junit.Assert.assertEquals; + +import com.google.api.FieldInfo.Format; +import com.google.api.generator.engine.ast.TypeNode; +import org.junit.Test; + +public class FieldTest { + + @Test + public void shouldAutoPopulate() { + Field FIELD = + Field.builder() + .setName("SampleField") + .setIsRequired(false) + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + + assertEquals(true, FIELD.canBeAutoPopulated()); + } + + @Test + public void isRequired_shouldNotAutoPopulate() { + Field FIELD = + Field.builder() + .setName("SampleField") + .setIsRequired(true) + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.STRING) + .build(); + + assertEquals(false, FIELD.canBeAutoPopulated()); + } + + @Test + public void fieldInfoFormatNotUUID4_shouldNotAutoPopulate() { + Field FIELD = + Field.builder() + .setName("SampleField") + .setIsRequired(true) + .setFieldInfoFormat(Format.IPV6) + .setType(TypeNode.STRING) + .build(); + + assertEquals(false, FIELD.canBeAutoPopulated()); + } + + @Test + public void typeNotString_shouldNotAutoPopulate() { + Field FIELD = + Field.builder() + .setName("SampleField") + .setIsRequired(true) + .setFieldInfoFormat(Format.UUID4) + .setType(TypeNode.BOOLEAN) + .build(); + + assertEquals(false, FIELD.canBeAutoPopulated()); + } +} diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java index 16340e0a6b..8fdf2576c9 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/protoparser/ParserTest.java @@ -145,7 +145,14 @@ public void parseMethods_basic() { assertEquals(echoMethod.name(), "Echo"); assertEquals(echoMethod.stream(), Method.Stream.NONE); assertEquals(true, echoMethod.hasAutoPopulatedFields()); - assertEquals(Arrays.asList("request_id"), echoMethod.autoPopulatedFields()); + assertEquals( + Arrays.asList( + "request_id", + "second_request_id", + "third_request_id", + "fourth_request_id", + "non_existent_field"), + echoMethod.autoPopulatedFields()); // Detailed method signature parsing tests are in a separate unit test. List> methodSignatures = echoMethod.methodSignatures(); @@ -451,6 +458,21 @@ public void parseFields_autoPopulated() { field = message.fieldMap().get("severity"); assertEquals(false, field.isRequired()); assertEquals(null, field.fieldInfoFormat()); + field = message.fieldMap().get("second_request_id"); + assertEquals(false, field.isRequired()); + assertEquals(Format.UUID4, field.fieldInfoFormat()); + field = message.fieldMap().get("third_request_id"); + assertEquals(false, field.isRequired()); + assertEquals(Format.UUID4, field.fieldInfoFormat()); + field = message.fieldMap().get("fourth_request_id"); + assertEquals(false, field.isRequired()); + assertEquals(Format.IPV4_OR_IPV6, field.fieldInfoFormat()); + field = message.fieldMap().get("fifth_request_id"); + assertEquals(false, field.isRequired()); + assertEquals(Format.UUID4, field.fieldInfoFormat()); + field = message.fieldMap().get("sixth_request_id"); + assertEquals(true, field.isRequired()); + assertEquals(Format.UUID4, field.fieldInfoFormat()); message = messageTypes.get("com.google.showcase.v1beta1.ExpandRequest"); field = message.fieldMap().get("request_id"); @@ -465,7 +487,12 @@ public void parseAutoPopulatedMethodsAndFields_exists() { assertEquals( true, autoPopulatedMethodsWithFields.containsKey("google.showcase.v1beta1.Echo.Echo")); assertEquals( - Arrays.asList("request_id"), + Arrays.asList( + "request_id", + "second_request_id", + "third_request_id", + "fourth_request_id", + "non_existent_field"), autoPopulatedMethodsWithFields.get("google.showcase.v1beta1.Echo.Echo")); } diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java b/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java index 79d37baf4f..8a812ea437 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java @@ -27,6 +27,7 @@ import com.google.api.generator.gapic.protoparser.BatchingSettingsConfigParser; import com.google.api.generator.gapic.protoparser.Parser; import com.google.api.generator.gapic.protoparser.ServiceConfigParser; +import com.google.api.generator.gapic.protoparser.ServiceYamlParser; import com.google.bookshop.v1beta1.BookshopProto; import com.google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTestingOuterClass; import com.google.logging.v2.LogEntryProto; @@ -162,12 +163,18 @@ public GapicContext parseShowcaseEcho() { ServiceDescriptor echoServiceDescriptor = echoFileDescriptor.getServices().get(0); assertEquals(echoServiceDescriptor.getName(), "Echo"); + String serviceYamlFilename = "echo_v1beta1.yaml"; + Path serviceYamlPath = Paths.get(testFilesDirectory, serviceYamlFilename); + Optional serviceYamlOpt = + ServiceYamlParser.parse(serviceYamlPath.toString()); + assertTrue(serviceYamlOpt.isPresent()); + Map messageTypes = Parser.parseMessages(echoFileDescriptor); Map resourceNames = Parser.parseResourceNames(echoFileDescriptor); Set outputResourceNames = new HashSet<>(); List services = Parser.parseService( - echoFileDescriptor, messageTypes, resourceNames, Optional.empty(), outputResourceNames); + echoFileDescriptor, messageTypes, resourceNames, serviceYamlOpt, outputResourceNames); // Explicitly adds service description, since this is not parsed from source code location // in test protos, as it would from a protoc CodeGeneratorRequest diff --git a/gapic-generator-java/src/test/proto/echo.proto b/gapic-generator-java/src/test/proto/echo.proto index d963447340..effa0325cd 100644 --- a/gapic-generator-java/src/test/proto/echo.proto +++ b/gapic-generator-java/src/test/proto/echo.proto @@ -185,6 +185,34 @@ message EchoRequest { (google.api.field_info).format = UUID4 ]; + // This field is added to test that autopopulation works for multiple fields + string second_request_id = 8 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.field_info).format = UUID4 + ]; + + // This field is added to test that autopopulation should not populate this field since it is not of type String + bool third_request_id = 9 [ + (google.api.field_info).format = UUID4 + ]; + + // This field is added to test that autopopulation should not populate this field since it is not annotated with UUID4 format + string fourth_request_id = 10 [ + (google.api.field_info).format = IPV4_OR_IPV6 + ]; + + // This field is added to test that autopopulation should not populate this field since it is not designated in the service_yaml + string fifth_request_id = 11 [ + (google.api.field_info).format = UUID4 + ]; + + // This field is added to test that autopopulation should not populate this field since it marked as Required + string sixth_request_id = 12 [ + (google.api.field_info).format = UUID4, + (google.api.field_behavior) = REQUIRED + ]; + + oneof response { // The content to be echoed by the server. string content = 1; diff --git a/gapic-generator-java/src/test/resources/echo_v1beta1.yaml b/gapic-generator-java/src/test/resources/echo_v1beta1.yaml index 57d9f90115..a6aea48e87 100644 --- a/gapic-generator-java/src/test/resources/echo_v1beta1.yaml +++ b/gapic-generator-java/src/test/resources/echo_v1beta1.yaml @@ -97,7 +97,10 @@ http: - post: '/v1beta3/{name=operations/**}:cancel' publishing: method_settings: - # TODO: Add more test cases for scenarios where the field does not exist, the field is not a String, etc. Eventually the API Linter should handle some of those cases. - selector: google.showcase.v1beta1.Echo.Echo auto_populated_fields: - - request_id \ No newline at end of file + - request_id + - second_request_id + - third_request_id + - fourth_request_id + - non_existent_field \ No newline at end of file diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallSettings.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallSettings.java index a5aef3d69f..fae4ae9d25 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallSettings.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallSettings.java @@ -30,6 +30,7 @@ package com.google.api.gax.grpc; import com.google.api.core.BetaApi; +import com.google.api.gax.rpc.RequestMutator; import com.google.api.gax.rpc.RequestParamsExtractor; import io.grpc.MethodDescriptor; @@ -37,11 +38,13 @@ public class GrpcCallSettings { private final MethodDescriptor methodDescriptor; private final RequestParamsExtractor paramsExtractor; + private final RequestMutator requestMutator; private final boolean alwaysAwaitTrailers; private GrpcCallSettings(Builder builder) { this.methodDescriptor = builder.methodDescriptor; this.paramsExtractor = builder.paramsExtractor; + this.requestMutator = builder.requestMutator; this.alwaysAwaitTrailers = builder.shouldAwaitTrailers; } @@ -53,6 +56,10 @@ public RequestParamsExtractor getParamsExtractor() { return paramsExtractor; } + public RequestMutator getRequestMutator() { + return requestMutator; + } + @BetaApi public boolean shouldAwaitTrailers() { return alwaysAwaitTrailers; @@ -76,6 +83,8 @@ public Builder toBuilder() { public static class Builder { private MethodDescriptor methodDescriptor; private RequestParamsExtractor paramsExtractor; + + private RequestMutator requestMutator; private boolean shouldAwaitTrailers; private Builder() {} @@ -83,6 +92,7 @@ private Builder() {} private Builder(GrpcCallSettings settings) { this.methodDescriptor = settings.methodDescriptor; this.paramsExtractor = settings.paramsExtractor; + this.requestMutator = settings.requestMutator; this.shouldAwaitTrailers = settings.alwaysAwaitTrailers; } @@ -98,6 +108,11 @@ public Builder setParamsExtractor( return this; } + public Builder setRequestMutator(RequestMutator requestMutator) { + this.requestMutator = requestMutator; + return this; + } + @BetaApi public Builder setShouldAwaitTrailers(boolean b) { this.shouldAwaitTrailers = b; diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallableFactory.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallableFactory.java index 8a0c4e0b37..974feb0c43 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallableFactory.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallableFactory.java @@ -85,7 +85,13 @@ public static UnaryCallable createBas GrpcRawCallableFactory.createUnaryCallable( grpcCallSettings, callSettings.getRetryableCodes()); - callable = Callables.retrying(callable, callSettings, clientContext); + if (grpcCallSettings.getRequestMutator() != null) { + callable = + Callables.retrying( + callable, callSettings, clientContext, grpcCallSettings.getRequestMutator()); + } else { + callable = Callables.retrying(callable, callSettings, clientContext); + } return callable; } diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectCallable.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectCallable.java index 5b6a5f1bad..33041145dd 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectCallable.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectCallable.java @@ -30,6 +30,7 @@ package com.google.api.gax.grpc; import com.google.api.core.ApiFuture; +import com.google.api.core.InternalApi; import com.google.api.core.ListenableFutureToApiFuture; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.UnaryCallable; @@ -43,6 +44,7 @@ * *

Package-private for internal use. */ +@InternalApi class GrpcDirectCallable extends UnaryCallable { private final MethodDescriptor descriptor; private final boolean awaitTrailers; diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallSettings.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallSettings.java index 7dd7732175..04411fc3d7 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallSettings.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallSettings.java @@ -29,6 +29,7 @@ */ package com.google.api.gax.httpjson; +import com.google.api.gax.rpc.RequestMutator; import com.google.api.gax.rpc.RequestParamsExtractor; import com.google.protobuf.TypeRegistry; @@ -36,11 +37,14 @@ public class HttpJsonCallSettings { private final ApiMethodDescriptor methodDescriptor; private final RequestParamsExtractor paramsExtractor; + + private final RequestMutator requestMutator; private final TypeRegistry typeRegistry; private HttpJsonCallSettings(Builder builder) { this.methodDescriptor = builder.methodDescriptor; this.paramsExtractor = builder.paramsExtractor; + this.requestMutator = builder.requestMutator; this.typeRegistry = builder.typeRegistry; } @@ -52,6 +56,10 @@ public RequestParamsExtractor getParamsExtractor() { return paramsExtractor; } + public RequestMutator getRequestMutator() { + return requestMutator; + } + public TypeRegistry getTypeRegistry() { return typeRegistry; } @@ -72,6 +80,8 @@ public Builder toBuilder() { } public static class Builder { + + private RequestMutator requestMutator; private ApiMethodDescriptor methodDescriptor; private RequestParamsExtractor paramsExtractor; private TypeRegistry typeRegistry; @@ -94,6 +104,11 @@ public Builder setParamsExtractor( return this; } + public Builder setRequestMutator(RequestMutator requestMutator) { + this.requestMutator = requestMutator; + return this; + } + public Builder setTypeRegistry(TypeRegistry typeRegistry) { this.typeRegistry = typeRegistry; return this; diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java index d95751e3b0..33e2ff886e 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallableFactory.java @@ -30,6 +30,7 @@ package com.google.api.gax.httpjson; import com.google.api.core.InternalApi; +import com.google.api.core.ObsoleteApi; import com.google.api.gax.longrunning.OperationSnapshot; import com.google.api.gax.rpc.BatchingCallSettings; import com.google.api.gax.rpc.Callables; @@ -72,6 +73,22 @@ private static UnaryCallable createDi return callable; } + /** Create httpJson UnaryCallable with request mutator. */ + static UnaryCallable createUnaryCallable( + UnaryCallable innerCallable, + UnaryCallSettings callSettings, + HttpJsonCallSettings httpJsonCallSettings, + ClientContext clientContext) { + UnaryCallable callable = + new HttpJsonExceptionCallable<>(innerCallable, callSettings.getRetryableCodes()); + callable = + Callables.retrying( + callable, callSettings, clientContext, httpJsonCallSettings.getRequestMutator()); + return callable.withDefaultCallContext(clientContext.getDefaultCallContext()); + } + + /** Use {@link #createUnaryCallable createUnaryCallable} method instead. */ + @ObsoleteApi("Please use other httpJson UnaryCallable method instead") static UnaryCallable createUnaryCallable( UnaryCallable innerCallable, UnaryCallSettings callSettings, @@ -96,7 +113,9 @@ public static UnaryCallable createBas ClientContext clientContext) { UnaryCallable callable = createDirectUnaryCallable(httpJsonCallSettings); callable = new HttpJsonExceptionCallable<>(callable, callSettings.getRetryableCodes()); - callable = Callables.retrying(callable, callSettings, clientContext); + callable = + Callables.retrying( + callable, callSettings, clientContext, httpJsonCallSettings.getRequestMutator()); return callable; } @@ -123,7 +142,7 @@ public static UnaryCallable createUna clientContext.getTracerFactory(), getSpanName(httpJsonCallSettings.getMethodDescriptor())); - return createUnaryCallable(innerCallable, callSettings, clientContext); + return createUnaryCallable(innerCallable, callSettings, httpJsonCallSettings, clientContext); } /** @@ -141,7 +160,8 @@ UnaryCallable createPagedCallable( PagedCallSettings pagedCallSettings, ClientContext clientContext) { UnaryCallable callable = createDirectUnaryCallable(httpJsonCallSettings); - callable = createUnaryCallable(callable, pagedCallSettings, clientContext); + callable = + createUnaryCallable(callable, pagedCallSettings, httpJsonCallSettings, clientContext); UnaryCallable pagedCallable = Callables.paged(callable, pagedCallSettings); return pagedCallable.withDefaultCallContext(clientContext.getDefaultCallContext()); @@ -162,7 +182,8 @@ public static UnaryCallable createBat BatchingCallSettings batchingCallSettings, ClientContext clientContext) { UnaryCallable callable = createDirectUnaryCallable(httpJsonCallSettings); - callable = createUnaryCallable(callable, batchingCallSettings, clientContext); + callable = + createUnaryCallable(callable, batchingCallSettings, httpJsonCallSettings, clientContext); callable = Callables.batching(callable, batchingCallSettings, clientContext); return callable.withDefaultCallContext(clientContext.getDefaultCallContext()); } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java index d03d7e57f0..f7b9935d31 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/RetryingTest.java @@ -29,9 +29,14 @@ */ package com.google.api.gax.httpjson; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpMethods; import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpStatusCodes; import com.google.api.core.ApiFuture; @@ -50,15 +55,16 @@ import com.google.api.gax.rpc.UnaryCallable; import com.google.api.gax.rpc.UnknownException; import com.google.common.collect.ImmutableSet; -import com.google.common.truth.Truth; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.protobuf.TypeRegistry; import java.util.Set; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.threeten.bp.Duration; @@ -68,10 +74,27 @@ public class RetryingTest { @SuppressWarnings("unchecked") private final UnaryCallable callInt = Mockito.mock(UnaryCallable.class); + private final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR_FOR_REQUEST_MUTATOR = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.v1.Fake/FakeMethodForRequestMutator") + .setHttpMethod(HttpMethods.POST) + .setRequestFormatter(Mockito.mock(HttpRequestFormatter.class)) + .setResponseParser(Mockito.mock(HttpResponseParser.class)) + .build(); + + private final Integer initialRequest = 1; + private final Integer modifiedRequest = 0; + + private final HttpJsonCallSettings httpJsonCallSettings = + HttpJsonCallSettings.newBuilder() + .setRequestMutator(request -> modifiedRequest) + .setMethodDescriptor(FAKE_METHOD_DESCRIPTOR_FOR_REQUEST_MUTATOR) + .setTypeRegistry(TypeRegistry.newBuilder().build()) + .build(); + private RecordingScheduler executor; private FakeApiClock fakeClock; private ClientContext clientContext; - private static final int HTTP_CODE_PRECONDITION_FAILED = 412; private HttpResponseException HTTP_SERVICE_UNAVAILABLE_EXCEPTION = @@ -112,7 +135,7 @@ public void teardown() { @Test public void retry() { ImmutableSet retryable = ImmutableSet.of(Code.UNAVAILABLE); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) @@ -121,8 +144,14 @@ public void retry() { UnaryCallSettings callSettings = createSettings(retryable, FAST_RETRY_SETTINGS); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - Truth.assertThat(callable.call(1)).isEqualTo(2); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + assertThat(callable.call(initialRequest)).isEqualTo(2); + + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0, 0, 0, 0).inOrder(); } @Test @@ -140,7 +169,7 @@ public void retryTotalTimeoutExceeded() { httpResponseException, HttpJsonStatusCode.of(Code.FAILED_PRECONDITION), false); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(apiException)) .thenReturn(ApiFutures.immediateFuture(2)); @@ -152,14 +181,19 @@ public void retryTotalTimeoutExceeded() { .build(); UnaryCallSettings callSettings = createSettings(retryable, retrySettings); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - assertThrows(ApiException.class, () -> callable.call(1)); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + assertThrows(ApiException.class, () -> callable.call(initialRequest)); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0); } @Test public void retryMaxAttemptsExceeded() { ImmutableSet retryable = ImmutableSet.of(Code.UNAVAILABLE); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFuture(2)); @@ -167,14 +201,19 @@ public void retryMaxAttemptsExceeded() { RetrySettings retrySettings = FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(2).build(); UnaryCallSettings callSettings = createSettings(retryable, retrySettings); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - assertThrows(ApiException.class, () -> callable.call(1)); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + assertThrows(ApiException.class, () -> callable.call(initialRequest)); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0, 0).inOrder(); } @Test public void retryWithinMaxAttempts() { ImmutableSet retryable = ImmutableSet.of(Code.UNAVAILABLE); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFailedFuture(HTTP_SERVICE_UNAVAILABLE_EXCEPTION)) .thenReturn(ApiFutures.immediateFuture(2)); @@ -182,9 +221,13 @@ public void retryWithinMaxAttempts() { RetrySettings retrySettings = FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(3).build(); UnaryCallSettings callSettings = createSettings(retryable, retrySettings); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - callable.call(1); - Truth.assertThat(callable.call(1)).isEqualTo(2); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + assertThat(callable.call(initialRequest)).isEqualTo(2); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0, 0, 0).inOrder(); } @Test @@ -196,7 +239,7 @@ public void retryOnStatusUnknown() { "temporary redirect", new HttpHeaders()) .build(); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(throwable)) .thenReturn(ApiFutures.immediateFailedFuture(throwable)) .thenReturn(ApiFutures.immediateFailedFuture(throwable)) @@ -204,22 +247,32 @@ public void retryOnStatusUnknown() { UnaryCallSettings callSettings = createSettings(retryable, FAST_RETRY_SETTINGS); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - Truth.assertThat(callable.call(1)).isEqualTo(2); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + assertThat(callable.call(initialRequest)).isEqualTo(2); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0, 0, 0, 0).inOrder(); } @Test public void retryOnUnexpectedException() { ImmutableSet retryable = ImmutableSet.of(Code.UNKNOWN); Throwable throwable = new RuntimeException("foobar"); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(throwable)); UnaryCallSettings callSettings = createSettings(retryable, FAST_RETRY_SETTINGS); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - ApiException exception = assertThrows(ApiException.class, () -> callable.call(1)); - Truth.assertThat(exception).hasCauseThat().isSameInstanceAs(throwable); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + ApiException exception = assertThrows(ApiException.class, () -> callable.call(initialRequest)); + assertThat(exception).hasCauseThat().isSameInstanceAs(throwable); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0).inOrder(); } @Test @@ -235,15 +288,20 @@ public void retryNoRecover() { httpResponseException, HttpJsonStatusCode.of(Code.FAILED_PRECONDITION), false); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(apiException)) .thenReturn(ApiFutures.immediateFuture(2)); UnaryCallSettings callSettings = createSettings(retryable, FAST_RETRY_SETTINGS); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - ApiException exception = assertThrows(ApiException.class, () -> callable.call(1)); - Truth.assertThat(exception).isSameInstanceAs(apiException); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + ApiException exception = assertThrows(ApiException.class, () -> callable.call(initialRequest)); + assertThat(exception).isSameInstanceAs(apiException); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0); } @Test @@ -253,19 +311,24 @@ public void retryKeepFailing() { new HttpResponseException.Builder( HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE, "Unavailable", new HttpHeaders()) .build(); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(throwable)); UnaryCallSettings callSettings = createSettings(retryable, FAST_RETRY_SETTINGS); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); // Need to advance time inside the call. - ApiFuture future = callable.futureCall(1); + ApiFuture future = callable.futureCall(initialRequest); UncheckedExecutionException exception = assertThrows(UncheckedExecutionException.class, () -> Futures.getUnchecked(future)); - Truth.assertThat(exception).hasCauseThat().isInstanceOf(ApiException.class); - Truth.assertThat(exception).hasCauseThat().hasMessageThat().contains("Unavailable"); + assertThat(exception).hasCauseThat().isInstanceOf(ApiException.class); + assertThat(exception).hasCauseThat().hasMessageThat().contains("Unavailable"); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getValue()).isEqualTo(0); } @Test @@ -289,34 +352,45 @@ public void testKnownStatusCode() { HTTP_CODE_PRECONDITION_FAILED, "precondition failed", new HttpHeaders()) .setMessage(throwableMessage) .build(); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(throwable)); UnaryCallSettings callSettings = UnaryCallSettings.newUnaryCallSettingsBuilder() .setRetryableCodes(retryable) .build(); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); ApiException exception = - assertThrows(FailedPreconditionException.class, () -> callable.call(1)); - Truth.assertThat(exception.getStatusCode().getTransportCode()) + assertThrows(FailedPreconditionException.class, () -> callable.call(initialRequest)); + assertThat(exception.getStatusCode().getTransportCode()) .isEqualTo(HTTP_CODE_PRECONDITION_FAILED); - Truth.assertThat(exception).hasMessageThat().contains("precondition failed"); + assertThat(exception).hasMessageThat().contains("precondition failed"); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0).inOrder(); } @Test public void testUnknownStatusCode() { ImmutableSet retryable = ImmutableSet.of(); - Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + Mockito.when(callInt.futureCall((Integer) any(), (ApiCallContext) any())) .thenReturn(ApiFutures.immediateFailedFuture(new RuntimeException("unknown"))); UnaryCallSettings callSettings = UnaryCallSettings.newUnaryCallSettingsBuilder() .setRetryableCodes(retryable) .build(); UnaryCallable callable = - HttpJsonCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); - UnknownException exception = assertThrows(UnknownException.class, () -> callable.call(1)); - Truth.assertThat(exception).hasMessageThat().isEqualTo("java.lang.RuntimeException: unknown"); + HttpJsonCallableFactory.createUnaryCallable( + callInt, callSettings, httpJsonCallSettings, clientContext); + UnknownException exception = + assertThrows(UnknownException.class, () -> callable.call(initialRequest)); + assertThat(exception).hasMessageThat().isEqualTo("java.lang.RuntimeException: unknown"); + // Capture the argument passed to futureCall + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(callInt, atLeastOnce()).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + assertThat(argumentCaptor.getAllValues()).containsExactly(0).inOrder(); } public static UnaryCallSettings createSettings( diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/Callables.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/Callables.java index 28a76fe721..b1f4b51d6a 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/Callables.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/Callables.java @@ -51,11 +51,53 @@ public class Callables { private Callables() {} + /** + * Create a callable object that represents a Unary API method. Designed for use by generated + * code. + * + * @param innerCallable the callable to issue calls + * @param callSettings {@link UnaryCallSettings} to configure the unary call-related settings + * with. + * @param clientContext {@link ClientContext} to use to connect to the service. + * @return {@link UnaryCallable} callable object. + */ public static UnaryCallable retrying( UnaryCallable innerCallable, UnaryCallSettings callSettings, ClientContext clientContext) { + ScheduledRetryingExecutor retryingExecutor = + getRetryingExecutor(callSettings, clientContext); + return new RetryingCallable<>( + clientContext.getDefaultCallContext(), innerCallable, retryingExecutor); + } + + /** + * Create a callable object that represents a Unary API method that contains a Request Mutator. + * Designed for use by generated code. + * + * @param innerCallable the callable to issue calls + * @param callSettings {@link UnaryCallSettings} to configure the unary call-related settings + * with. + * @param clientContext {@link ClientContext} to use to connect to the service. + * @param requestMutator {@link RequestMutator} to modify the request. Currently only used for + * autopopulated fields. + * @return {@link UnaryCallable} callable object. + */ + public static UnaryCallable retrying( + UnaryCallable innerCallable, + UnaryCallSettings callSettings, + ClientContext clientContext, + RequestMutator requestMutator) { + + ScheduledRetryingExecutor retryingExecutor = + getRetryingExecutor(callSettings, clientContext); + return new RetryingCallable<>( + clientContext.getDefaultCallContext(), innerCallable, retryingExecutor, requestMutator); + } + + private static ScheduledRetryingExecutor getRetryingExecutor( + UnaryCallSettings callSettings, ClientContext clientContext) { UnaryCallSettings settings = callSettings; if (areRetriesDisabled(settings.getRetryableCodes(), settings.getRetrySettings())) { @@ -73,8 +115,7 @@ public static UnaryCallable retrying( new ExponentialRetryAlgorithm(settings.getRetrySettings(), clientContext.getClock())); ScheduledRetryingExecutor retryingExecutor = new ScheduledRetryingExecutor<>(retryAlgorithm, clientContext.getExecutor()); - return new RetryingCallable<>( - clientContext.getDefaultCallContext(), innerCallable, retryingExecutor); + return retryingExecutor; } public static ServerStreamingCallable retrying( diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/RequestMutator.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/RequestMutator.java new file mode 100644 index 0000000000..d26949d87d --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/RequestMutator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.InternalApi; + +/** + * A request mutator takes a {@code request} message, applies some Function to it, and then returns + * the modified {@code request} message. This is currently only used for autopopulation of the + * request ID. + * + *

Implementations of this interface are expected to be autogenerated. + * + * @param request message type + */ +@InternalApi("For use by transport-specific implementations") +@FunctionalInterface +public interface RequestMutator { + /** + * Applies a Function to {@code request} message + * + * @param request request message + */ + RequestT apply(RequestT request); +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/RetryingCallable.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/RetryingCallable.java index 0a92794a20..e4fe13295a 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/RetryingCallable.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/RetryingCallable.java @@ -43,20 +43,35 @@ class RetryingCallable extends UnaryCallable callable; private final RetryingExecutorWithContext executor; + private final RequestMutator requestMutator; + RetryingCallable( ApiCallContext callContextPrototype, UnaryCallable callable, RetryingExecutorWithContext executor) { + this(callContextPrototype, callable, executor, null); + } + + RetryingCallable( + ApiCallContext callContextPrototype, + UnaryCallable callable, + RetryingExecutorWithContext executor, + RequestMutator requestMutator) { this.callContextPrototype = Preconditions.checkNotNull(callContextPrototype); this.callable = Preconditions.checkNotNull(callable); this.executor = Preconditions.checkNotNull(executor); + this.requestMutator = requestMutator; } @Override public RetryingFuture futureCall(RequestT request, ApiCallContext inputContext) { ApiCallContext context = callContextPrototype.nullToSelf(inputContext); + RequestT modifiedRequest = request; + if (this.requestMutator != null) { + modifiedRequest = requestMutator.apply(request); + } AttemptCallable retryCallable = - new AttemptCallable<>(callable, request, context); + new AttemptCallable<>(callable, modifiedRequest, context); RetryingFuture retryingFuture = executor.createFuture(retryCallable, inputContext); retryCallable.setExternalFuture(retryingFuture); diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/CallableTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/CallableTest.java index 04e025fecb..8d24a19c53 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/CallableTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/CallableTest.java @@ -29,17 +29,20 @@ */ package com.google.api.gax.rpc; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFuture; import com.google.api.core.SettableApiFuture; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.testing.FakeCallContext; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; @@ -78,17 +81,28 @@ public void testNonRetriedCallable() throws Exception { innerResult = SettableApiFuture.create(); when(innerCallable.futureCall(anyString(), any(ApiCallContext.class))).thenReturn(innerResult); Duration timeout = Duration.ofMillis(5L); + String initialRequest = "Is your refrigerator running?"; + String modifiedRequest = "What about now?"; + + RequestMutator requestMutator = (request -> modifiedRequest); UnaryCallSettings callSettings = UnaryCallSettings.newUnaryCallSettingsBuilder().setSimpleTimeoutNoRetries(timeout).build(); UnaryCallable callable = - Callables.retrying(innerCallable, callSettings, clientContext); - innerResult.set("No, my refrigerator is not running!"); + Callables.retrying(innerCallable, callSettings, clientContext, requestMutator); + String expectedResponse = "No, my refrigerator is not running!"; + innerResult.set(expectedResponse); + + ApiFuture futureResponse = callable.futureCall(initialRequest, callContext); - callable.futureCall("Is your refrigerator running?", callContext); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + verify(innerCallable).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + String expectedRequest = "What about now?"; + assertEquals(expectedRequest, argumentCaptor.getValue()); verify(callContext, atLeastOnce()).getRetrySettings(); verify(callContext).getTimeout(); verify(callContext).withTimeout(timeout); + assertEquals(expectedResponse, futureResponse.get()); } @Test @@ -96,21 +110,33 @@ public void testNonRetriedCallableWithRetrySettings() throws Exception { innerResult = SettableApiFuture.create(); when(innerCallable.futureCall(anyString(), any(ApiCallContext.class))).thenReturn(innerResult); + String initialRequest = "Is your refrigerator running?"; + String modifiedRequest = "What about now?"; + RequestMutator requestMutator = (request -> modifiedRequest); + UnaryCallSettings callSettings = UnaryCallSettings.newUnaryCallSettingsBuilder() .setSimpleTimeoutNoRetries(Duration.ofMillis(10L)) .build(); UnaryCallable callable = - Callables.retrying(innerCallable, callSettings, clientContext); - innerResult.set("No, my refrigerator is not running!"); + Callables.retrying(innerCallable, callSettings, clientContext, requestMutator); + String expectedResponse = "No, my refrigerator is not running!"; + innerResult.set(expectedResponse); Duration timeout = retrySettings.getInitialRpcTimeout(); - callable.futureCall("Is your refrigerator running?", callContextWithRetrySettings); + ApiFuture futureResponse = + callable.futureCall(initialRequest, callContextWithRetrySettings); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + verify(innerCallable).futureCall(argumentCaptor.capture(), any(ApiCallContext.class)); + String expectedRequest = "What about now?"; + assertEquals(expectedRequest, argumentCaptor.getValue()); verify(callContextWithRetrySettings, atLeastOnce()).getRetrySettings(); verify(callContextWithRetrySettings).getTimeout(); verify(callContextWithRetrySettings).withTimeout(timeout); + assertEquals(expectedResponse, futureResponse.get()); } @Test diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/RetryingCallableTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/RetryingCallableTest.java new file mode 100644 index 0000000000..0411892979 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/RetryingCallableTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import static org.mockito.Mockito.when; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.retrying.RetryingExecutorWithContext; +import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.retrying.TimedAttemptSettings; +import com.google.api.gax.rpc.testing.FakeCallContext; +import java.util.concurrent.Callable; +import org.junit.Test; +import org.mockito.Mockito; +import org.threeten.bp.Duration; +import org.threeten.bp.temporal.ChronoUnit; + +public class RetryingCallableTest { + @Test + public void futureCall() { + FakeCallContext fakeCallContext = FakeCallContext.createDefault(); + UnaryCallable innerCallable = Mockito.mock(UnaryCallable.class); + RetryingExecutorWithContext executor = Mockito.mock(RetryingExecutorWithContext.class); + RetryingFuture retryingFuture = Mockito.mock(RetryingFuture.class); + TimedAttemptSettings fakeAttemptSettings = + TimedAttemptSettings.newBuilder() + .setRpcTimeout(Duration.of(1, ChronoUnit.SECONDS)) + .setAttemptCount(3) + .setGlobalSettings(RetrySettings.newBuilder().build()) + .setRetryDelay(Duration.of(1, ChronoUnit.SECONDS)) + .setRandomizedRetryDelay(Duration.of(1, ChronoUnit.SECONDS)) + .setFirstAttemptStartTimeNanos(5) + .build(); + + when(retryingFuture.getAttemptSettings()).thenReturn(fakeAttemptSettings); + when(executor.createFuture(Mockito.any(Callable.class), Mockito.eq(fakeCallContext))) + .thenReturn(retryingFuture); + Integer modifiedRequest = 5; + RequestMutator requestMutator = request -> modifiedRequest; + RetryingCallable retryingCallable = + new RetryingCallable<>(fakeCallContext, innerCallable, executor, requestMutator); + + retryingCallable.futureCall(1, fakeCallContext); + Mockito.verify(innerCallable) + .futureCall(Mockito.eq(modifiedRequest), Mockito.any(ApiCallContext.class)); + } +} diff --git a/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/GrpcEchoStub.java b/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/GrpcEchoStub.java index ebdcb34c32..217a66b80c 100644 --- a/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/GrpcEchoStub.java +++ b/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/GrpcEchoStub.java @@ -37,6 +37,7 @@ import com.google.cloud.location.ListLocationsRequest; import com.google.cloud.location.ListLocationsResponse; import com.google.cloud.location.Location; +import com.google.common.base.Strings; import com.google.iam.v1.GetIamPolicyRequest; import com.google.iam.v1.Policy; import com.google.iam.v1.SetIamPolicyRequest; @@ -62,6 +63,7 @@ import io.grpc.protobuf.ProtoUtils; import java.io.IOException; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Generated; @@ -305,6 +307,14 @@ protected GrpcEchoStub( builder.add(request.getOtherHeader(), "qux", ECHO_7_PATH_TEMPLATE); return builder.build(); }) + .setRequestMutator( + request -> { + EchoRequest.Builder requestBuilder = request.toBuilder(); + if (Strings.isNullOrEmpty(request.getRequestId())) { + requestBuilder.setRequestId(UUID.randomUUID().toString()); + } + return requestBuilder.build(); + }) .build(); GrpcCallSettings echoErrorDetailsTransportSettings = diff --git a/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/HttpJsonEchoStub.java b/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/HttpJsonEchoStub.java index 1116e8a9b2..80d9439ec9 100644 --- a/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/HttpJsonEchoStub.java +++ b/showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/stub/HttpJsonEchoStub.java @@ -46,6 +46,7 @@ import com.google.cloud.location.ListLocationsRequest; import com.google.cloud.location.ListLocationsResponse; import com.google.cloud.location.Location; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.iam.v1.GetIamPolicyRequest; import com.google.iam.v1.Policy; @@ -73,6 +74,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Generated; @@ -660,6 +662,14 @@ protected HttpJsonEchoStub( builder.add(request.getOtherHeader(), "qux", ECHO_7_PATH_TEMPLATE); return builder.build(); }) + .setRequestMutator( + request -> { + EchoRequest.Builder requestBuilder = request.toBuilder(); + if (Strings.isNullOrEmpty(request.getRequestId())) { + requestBuilder.setRequestId(UUID.randomUUID().toString()); + } + return requestBuilder.build(); + }) .build(); HttpJsonCallSettings echoErrorDetailsTransportSettings = diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITAutoPopulatedFields.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITAutoPopulatedFields.java new file mode 100644 index 0000000000..d448a2af8b --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITAutoPopulatedFields.java @@ -0,0 +1,373 @@ +/* + * Copyright 2024 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. + */ +package com.google.showcase.v1beta1.it; + +import static org.junit.Assert.assertThrows; + +import com.google.api.gax.httpjson.ApiMethodDescriptor; +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall; +import com.google.api.gax.httpjson.HttpJsonCallOptions; +import com.google.api.gax.httpjson.HttpJsonChannel; +import com.google.api.gax.httpjson.HttpJsonClientCall; +import com.google.api.gax.httpjson.HttpJsonClientInterceptor; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.Truth; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoResponse; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.MethodDescriptor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.threeten.bp.Duration; + +public class ITAutoPopulatedFields { + + private static class HttpJsonInterceptor implements HttpJsonClientInterceptor { + private Consumer onRequestIntercepted; + + private HttpJsonInterceptor() {} + + private void setOnRequestIntercepted(Consumer onRequestIntercepted) { + this.onRequestIntercepted = onRequestIntercepted; + } + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + HttpJsonClientCall call = next.newCall(method, callOptions); + + return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall( + call) { + @Override + public void sendMessage(ReqT message) { + // Capture the request message + if (onRequestIntercepted != null) { + onRequestIntercepted.accept(message); + } + super.sendMessage(message); + } + }; + } + } + + // Implement a request interceptor to retrieve the request ID being sent on the request. + private static class GRPCInterceptor implements ClientInterceptor { + private Consumer onRequestIntercepted; + + private GRPCInterceptor() {} + + private void setOnRequestIntercepted(Consumer onRequestIntercepted) { + this.onRequestIntercepted = onRequestIntercepted; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void sendMessage(ReqT message) { + // Capture the request message + if (onRequestIntercepted != null) { + onRequestIntercepted.accept(message); + } + super.sendMessage(message); + } + }; + } + } + + private GRPCInterceptor grpcRequestInterceptor; + private HttpJsonInterceptor httpJsonInterceptor; + private EchoClient grpcClientWithoutRetries; + private EchoClient grpcClientWithRetries; + + private EchoClient httpJsonClient; + private EchoClient httpJsonClientWithRetries; + + @Before + public void createClients() throws Exception { + RetrySettings defaultRetrySettings = + RetrySettings.newBuilder() + .setInitialRpcTimeout(Duration.ofMillis(5000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(5000L)) + .setTotalTimeout(Duration.ofMillis(5000L)) + // Cap retries at 5 + .setMaxAttempts(5) + .build(); + + // Adding `Code.INTERNAL` is necessary because for httpJson requests, the httpJson status code + // is mapped here: + // https://github.com/googleapis/sdk-platform-java/blob/acdde47445916dd306ce8b91489fab45c9c2ef50/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonStatusCode.java#L96-L133 + // Therefore, just setting the error code to `Code.UNKNOWN` for httpJson will get translated + // instead to `Code.INTERNAL`. + Set retryableCodes = ImmutableSet.of(Code.UNKNOWN, Code.INTERNAL); + + // Create gRPC Interceptor and Client + grpcRequestInterceptor = new ITAutoPopulatedFields.GRPCInterceptor(); + grpcClientWithoutRetries = + TestClientInitializer.createGrpcEchoClient(ImmutableList.of(grpcRequestInterceptor)); + grpcClientWithRetries = + TestClientInitializer.createGrpcEchoClientWithRetrySettings( + defaultRetrySettings, retryableCodes, ImmutableList.of(grpcRequestInterceptor)); + + // Create HttpJson Interceptor and Client + httpJsonInterceptor = new ITAutoPopulatedFields.HttpJsonInterceptor(); + httpJsonClient = + TestClientInitializer.createHttpJsonEchoClient(ImmutableList.of(httpJsonInterceptor)); + httpJsonClientWithRetries = + TestClientInitializer.createHttpJsonEchoClientWithRetrySettings( + defaultRetrySettings, retryableCodes, ImmutableList.of(httpJsonInterceptor)); + } + + @After + public void destroyClient() { + grpcClientWithoutRetries.close(); + grpcClientWithRetries.close(); + httpJsonClient.close(); + } + + @Test + public void testGrpc_autoPopulateRequestIdWhenAttemptedOnceSuccessfully() { + List capturedRequestIds = new ArrayList<>(); + grpcRequestInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + grpcClientWithoutRetries.echo(EchoRequest.newBuilder().build()); + Truth.assertThat(capturedRequestIds).isNotEmpty(); + // Autopopulation of UUID is currently only configured for format UUID4. + Integer UUIDVersion = 4; + Truth.assertThat(UUID.fromString(capturedRequestIds.get(0)).version()).isEqualTo(UUIDVersion); + } + + @Test + public void testGrpc_shouldNotAutoPopulateRequestIdIfSetInRequest() { + List capturedRequestIds = new ArrayList<>(); + grpcRequestInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + String UUIDsent = UUID.randomUUID().toString(); + grpcClientWithoutRetries.echo(EchoRequest.newBuilder().setRequestId(UUIDsent).build()); + Truth.assertThat(capturedRequestIds).isNotEmpty(); + Truth.assertThat(capturedRequestIds).contains(UUIDsent); + } + + @Test + public void testHttpJson_autoPopulateRequestIdWhenAttemptedOnceSuccessfully() { + List capturedRequestIds = new ArrayList<>(); + httpJsonInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + httpJsonClient.echo(EchoRequest.newBuilder().build()); + Truth.assertThat(capturedRequestIds).isNotEmpty(); + // Autopopulation of UUID is currently only configured for format UUID4. + Integer UUIDVersion = 4; + Truth.assertThat(UUID.fromString(capturedRequestIds.get(0)).version()).isEqualTo(UUIDVersion); + } + + @Test + public void testHttpJson_shouldNotAutoPopulateRequestIdIfSetInRequest() { + String UUIDsent = UUID.randomUUID().toString(); + List capturedRequestIds = new ArrayList<>(); + httpJsonInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + httpJsonClient.echo(EchoRequest.newBuilder().setRequestId(UUIDsent).build()); + Truth.assertThat(capturedRequestIds).isNotEmpty(); + Truth.assertThat(capturedRequestIds).contains(UUIDsent); + } + + @Test + public void testGRPC_setsSameRequestIdIfSetInRequestWhenRequestsAreRetried() throws Exception { + List capturedRequestIds = new ArrayList<>(); + grpcRequestInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + String UUIDsent = UUID.randomUUID().toString(); + EchoRequest requestSent = + EchoRequest.newBuilder() + .setRequestId(UUIDsent) + .setError(Status.newBuilder().setCode(Code.UNKNOWN.ordinal()).build()) + .build(); + + try { + RetryingFuture retryingFuture = + (RetryingFuture) + grpcClientWithRetries.echoCallable().futureCall(requestSent); + assertThrows(ExecutionException.class, () -> retryingFuture.get(10, TimeUnit.SECONDS)); + // assert that the number of request IDs is equal to the max attempt + Truth.assertThat(capturedRequestIds).hasSize(5); + // assert that each request ID sent is the same as the UUIDSent + Truth.assertThat(capturedRequestIds) + .containsExactly(UUIDsent, UUIDsent, UUIDsent, UUIDsent, UUIDsent); + } finally { + grpcClientWithRetries.close(); + grpcClientWithRetries.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testGRPC_setsSameAutoPopulatedRequestIdWhenRequestsAreRetried() throws Exception { + List capturedRequestIds = new ArrayList<>(); + grpcRequestInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + + EchoRequest requestSent = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(Code.UNKNOWN.ordinal()).build()) + .build(); + + try { + RetryingFuture retryingFuture = + (RetryingFuture) + grpcClientWithRetries.echoCallable().futureCall(requestSent); + assertThrows(ExecutionException.class, () -> retryingFuture.get(10, TimeUnit.SECONDS)); + // assert that the number of request IDs is equal to the max attempt + Truth.assertThat(capturedRequestIds).hasSize(5); + // assert that each request ID sent is the same + Truth.assertThat(capturedRequestIds) + .containsExactly( + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0)); + } finally { + grpcClientWithRetries.close(); + grpcClientWithRetries.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testHttpJson_setsSameRequestIdIfSetInRequestWhenRequestsAreRetried() + throws Exception { + List capturedRequestIds = new ArrayList<>(); + httpJsonInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + String UUIDsent = UUID.randomUUID().toString(); + EchoRequest requestSent = + EchoRequest.newBuilder() + .setRequestId(UUIDsent) + .setError(Status.newBuilder().setCode(Code.UNKNOWN.getHttpStatusCode()).build()) + .build(); + try { + RetryingFuture retryingFuture = + (RetryingFuture) + httpJsonClientWithRetries.echoCallable().futureCall(requestSent); + assertThrows(ExecutionException.class, () -> retryingFuture.get(10, TimeUnit.SECONDS)); + // assert that the number of request IDs is equal to the max attempt + Truth.assertThat(capturedRequestIds).hasSize(5); + // assert that each request ID sent is the same as the UUIDSent + Truth.assertThat(capturedRequestIds) + .containsExactly(UUIDsent, UUIDsent, UUIDsent, UUIDsent, UUIDsent); + } finally { + httpJsonClientWithRetries.close(); + httpJsonClientWithRetries.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } + + @Test + public void testHttpJson_setsSameAutoPopulatedRequestIdWhenRequestsAreRetried() throws Exception { + List capturedRequestIds = new ArrayList<>(); + httpJsonInterceptor.setOnRequestIntercepted( + request -> { + if (request instanceof EchoRequest) { + EchoRequest echoRequest = (EchoRequest) request; + capturedRequestIds.add(echoRequest.getRequestId()); + } + }); + EchoRequest requestSent = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(Code.UNKNOWN.getHttpStatusCode()).build()) + .build(); + try { + RetryingFuture retryingFuture = + (RetryingFuture) + httpJsonClientWithRetries.echoCallable().futureCall(requestSent); + assertThrows(ExecutionException.class, () -> retryingFuture.get(10, TimeUnit.SECONDS)); + // assert that the number of request IDs is equal to the max attempt + Truth.assertThat(capturedRequestIds).hasSize(5); + // assert that each request ID sent is the same + Truth.assertThat(capturedRequestIds) + .containsExactly( + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0), + capturedRequestIds.get(0)); + } finally { + httpJsonClientWithRetries.close(); + httpJsonClientWithRetries.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } +} diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index 9fbedbc107..640df1489d 100644 --- a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -61,6 +61,31 @@ public static EchoClient createGrpcEchoClient(List intercepto return EchoClient.create(grpcEchoSettings); } + public static EchoClient createGrpcEchoClientWithRetrySettings( + RetrySettings retrySettings, + Set retryableCodes, + List interceptorList) + throws Exception { + EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder(); + grpcEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(retryableCodes); + EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build()); + grpcEchoSettings = + grpcEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .setInterceptorProvider(() -> interceptorList) + .build()) + .setEndpoint("localhost:7469") + .build(); + return EchoClient.create(grpcEchoSettings); + } + public static EchoClient createHttpJsonEchoClient() throws Exception { return createHttpJsonEchoClient(ImmutableList.of()); } @@ -81,6 +106,32 @@ public static EchoClient createHttpJsonEchoClient(List retryableCodes, + List interceptorList) + throws Exception { + EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder(); + httpJsonEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(retryableCodes); + EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build()); + httpJsonEchoSettings = + httpJsonEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setInterceptorProvider(() -> interceptorList) + .setEndpoint("http://localhost:7469") + .build()) + .build(); + return EchoClient.create(httpJsonEchoSettings); + } + public static EchoClient createGrpcEchoClientCustomBlockSettings( RetrySettings retrySettings, Set retryableCodes) throws Exception { EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder();