Skip to content

Commit

Permalink
feat: autopopulate fields in the request (#2353)
Browse files Browse the repository at this point in the history
* feat: autopopulate fields in the request

* revert showcase golden changes

* add AbstractTransportServiceStubClassComposerTest

* add REST implementation

* add GrpcDirectCallableTest

* remove unnecessary null check

* add field info proto registry and more unit test cases

* add HttpJsonDirectCallableTest

* refactor AbstractTransportServiceSTubClassComposer

* add more unit tests for Field

* feat: refactor request mutator expression

* fix goldens

* add showcase test for autopopulation

* fix lint

* change assertion in showcase test

* refactor for sonarcloud

* sonarcloud fixes

* sonarcloud

* sonarcloud fix

* fix sonarcloud

* slight refactoring

* revert changes to directCallable and replace with retryable Callable

* overload retrying Callables method

* change license header format

* fix license header

* fix showcase lint

* add comment

* add showcase comment

* add CallableTest and httpjson Retrying test

* fix lint

* add RetryingCallable test and some refactoring

* refactor GrpcCallableFactory

* remove extraneous from HttpJsonDirectCallableTest

* remove FakeHttpJsonChannel

* revert changes to tests for extra param

* refactoring

* Update gapic-generator-java/src/test/java/com/google/api/generator/gapic/model/FieldTest.java

Co-authored-by: Blake Li <[email protected]>

* Update gax-java/gax/src/test/java/com/google/api/gax/rpc/RetryingCallableTest.java

Co-authored-by: Blake Li <[email protected]>

---------

Co-authored-by: Blake Li <[email protected]>
  • Loading branch information
alicejli and blakeli0 authored Jan 31, 2024
1 parent 9916540 commit b28235a
Show file tree
Hide file tree
Showing 40 changed files with 1,928 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -136,6 +142,7 @@ private static TypeStore createStaticTypes() {
OperationCallable.class,
OperationSnapshot.class,
RequestParamsExtractor.class,
UUID.class,
ServerStreamingCallable.class,
TimeUnit.class,
TypeRegistry.class,
Expand Down Expand Up @@ -277,7 +284,8 @@ protected Expr createTransportSettingsInitExpr(
Method method,
VariableExpr transportSettingsVarExpr,
VariableExpr methodDescriptorVarExpr,
List<Statement> classStatements) {
List<Statement> classStatements,
ImmutableMap<String, Message> messageTypes) {
MethodInvocationExpr callSettingsBuilderExpr =
MethodInvocationExpr.builder()
.setStaticReferenceType(getTransportContext().transportCallSettingsType())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -760,7 +777,8 @@ protected List<MethodDefinition> 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()));
Expand Down Expand Up @@ -1233,12 +1251,197 @@ protected TypeNode getTransportOperationsStubType(Service service) {
return transportOpeationsStubType;
}

protected static LambdaExpr createRequestMutatorClassInstance(
Method method, ImmutableMap<String, Message> messageTypes) {
List<Statement> 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<Statement> createRequestMutatorBody(
Method method,
ImmutableMap<String, Message> messageTypes,
List<Statement> 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<String, Message> 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<String> shouldAutoPopulate(Message methodRequestMessage) {
return fieldName ->
methodRequestMessage.fields().stream()
.anyMatch(field -> field.name().equals(fieldName) && field.canBeAutoPopulated());
}

protected LambdaExpr createRequestParamsExtractorClassInstance(
Method method, List<Statement> classStatements) {
List<Statement> 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()
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit b28235a

Please sign in to comment.