From bcc1bdb5fdf57d8843a03318d746df7d8392d848 Mon Sep 17 00:00:00 2001 From: Blake Li Date: Fri, 28 Jan 2022 00:48:00 -0500 Subject: [PATCH] feat: Support explicit dynamic routing header (#887) This PR is to support explicit dynamic routing header feature proposed in go/actools-dynamic-routing-proposal. All examples provided in routing.proto are covered by golden files unit tests. Added validation that the field has to be of String type as suggested(Compare to implicit routing headers support primitive types as well). For nested fields, when getting field values, added null check for each level to prevent null pointer exception during runtime. --- WORKSPACE | 2 +- ...ractTransportServiceStubClassComposer.java | 19 +- .../grpc/GrpcServiceStubClassComposer.java | 259 ++++++-- .../HttpJsonServiceStubClassComposer.java | 5 +- .../api/generator/gapic/model/Message.java | 41 ++ .../api/generator/gapic/model/Method.java | 13 + .../gapic/model/RoutingHeaderRule.java | 68 ++ .../generator/gapic/protoparser/BUILD.bazel | 1 + .../gapic/protoparser/HttpRuleParser.java | 19 +- .../generator/gapic/protoparser/Parser.java | 4 + .../gapic/protoparser/PatternParser.java | 36 ++ .../gapic/protoparser/RoutingRuleParser.java | 66 ++ .../gapic/composer/common/BUILD.bazel | 1 + .../composer/common/TestProtoLoader.java | 26 + .../generator/gapic/composer/grpc/BUILD.bazel | 1 + .../GrpcServiceStubClassComposerTest.java | 15 + .../goldens/GrpcRoutingHeadersStub.golden | 594 ++++++++++++++++++ .../grpc/goldens/GrpcTestingStub.golden | 17 +- .../api/generator/gapic/model/BUILD.bazel | 2 + .../generator/gapic/model/MessageTest.java | 151 +++++ .../api/generator/gapic/model/MethodTest.java | 83 +++ .../gapic/model/RoutingHeaderParamTest.java | 32 + .../generator/gapic/protoparser/BUILD.bazel | 3 + .../gapic/protoparser/PatternParserTest.java | 40 ++ .../protoparser/RoutingRuleParserTest.java | 112 ++++ .../api/generator/gapic/testdata/BUILD.bazel | 20 + ...licit_dynamic_routing_header_testing.proto | 304 +++++++++ .../routing_rule_parser_testing.proto | 143 +++++ .../generator/gapic/testdata/testing.proto | 16 + 29 files changed, 2014 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/google/api/generator/gapic/model/RoutingHeaderRule.java create mode 100644 src/main/java/com/google/api/generator/gapic/protoparser/PatternParser.java create mode 100644 src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java create mode 100644 src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcRoutingHeadersStub.golden create mode 100644 src/test/java/com/google/api/generator/gapic/model/MessageTest.java create mode 100644 src/test/java/com/google/api/generator/gapic/model/RoutingHeaderParamTest.java create mode 100644 src/test/java/com/google/api/generator/gapic/protoparser/PatternParserTest.java create mode 100644 src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java create mode 100644 src/test/java/com/google/api/generator/gapic/testdata/explicit_dynamic_routing_header_testing.proto create mode 100644 src/test/java/com/google/api/generator/gapic/testdata/routing_rule_parser_testing.proto diff --git a/WORKSPACE b/WORKSPACE index 87f44eeca7..1b5ed1b3db 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -33,7 +33,7 @@ jvm_maven_import_external( # which in its turn, prioritizes actual generated clients runtime dependencies # over the generator dependencies. -_gax_java_version = "2.10.0" +_gax_java_version = "2.11.0" http_archive( name = "com_google_api_gax_java", diff --git a/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java index 1b4827ef70..df41692e61 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java @@ -51,7 +51,6 @@ import com.google.api.generator.engine.ast.VariableExpr; 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.ClassNames; import com.google.api.generator.gapic.composer.utils.PackageChecker; import com.google.api.generator.gapic.model.GapicClass; import com.google.api.generator.gapic.model.GapicClass.Kind; @@ -204,7 +203,6 @@ public GapicClass generate(GapicContext context, Service service) { .setName(className) .setExtendsType( typeStore.get(getTransportContext().classNames().getServiceStubClassName(service))) - .setStatements(classStatements) .setMethods( createClassMethods( context, @@ -212,7 +210,8 @@ public GapicClass generate(GapicContext context, Service service) { typeStore, classMemberVarExprs, callableClassMemberVarExprs, - protoMethodNameToDescriptorVarExprs)) + protoMethodNameToDescriptorVarExprs, classStatements)) + .setStatements(classStatements) .build(); return GapicClass.create(kind, classDef); } @@ -249,7 +248,8 @@ protected List createOperationsStubGetterMethod( } protected abstract Expr createTransportSettingsInitExpr( - Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr); + Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr, + List classStatements); protected List createGetMethodDescriptorsMethod( Service service, @@ -430,7 +430,8 @@ protected List createClassMethods( TypeStore typeStore, Map classMemberVarExprs, Map callableClassMemberVarExprs, - Map protoMethodNameToDescriptorVarExprs) { + Map protoMethodNameToDescriptorVarExprs, + List classStatements) { List javaMethods = new ArrayList<>(); javaMethods.addAll(createStaticCreatorMethods(service, typeStore, "newBuilder")); javaMethods.addAll( @@ -440,7 +441,8 @@ protected List createClassMethods( typeStore, classMemberVarExprs, callableClassMemberVarExprs, - protoMethodNameToDescriptorVarExprs)); + protoMethodNameToDescriptorVarExprs, + classStatements)); javaMethods.addAll( createGetMethodDescriptorsMethod(service, typeStore, protoMethodNameToDescriptorVarExprs)); javaMethods.addAll( @@ -541,7 +543,8 @@ protected List createConstructorMethods( TypeStore typeStore, Map classMemberVarExprs, Map callableClassMemberVarExprs, - Map protoMethodNameToDescriptorVarExprs) { + Map protoMethodNameToDescriptorVarExprs, + List classStatements) { TypeNode stubSettingsType = typeStore.get(getTransportContext().classNames().getServiceStubSettingsClassName(service)); VariableExpr settingsVarExpr = @@ -666,7 +669,7 @@ protected List createConstructorMethods( m, javaStyleMethodNameToTransportSettingsVarExprs.get( JavaStyle.toLowerCamelCase(m.name())), - protoMethodNameToDescriptorVarExprs.get(m.name()))) + protoMethodNameToDescriptorVarExprs.get(m.name()), classStatements)) .collect(Collectors.toList())); secondCtorStatements.addAll( secondCtorExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())); diff --git a/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java index c82de5552c..a33056dad4 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java @@ -16,13 +16,17 @@ import com.google.api.gax.grpc.GrpcCallSettings; import com.google.api.gax.grpc.GrpcStubCallableFactory; +import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.generator.engine.ast.AssignmentExpr; import com.google.api.generator.engine.ast.ConcreteReference; import com.google.api.generator.engine.ast.EnumRefExpr; import com.google.api.generator.engine.ast.Expr; import com.google.api.generator.engine.ast.ExprStatement; +import com.google.api.generator.engine.ast.IfStatement; import com.google.api.generator.engine.ast.LambdaExpr; +import com.google.api.generator.engine.ast.LogicalOperationExpr; import com.google.api.generator.engine.ast.MethodInvocationExpr; +import com.google.api.generator.engine.ast.RelationalOperationExpr; import com.google.api.generator.engine.ast.ScopeNode; import com.google.api.generator.engine.ast.Statement; import com.google.api.generator.engine.ast.StringObjectValue; @@ -35,9 +39,12 @@ import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.Method; +import com.google.api.generator.gapic.model.RoutingHeaderRule.RoutingHeaderParam; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.gapic.utils.JavaStyle; -import com.google.common.base.Preconditions; +import com.google.api.pathtemplate.PathTemplate; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.longrunning.stub.GrpcOperationsStub; import io.grpc.MethodDescriptor; @@ -50,9 +57,9 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.stream.Collectors; public class GrpcServiceStubClassComposer extends AbstractTransportServiceStubClassComposer { + private static final GrpcServiceStubClassComposer INSTANCE = new GrpcServiceStubClassComposer(); // Legacy support for the original reroute_to_grpc_interface option in gapic.yaml. These two APIs @@ -194,7 +201,10 @@ protected EnumRefExpr getMethodDescriptorMethodTypeExpr(Method protoMethod) { @Override protected Expr createTransportSettingsInitExpr( - Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr) { + Method method, + VariableExpr transportSettingsVarExpr, + VariableExpr methodDescriptorVarExpr, + List classStatements) { MethodInvocationExpr callSettingsBuilderExpr = MethodInvocationExpr.builder() .setStaticReferenceType(getTransportContext().transportCallSettingsType()) @@ -208,12 +218,12 @@ protected Expr createTransportSettingsInitExpr( .setArguments(Arrays.asList(methodDescriptorVarExpr)) .build(); - if (method.hasHttpBindings()) { + if (method.shouldSetParamsExtractor()) { callSettingsBuilderExpr = MethodInvocationExpr.builder() .setExprReferenceExpr(callSettingsBuilderExpr) .setMethodName("setParamsExtractor") - .setArguments(createRequestParamsExtractorClassInstance(method)) + .setArguments(createRequestParamsExtractorClassInstance(method, classStatements)) .build(); } @@ -245,10 +255,44 @@ protected String getProtoRpcFullMethodName(Service protoService, Method protoMet return String.format("google.iam.v1.IAMPolicy/%s", protoMethod.name()); } - private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { - Preconditions.checkState( - method.hasHttpBindings(), String.format("Method %s has no HTTP binding", method.name())); + private LambdaExpr createRequestParamsExtractorClassInstance( + Method method, List classStatements) { + List bodyStatements = new ArrayList<>(); + VariableExpr requestVarExpr = + VariableExpr.withVariable( + Variable.builder().setType(method.inputType()).setName("request").build()); + TypeNode returnType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(Map.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + MethodInvocationExpr.Builder returnExpr = + MethodInvocationExpr.builder().setReturnType(returnType); + // If the google.api.routing annotation is present(even with empty routing parameters), + // the implicit routing headers specified in the google.api.http annotation should not be sent + if (method.routingHeaderRule() == null) { + createRequestParamsExtractorBodyForHttpBindings( + method, requestVarExpr, bodyStatements, returnExpr); + } else { + createRequestParamsExtractorBodyForRoutingHeaders( + method, requestVarExpr, classStatements, bodyStatements, returnExpr); + } + // Overrides extract(). + // https://github.com/googleapis/gax-java/blob/8d45d186e36ae97b789a6f89d80ae5213a773b65/gax/src/main/java/com/google/api/gax/rpc/RequestParamsExtractor.java#L55 + return LambdaExpr.builder() + .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) + .setBody(bodyStatements) + .setReturnExpr(returnExpr.build()) + .build(); + } + + private void createRequestParamsExtractorBodyForHttpBindings( + Method method, + VariableExpr requestVarExpr, + List bodyStatements, + MethodInvocationExpr.Builder returnExprBuilder) { TypeNode paramsVarType = TypeNode.withReference( ConcreteReference.builder() @@ -269,32 +313,11 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { .setReturnType(paramsVarType) .build()) .build(); - List bodyExprs = new ArrayList<>(); - bodyExprs.add(paramsAssignExpr); - - VariableExpr requestVarExpr = - VariableExpr.withVariable( - Variable.builder().setType(method.inputType()).setName("request").build()); + bodyStatements.add(ExprStatement.withExpr(paramsAssignExpr)); for (HttpBinding httpBindingFieldBinding : method.httpBindings().pathParameters()) { - // Handle foo.bar cases by descending into the subfields. - MethodInvocationExpr.Builder requestFieldGetterExprBuilder = - MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); - String[] descendantFields = httpBindingFieldBinding.name().split("\\."); - for (int i = 0; i < descendantFields.length; i++) { - String currFieldName = descendantFields[i]; - String bindingFieldMethodName = - String.format("get%s", JavaStyle.toUpperCamelCase(currFieldName)); - requestFieldGetterExprBuilder = - requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); - if (i < descendantFields.length - 1) { - requestFieldGetterExprBuilder = - MethodInvocationExpr.builder() - .setExprReferenceExpr(requestFieldGetterExprBuilder.build()); - } - } - - MethodInvocationExpr requestBuilderExpr = requestFieldGetterExprBuilder.build(); + MethodInvocationExpr requestBuilderExpr = + createRequestFieldGetterExpr(requestVarExpr, httpBindingFieldBinding.name()); Expr valueOfExpr = MethodInvocationExpr.builder() .setStaticReferenceType(TypeNode.STRING) @@ -310,29 +333,165 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { ValueExpr.withValue(StringObjectValue.withValue(httpBindingFieldBinding.name())), valueOfExpr) .build(); - bodyExprs.add(paramsPutExpr); + bodyStatements.add(ExprStatement.withExpr(paramsPutExpr)); } - TypeNode returnType = + returnExprBuilder.setExprReferenceExpr(paramsVarExpr).setMethodName("build"); + } + + private void createRequestParamsExtractorBodyForRoutingHeaders( + Method method, + VariableExpr requestVarExpr, + List classStatements, + List bodyStatements, + MethodInvocationExpr.Builder returnExprBuilder) { + TypeNode routingHeadersBuilderType = TypeNode.withReference( - ConcreteReference.builder() - .setClazz(Map.class) - .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) - .build()); - Expr returnExpr = + ConcreteReference.builder().setClazz(RequestParamsBuilder.class).build()); + VariableExpr routingHeadersBuilderVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("builder").setType(routingHeadersBuilderType).build()) + .setIsDecl(true) + .build(); + MethodInvocationExpr routingHeaderBuilderInvokeExpr = MethodInvocationExpr.builder() - .setExprReferenceExpr(paramsVarExpr) - .setMethodName("build") - .setReturnType(returnType) + .setStaticReferenceType(routingHeadersBuilderType) + .setMethodName("create") + .setReturnType(routingHeadersBuilderType) + .build(); + Expr routingHeadersBuilderInitExpr = + AssignmentExpr.builder() + .setVariableExpr(routingHeadersBuilderVarExpr) + .setValueExpr(routingHeaderBuilderInvokeExpr) + .build(); + bodyStatements.add(ExprStatement.withExpr(routingHeadersBuilderInitExpr)); + List routingHeaderParams = method.routingHeaderRule().routingHeaderParams(); + VariableExpr routingHeadersBuilderVarNonDeclExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("builder").setType(routingHeadersBuilderType).build()) .build(); + for (int i = 0; i < routingHeaderParams.size(); i++) { + RoutingHeaderParam routingHeaderParam = routingHeaderParams.get(i); + MethodInvocationExpr requestFieldGetterExpr = + createRequestFieldGetterExpr(requestVarExpr, routingHeaderParam.fieldName()); + Expr routingHeaderKeyExpr = + ValueExpr.withValue(StringObjectValue.withValue(routingHeaderParam.key())); + String pathTemplateName = + String.format("%s_%s_PATH_TEMPLATE", JavaStyle.toUpperSnakeCase(method.name()), i); + TypeNode pathTemplateType = + TypeNode.withReference(ConcreteReference.withClazz(PathTemplate.class)); + Variable pathTemplateVar = + Variable.builder().setType(pathTemplateType).setName(pathTemplateName).build(); + Expr routingHeaderPatternExpr = VariableExpr.withVariable(pathTemplateVar); + Statement pathTemplateClassVar = + createPathTemplateClassStatement(routingHeaderParam, pathTemplateType, pathTemplateVar); + classStatements.add(pathTemplateClassVar); + MethodInvocationExpr addParamMethodExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(routingHeadersBuilderVarNonDeclExpr) + .setMethodName("add") + .setArguments(requestFieldGetterExpr, routingHeaderKeyExpr, routingHeaderPatternExpr) + .build(); - // Overrides extract(). - // https://github.com/googleapis/gax-java/blob/8d45d186e36ae97b789a6f89d80ae5213a773b65/gax/src/main/java/com/google/api/gax/rpc/RequestParamsExtractor.java#L55 - return LambdaExpr.builder() - .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) - .setBody( - bodyExprs.stream().map(e -> ExprStatement.withExpr(e)).collect(Collectors.toList())) - .setReturnExpr(returnExpr) - .build(); + ExprStatement addParamStatement = ExprStatement.withExpr(addParamMethodExpr); + // No need to add null check if there is no nested fields + if (routingHeaderParam.getDescendantFieldNames().size() == 1) { + bodyStatements.add(addParamStatement); + } else { + IfStatement ifStatement = + IfStatement.builder() + .setConditionExpr( + fieldValuesNotNullConditionExpr( + requestVarExpr, routingHeaderParam.getDescendantFieldNames())) + .setBody(ImmutableList.of(addParamStatement)) + .build(); + bodyStatements.add(ifStatement); + } + } + returnExprBuilder + .setExprReferenceExpr(routingHeadersBuilderVarNonDeclExpr) + .setMethodName("build"); + } + + private Statement createPathTemplateClassStatement( + RoutingHeaderParam routingHeaderParam, TypeNode pathTemplateType, Variable pathTemplateVar) { + VariableExpr pathTemplateVarExpr = + VariableExpr.builder() + .setVariable(pathTemplateVar) + .setIsDecl(true) + .setIsStatic(true) + .setIsFinal(true) + .setScope(ScopeNode.PRIVATE) + .build(); + ValueExpr valueExpr = + ValueExpr.withValue(StringObjectValue.withValue(routingHeaderParam.pattern())); + Expr pathTemplateExpr = + AssignmentExpr.builder() + .setVariableExpr(pathTemplateVarExpr) + .setValueExpr( + MethodInvocationExpr.builder() + .setStaticReferenceType(pathTemplateType) + .setMethodName("create") + .setArguments(valueExpr) + .setReturnType(pathTemplateType) + .build()) + .build(); + return ExprStatement.withExpr(pathTemplateExpr); + } + + private Expr fieldValuesNotNullConditionExpr( + VariableExpr requestVarExpr, List fieldNames) { + MethodInvocationExpr.Builder requestFieldGetterExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); + Expr fieldValuesNotNullExpr = null; + for (int i = 0; i < fieldNames.size() - 1; i++) { + String currFieldName = fieldNames.get(i); + String bindingFieldMethodName = + String.format("get%s", JavaStyle.toUpperCamelCase(currFieldName)); + requestFieldGetterExprBuilder = + requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); + // set return type of each method invocation to String just to pass the validation for + // RelationalOperationExpr that both side of relational operation needs to be a valid equality + // type + MethodInvocationExpr requestGetterExpr = + requestFieldGetterExprBuilder.setReturnType(TypeNode.STRING).build(); + Expr currentValueNotNullExpr = + RelationalOperationExpr.notEqualToWithExprs( + requestGetterExpr, ValueExpr.createNullExpr()); + if (fieldValuesNotNullExpr == null) { + fieldValuesNotNullExpr = currentValueNotNullExpr; + } else { + fieldValuesNotNullExpr = + LogicalOperationExpr.logicalAndWithExprs( + fieldValuesNotNullExpr, currentValueNotNullExpr); + } + requestFieldGetterExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestGetterExpr); + } + return fieldValuesNotNullExpr; + } + + private MethodInvocationExpr createRequestFieldGetterExpr( + VariableExpr requestVarExpr, String fieldName) { + MethodInvocationExpr.Builder requestFieldGetterExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); + List descendantFields = Splitter.on(".").splitToList(fieldName); + // Handle foo.bar cases by descending into the subfields. + // e.g. foo.bar -> request.getFoo().getBar() + for (int i = 0; i < descendantFields.size(); i++) { + String currFieldName = descendantFields.get(i); + String bindingFieldMethodName = + String.format("get%s", JavaStyle.toUpperCamelCase(currFieldName)); + requestFieldGetterExprBuilder = + requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); + if (i < descendantFields.size() - 1) { + requestFieldGetterExprBuilder = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestFieldGetterExprBuilder.build()); + } + } + return requestFieldGetterExprBuilder.build(); } } diff --git a/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java index 19bd60aae7..3fe0af3df1 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java @@ -199,7 +199,10 @@ protected List createOperationsStubGetterMethod( @Override protected Expr createTransportSettingsInitExpr( - Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr) { + Method method, + VariableExpr transportSettingsVarExpr, + VariableExpr methodDescriptorVarExpr, + List classStatements) { MethodInvocationExpr callSettingsBuilderExpr = MethodInvocationExpr.builder() .setStaticReferenceType( diff --git a/src/main/java/com/google/api/generator/gapic/model/Message.java b/src/main/java/com/google/api/generator/gapic/model/Message.java index 91a5388e37..ecfdd484df 100644 --- a/src/main/java/com/google/api/generator/gapic/model/Message.java +++ b/src/main/java/com/google/api/generator/gapic/model/Message.java @@ -18,6 +18,9 @@ import com.google.api.generator.engine.ast.Reference; import com.google.api.generator.engine.ast.TypeNode; import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; @@ -32,6 +35,7 @@ @AutoValue public abstract class Message { + public abstract String name(); // The fully-qualified proto name, which differs from the Java fully-qualified name. @@ -80,6 +84,43 @@ public boolean hasResource() { return resource() != null; } + /** + * Validates if the field or fields exist in the message and the type of the leaf level field. + * + * @param fieldName The field name. For nested field, concatenate each field name with dot. For + * example: abc.def.ghi + * @param messageTypes All messages configured in a rpc service. + * @param type {@link TypeNode} The expected type for the leaf level field + */ + public void validateField(String fieldName, Map messageTypes, TypeNode type) { + List subFields = Splitter.on(".").splitToList(fieldName); + Message nestedMessage = this; + for (int i = 0; i < subFields.size(); i++) { + String subFieldName = subFields.get(i); + Preconditions.checkState( + !Strings.isNullOrEmpty(subFieldName), + String.format("Null or empty field name found for message %s", nestedMessage.name())); + Field field = nestedMessage.fieldMap().get(subFieldName); + Preconditions.checkNotNull( + field, + String.format( + "Expected message %s to contain field %s but none found", + nestedMessage.name(), subFieldName)); + if (i < subFields.size() - 1) { + nestedMessage = messageTypes.get(field.type().reference().fullName()); + Preconditions.checkNotNull( + nestedMessage, + String.format( + "No containing message found for field %s with type %s", + field.name(), field.type().reference().simpleName())); + } else { + Preconditions.checkState( + !field.isRepeated() && field.type().equals(type), + String.format("The type of field %s must be String and not repeated.", field.name())); + } + } + } + /** Returns the first list repeated field in a message, unwrapped from its list type. */ @Nullable public Field findAndUnwrapPaginatedRepeatedField() { diff --git a/src/main/java/com/google/api/generator/gapic/model/Method.java b/src/main/java/com/google/api/generator/gapic/model/Method.java index 286bd8c84e..dc2452f2d5 100644 --- a/src/main/java/com/google/api/generator/gapic/model/Method.java +++ b/src/main/java/com/google/api/generator/gapic/model/Method.java @@ -62,6 +62,9 @@ public boolean isPaged() { @Nullable public abstract HttpBindings httpBindings(); + @Nullable + public abstract RoutingHeaderRule routingHeaderRule(); + // Example from Expand in echo.proto: Thet TypeNodes that map to // [["content", "error"], ["content", "error", "info"]]. public abstract ImmutableList> methodSignatures(); @@ -80,6 +83,14 @@ public boolean hasHttpBindings() { return httpBindings() != null && !httpBindings().pathParameters().isEmpty(); } + public boolean hasRoutingHeaderParams() { + return routingHeaderRule() != null && !routingHeaderRule().routingHeaderParams().isEmpty(); + } + + public boolean shouldSetParamsExtractor() { + return (hasHttpBindings() && routingHeaderRule() == null) || hasRoutingHeaderParams(); + } + public boolean isMixin() { return mixedInApiName() != null; } @@ -140,6 +151,8 @@ public abstract static class Builder { public abstract Builder setOperationPollingMethod(boolean operationPollingMethod); + public abstract Builder setRoutingHeaderRule(RoutingHeaderRule routingHeaderRule); + public abstract Method build(); } } diff --git a/src/main/java/com/google/api/generator/gapic/model/RoutingHeaderRule.java b/src/main/java/com/google/api/generator/gapic/model/RoutingHeaderRule.java new file mode 100644 index 0000000000..ff6cf4de28 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/model/RoutingHeaderRule.java @@ -0,0 +1,68 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.model; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.util.List; + +/** + * This model represents routing rules configured in rpc services. It will be used for generating + * the logic to match-and-extract field values from request, the extracted values will be + * concatenated to a request header that is used for routing purposes. + */ +@AutoValue +public abstract class RoutingHeaderRule { + + public abstract ImmutableList routingHeaderParams(); + + public static Builder builder() { + return new AutoValue_RoutingHeaderRule.Builder().setRoutingHeaderParams(ImmutableList.of()); + } + + @AutoValue + public abstract static class RoutingHeaderParam { + + public abstract String fieldName(); + + public abstract String key(); + + public abstract String pattern(); + + public static RoutingHeaderParam create(String field, String key, String pattern) { + return new AutoValue_RoutingHeaderRule_RoutingHeaderParam(field, key, pattern); + } + + public List getDescendantFieldNames() { + return Splitter.on(".").splitToList(fieldName()); + } + } + + @AutoValue.Builder + public abstract static class Builder { + abstract ImmutableList.Builder routingHeaderParamsBuilder(); + + public final Builder addParam(RoutingHeaderParam routingHeaderParam) { + routingHeaderParamsBuilder().add(routingHeaderParam); + return this; + } + + public abstract Builder setRoutingHeaderParams( + ImmutableList routingHeaderParams); + + public abstract RoutingHeaderRule build(); + } +} diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/BUILD.bazel b/src/main/java/com/google/api/generator/gapic/protoparser/BUILD.bazel index 904b7fbf2b..aad6c7f80c 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/BUILD.bazel +++ b/src/main/java/com/google/api/generator/gapic/protoparser/BUILD.bazel @@ -27,6 +27,7 @@ java_library( "//src/main/java/com/google/api/generator/gapic/model", "//src/main/java/com/google/api/generator/gapic/utils", "@com_google_api_api_common//jar", + "@com_google_api_grpc_proto_google_common_protos", "@com_google_code_findbugs_jsr305//jar", "@com_google_code_gson//jar", "@com_google_googleapis//google/api:api_java_proto", diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java index 5a37a38074..db8f74292a 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java @@ -36,7 +36,6 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class HttpRuleParser { private static final String ASTERISK = "*"; @@ -68,11 +67,12 @@ private static HttpBindings parseHttpRuleHelper( HttpRule httpRule, Optional inputMessageOpt, Map messageTypes) { // Get pattern. String pattern = getHttpVerbPattern(httpRule); - ImmutableSet.Builder bindingsBuilder = getPatternBindings(pattern); + ImmutableSet.Builder bindingsBuilder = ImmutableSet.builder(); + bindingsBuilder.addAll(PatternParser.getPattenBindings(pattern)); if (httpRule.getAdditionalBindingsCount() > 0) { for (HttpRule additionalRule : httpRule.getAdditionalBindingsList()) { // TODO: save additional bindings path in HttpRuleBindings - bindingsBuilder.addAll(getPatternBindings(getHttpVerbPattern(additionalRule)).build()); + bindingsBuilder.addAll(PatternParser.getPattenBindings(getHttpVerbPattern(additionalRule))); } } @@ -177,19 +177,6 @@ private static String getHttpVerbPattern(HttpRule httpRule) { } } - private static ImmutableSortedSet.Builder getPatternBindings(String pattern) { - ImmutableSortedSet.Builder bindings = ImmutableSortedSet.naturalOrder(); - if (pattern.isEmpty()) { - return bindings; - } - - PathTemplate template = PathTemplate.create(pattern); - // Filter out any unbound variable like "$0, $1, etc. - bindings.addAll( - template.vars().stream().filter(s -> !s.contains("$")).collect(Collectors.toSet())); - return bindings; - } - private static void checkHttpFieldIsValid(String binding, Message inputMessage, boolean isBody) { Preconditions.checkState( !Strings.isNullOrEmpty(binding), diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index bbfcbbce05..df45446229 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java @@ -34,6 +34,7 @@ import com.google.api.generator.gapic.model.OperationResponse; import com.google.api.generator.gapic.model.ResourceName; import com.google.api.generator.gapic.model.ResourceReference; +import com.google.api.generator.gapic.model.RoutingHeaderRule; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.gapic.model.SourceCodeInfoLocation; import com.google.api.generator.gapic.model.Transport; @@ -709,6 +710,8 @@ static List parseMethods( .getOptions() .getExtension(ExtendedOperationsProto.operationPollingMethod) : false; + RoutingHeaderRule routingHeaderRule = + RoutingRuleParser.parse(protoMethod, inputMessage, messageTypes); methods.add( methodBuilder .setName(protoMethod.getName()) @@ -726,6 +729,7 @@ static List parseMethods( resourceNames, outputArgResourceNames)) .setHttpBindings(httpBindings) + .setRoutingHeaderRule(routingHeaderRule) .setIsBatching(isBatching) .setPageSizeFieldName(parsePageSizeFieldName(protoMethod, messageTypes, transport)) .setIsDeprecated(isDeprecated) diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/PatternParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/PatternParser.java new file mode 100644 index 0000000000..2a374c47af --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/protoparser/PatternParser.java @@ -0,0 +1,36 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.protoparser; + +import com.google.api.pathtemplate.PathTemplate; +import com.google.common.collect.ImmutableSortedSet; +import java.util.Set; + +public class PatternParser { + + // This method tries to parse all named segments from pattern and sort in natual order + // e.g. /v1beta1/{table_name=tests/*}/{routing_id=instances/*}/** -> (routing_id, table_name) + public static Set getPattenBindings(String pattern) { + ImmutableSortedSet.Builder bindings = ImmutableSortedSet.naturalOrder(); + if (pattern.isEmpty()) { + return bindings.build(); + } + + PathTemplate template = PathTemplate.create(pattern); + // Filter out any unbound variable like "$0, $1, etc. + template.vars().stream().filter(s -> !s.contains("$")).forEach(bindings::add); + return bindings.build(); + } +} diff --git a/src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java b/src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java new file mode 100644 index 0000000000..23f8ed0741 --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java @@ -0,0 +1,66 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.protoparser; + +import com.google.api.RoutingParameter; +import com.google.api.RoutingProto; +import com.google.api.RoutingRule; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.gapic.model.Message; +import com.google.api.generator.gapic.model.RoutingHeaderRule; +import com.google.api.generator.gapic.model.RoutingHeaderRule.RoutingHeaderParam; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.protobuf.DescriptorProtos.MethodOptions; +import com.google.protobuf.Descriptors.MethodDescriptor; +import java.util.Map; +import java.util.Set; + +public class RoutingRuleParser { + + public static RoutingHeaderRule parse( + MethodDescriptor protoMethod, Message inputMessage, Map messageTypes) { + MethodOptions methodOptions = protoMethod.getOptions(); + if (!methodOptions.hasExtension(RoutingProto.routing)) { + return null; + } + + RoutingHeaderRule.Builder routingHeaderRuleBuilder = RoutingHeaderRule.builder(); + RoutingRule routingRule = methodOptions.getExtension(RoutingProto.routing); + for (RoutingParameter routingParameter : routingRule.getRoutingParametersList()) { + String pathTemplate = routingParameter.getPathTemplate(); + String fieldName = routingParameter.getField(); + // validate if field exist in Message or nested Messages and the type of leaf level field + inputMessage.validateField(fieldName, messageTypes, TypeNode.STRING); + String key; + if (Strings.isNullOrEmpty(pathTemplate)) { + key = fieldName; + pathTemplate = String.format("{%s=**}", key); + } else { + Set namedSegments = PatternParser.getPattenBindings(pathTemplate); + Preconditions.checkArgument( + namedSegments.size() == 1, + String.format( + "There needs to be one and only one named segment in path template %s", + pathTemplate)); + key = namedSegments.iterator().next(); + } + RoutingHeaderParam routingHeaderParam = + RoutingHeaderParam.create(fieldName, key, pathTemplate); + routingHeaderRuleBuilder.addParam(routingHeaderParam); + } + return routingHeaderRuleBuilder.build(); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/composer/common/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/composer/common/BUILD.bazel index 6ec76efdc2..f66d2ce260 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/common/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/composer/common/BUILD.bazel @@ -28,6 +28,7 @@ TEST_DEPS = [ "//src/test/java/com/google/api/generator/gapic/testdata:deprecated_service_java_proto", "//src/test/java/com/google/api/generator/gapic/testdata:bookshop_java_proto", "//src/test/java/com/google/api/generator/gapic/testdata:showcase_java_proto", + "//src/test/java/com/google/api/generator/gapic/testdata:explicit_dynamic_routing_headers_testing_java_proto", "//src/test/java/com/google/api/generator/gapic/testdata:testgapic_java_proto", "@com_google_api_api_common//jar", "@com_google_api_gax_java//gax", diff --git a/src/test/java/com/google/api/generator/gapic/composer/common/TestProtoLoader.java b/src/test/java/com/google/api/generator/gapic/composer/common/TestProtoLoader.java index ecc8be4615..b7d32007eb 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/common/TestProtoLoader.java +++ b/src/test/java/com/google/api/generator/gapic/composer/common/TestProtoLoader.java @@ -28,6 +28,7 @@ import com.google.api.generator.gapic.protoparser.Parser; import com.google.api.generator.gapic.protoparser.ServiceConfigParser; import com.google.bookshop.v1beta1.BookshopProto; +import com.google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTestingOuterClass; import com.google.logging.v2.LogEntryProto; import com.google.logging.v2.LoggingConfigProto; import com.google.logging.v2.LoggingMetricsProto; @@ -218,6 +219,31 @@ public GapicContext parseShowcaseTesting() { .build(); } + public GapicContext parseExplicitDynamicRoutingHeaderTesting() { + FileDescriptor testingFileDescriptor = ExplicitDynamicRoutingHeaderTestingOuterClass.getDescriptor(); + ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0); + assertEquals(testingService.getName(), "ExplicitDynamicRoutingHeaderTesting"); + + Map messageTypes = Parser.parseMessages(testingFileDescriptor); + Map resourceNames = Parser.parseResourceNames(testingFileDescriptor); + Set outputResourceNames = new HashSet<>(); + List services = + Parser.parseService( + testingFileDescriptor, + messageTypes, + resourceNames, + Optional.empty(), + outputResourceNames); + + return GapicContext.builder() + .setMessages(messageTypes) + .setResourceNames(resourceNames) + .setServices(services) + .setHelperResourceNames(outputResourceNames) + .setTransport(transport) + .build(); + } + public GapicContext parsePubSubPublisher() { FileDescriptor serviceFileDescriptor = PubsubProto.getDescriptor(); FileDescriptor commonResourcesFileDescriptor = CommonResources.getDescriptor(); diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpc/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/composer/grpc/BUILD.bazel index 3537002a84..d0bdef15c1 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/grpc/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/composer/grpc/BUILD.bazel @@ -35,6 +35,7 @@ TEST_DEPS = [ "//src/main/java/com/google/api/generator/gapic/protoparser", "//src/main/java/com/google/api/generator/gapic/composer/defaultvalue", "//src/test/java/com/google/api/generator/gapic/testdata:showcase_java_proto", + "//src/test/java/com/google/api/generator/gapic/testdata:explicit_dynamic_routing_headers_testing_java_proto", "@com_google_api_api_common//jar", "@com_google_api_gax_java//gax", "@com_google_api_api_common", diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java index cf07432966..e26d8696e8 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposerTest.java @@ -65,6 +65,21 @@ public void generateGrpcServiceStubClass_httpBindings() { Assert.assertCodeEquals(goldenFilePath, visitor.write()); } + @Test + public void generateGrpcServiceStubClass_routingHeaders() { + GapicContext context = + GrpcTestProtoLoader.instance().parseExplicitDynamicRoutingHeaderTesting(); + Service service = context.services().get(0); + GapicClass clazz = GrpcServiceStubClassComposer.instance().generate(context, service); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + Utils.saveCodegenToFile(this.getClass(), "GrpcRoutingHeadersStub.golden", visitor.write()); + Path goldenFilePath = + Paths.get(Utils.getGoldenDir(this.getClass()), "GrpcRoutingHeadersStub.golden"); + Assert.assertCodeEquals(goldenFilePath, visitor.write()); + } + @Test public void generateGrpcServiceStubClass_httpBindingsWithSubMessageFields() { GapicContext context = GrpcTestProtoLoader.instance().parsePubSubPublisher(); diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcRoutingHeadersStub.golden b/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcRoutingHeadersStub.golden new file mode 100644 index 0000000000..84489dd164 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcRoutingHeadersStub.golden @@ -0,0 +1,594 @@ +package com.google.explicit.dynamic.routing.header.stub; + +import com.google.api.gax.core.BackgroundResource; +import com.google.api.gax.core.BackgroundResourceAggregation; +import com.google.api.gax.grpc.GrpcCallSettings; +import com.google.api.gax.grpc.GrpcStubCallableFactory; +import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.RequestParamsBuilder; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.pathtemplate.PathTemplate; +import com.google.common.collect.ImmutableMap; +import com.google.explicit.dynamic.routing.header.Request; +import com.google.explicit.dynamic.routing.header.RequestWithNestedField; +import com.google.longrunning.stub.GrpcOperationsStub; +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import io.grpc.protobuf.ProtoUtils; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.annotation.Generated; + +// AUTO-GENERATED DOCUMENTATION AND CLASS. +/** + * gRPC stub implementation for the ExplicitDynamicRoutingHeaderTesting service API. + * + *

This class is for advanced usage and reflects the underlying API directly. + */ +@Generated("by gapic-generator-java") +public class GrpcExplicitDynamicRoutingHeaderTestingStub + extends ExplicitDynamicRoutingHeaderTestingStub { + private static final MethodDescriptor example1TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example1Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example2TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example2Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example3TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example3Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example3CTestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example3CTest") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example4TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example4Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example5TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example5Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example6TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example6Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example7TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example7Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example8TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example8Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor example9TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/Example9Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor backwardsCompatible1TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/BackwardsCompatible1Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor backwardsCompatible2TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/BackwardsCompatible2Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor backwardsCompatible3TestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/BackwardsCompatible3Test") + .setRequestMarshaller(ProtoUtils.marshaller(Request.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private static final MethodDescriptor + nestedFieldTestMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTesting/NestedFieldTest") + .setRequestMarshaller( + ProtoUtils.marshaller(RequestWithNestedField.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Empty.getDefaultInstance())) + .build(); + + private final UnaryCallable example1TestCallable; + private final UnaryCallable example2TestCallable; + private final UnaryCallable example3TestCallable; + private final UnaryCallable example3CTestCallable; + private final UnaryCallable example4TestCallable; + private final UnaryCallable example5TestCallable; + private final UnaryCallable example6TestCallable; + private final UnaryCallable example7TestCallable; + private final UnaryCallable example8TestCallable; + private final UnaryCallable example9TestCallable; + private final UnaryCallable backwardsCompatible1TestCallable; + private final UnaryCallable backwardsCompatible2TestCallable; + private final UnaryCallable backwardsCompatible3TestCallable; + private final UnaryCallable nestedFieldTestCallable; + + private final BackgroundResource backgroundResources; + private final GrpcOperationsStub operationsStub; + private final GrpcStubCallableFactory callableFactory; + + private static final PathTemplate EXAMPLE1_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{app_profile_id=**}"); + private static final PathTemplate EXAMPLE2_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=**}"); + private static final PathTemplate EXAMPLE3_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{table_name=projects/*/instances/*/**}"); + private static final PathTemplate EXAMPLE3_C_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{table_name=regions/*/zones/*/**}"); + private static final PathTemplate EXAMPLE3_C_TEST_1_PATH_TEMPLATE = + PathTemplate.create("{table_name=projects/*/instances/*/**}"); + private static final PathTemplate EXAMPLE4_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + private static final PathTemplate EXAMPLE5_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + private static final PathTemplate EXAMPLE5_TEST_1_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*/instances/*}/**"); + private static final PathTemplate EXAMPLE6_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{project_id=projects/*}/instances/*/**"); + private static final PathTemplate EXAMPLE6_TEST_1_PATH_TEMPLATE = + PathTemplate.create("projects/*/{instance_id=instances/*}/**"); + private static final PathTemplate EXAMPLE7_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{project_id=projects/*}/**"); + private static final PathTemplate EXAMPLE7_TEST_1_PATH_TEMPLATE = + PathTemplate.create("{routing_id=**}"); + private static final PathTemplate EXAMPLE8_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + private static final PathTemplate EXAMPLE8_TEST_1_PATH_TEMPLATE = + PathTemplate.create("{routing_id=regions/*}/**"); + private static final PathTemplate EXAMPLE8_TEST_2_PATH_TEMPLATE = + PathTemplate.create("{routing_id=**}"); + private static final PathTemplate EXAMPLE9_TEST_0_PATH_TEMPLATE = + PathTemplate.create("projects/*/{table_location=instances/*}/tables/*"); + private static final PathTemplate EXAMPLE9_TEST_1_PATH_TEMPLATE = + PathTemplate.create("{table_location=regions/*/zones/*}/tables/*"); + private static final PathTemplate EXAMPLE9_TEST_2_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + private static final PathTemplate EXAMPLE9_TEST_3_PATH_TEMPLATE = + PathTemplate.create("{routing_id=**}"); + private static final PathTemplate EXAMPLE9_TEST_4_PATH_TEMPLATE = + PathTemplate.create("profiles/{routing_id=*}"); + private static final PathTemplate BACKWARDS_COMPATIBLE1_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + private static final PathTemplate NESTED_FIELD_TEST_0_PATH_TEMPLATE = + PathTemplate.create("{routing_id=projects/*}/**"); + + public static final GrpcExplicitDynamicRoutingHeaderTestingStub create( + ExplicitDynamicRoutingHeaderTestingStubSettings settings) throws IOException { + return new GrpcExplicitDynamicRoutingHeaderTestingStub( + settings, ClientContext.create(settings)); + } + + public static final GrpcExplicitDynamicRoutingHeaderTestingStub create( + ClientContext clientContext) throws IOException { + return new GrpcExplicitDynamicRoutingHeaderTestingStub( + ExplicitDynamicRoutingHeaderTestingStubSettings.newBuilder().build(), clientContext); + } + + public static final GrpcExplicitDynamicRoutingHeaderTestingStub create( + ClientContext clientContext, GrpcStubCallableFactory callableFactory) throws IOException { + return new GrpcExplicitDynamicRoutingHeaderTestingStub( + ExplicitDynamicRoutingHeaderTestingStubSettings.newBuilder().build(), + clientContext, + callableFactory); + } + + /** + * Constructs an instance of GrpcExplicitDynamicRoutingHeaderTestingStub, using the given + * settings. This is protected so that it is easy to make a subclass, but otherwise, the static + * factory methods should be preferred. + */ + protected GrpcExplicitDynamicRoutingHeaderTestingStub( + ExplicitDynamicRoutingHeaderTestingStubSettings settings, ClientContext clientContext) + throws IOException { + this(settings, clientContext, new GrpcExplicitDynamicRoutingHeaderTestingCallableFactory()); + } + + /** + * Constructs an instance of GrpcExplicitDynamicRoutingHeaderTestingStub, using the given + * settings. This is protected so that it is easy to make a subclass, but otherwise, the static + * factory methods should be preferred. + */ + protected GrpcExplicitDynamicRoutingHeaderTestingStub( + ExplicitDynamicRoutingHeaderTestingStubSettings settings, + ClientContext clientContext, + GrpcStubCallableFactory callableFactory) + throws IOException { + this.callableFactory = callableFactory; + this.operationsStub = GrpcOperationsStub.create(clientContext, callableFactory); + + GrpcCallSettings example1TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example1TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + request.getAppProfileId(), "app_profile_id", EXAMPLE1_TEST_0_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example2TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example2TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + request.getAppProfileId(), "routing_id", EXAMPLE2_TEST_0_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example3TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example3TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "table_name", EXAMPLE3_TEST_0_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example3CTestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example3CTestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + request.getTableName(), "table_name", EXAMPLE3_C_TEST_0_PATH_TEMPLATE); + builder.add( + request.getTableName(), "table_name", EXAMPLE3_C_TEST_1_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example4TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example4TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "routing_id", EXAMPLE4_TEST_0_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example5TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example5TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "routing_id", EXAMPLE5_TEST_0_PATH_TEMPLATE); + builder.add(request.getTableName(), "routing_id", EXAMPLE5_TEST_1_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example6TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example6TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "project_id", EXAMPLE6_TEST_0_PATH_TEMPLATE); + builder.add(request.getTableName(), "instance_id", EXAMPLE6_TEST_1_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example7TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example7TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "project_id", EXAMPLE7_TEST_0_PATH_TEMPLATE); + builder.add( + request.getAppProfileId(), "routing_id", EXAMPLE7_TEST_1_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example8TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example8TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getTableName(), "routing_id", EXAMPLE8_TEST_0_PATH_TEMPLATE); + builder.add(request.getTableName(), "routing_id", EXAMPLE8_TEST_1_PATH_TEMPLATE); + builder.add( + request.getAppProfileId(), "routing_id", EXAMPLE8_TEST_2_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings example9TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(example9TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + request.getTableName(), "table_location", EXAMPLE9_TEST_0_PATH_TEMPLATE); + builder.add( + request.getTableName(), "table_location", EXAMPLE9_TEST_1_PATH_TEMPLATE); + builder.add(request.getTableName(), "routing_id", EXAMPLE9_TEST_2_PATH_TEMPLATE); + builder.add( + request.getAppProfileId(), "routing_id", EXAMPLE9_TEST_3_PATH_TEMPLATE); + builder.add( + request.getAppProfileId(), "routing_id", EXAMPLE9_TEST_4_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings backwardsCompatible1TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(backwardsCompatible1TestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + request.getTableName(), + "routing_id", + BACKWARDS_COMPATIBLE1_TEST_0_PATH_TEMPLATE); + return builder.build(); + }) + .build(); + GrpcCallSettings backwardsCompatible2TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(backwardsCompatible2TestMethodDescriptor) + .build(); + GrpcCallSettings backwardsCompatible3TestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(backwardsCompatible3TestMethodDescriptor) + .setParamsExtractor( + request -> { + ImmutableMap.Builder params = ImmutableMap.builder(); + params.put("table_name", String.valueOf(request.getTableName())); + return params.build(); + }) + .build(); + GrpcCallSettings nestedFieldTestTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(nestedFieldTestMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + if (request.getNestedField() != null + && request.getNestedField().getAnotherNestedField() != null) { + builder.add( + request.getNestedField().getAnotherNestedField().getName(), + "routing_id", + NESTED_FIELD_TEST_0_PATH_TEMPLATE); + } + return builder.build(); + }) + .build(); + + this.example1TestCallable = + callableFactory.createUnaryCallable( + example1TestTransportSettings, settings.example1TestSettings(), clientContext); + this.example2TestCallable = + callableFactory.createUnaryCallable( + example2TestTransportSettings, settings.example2TestSettings(), clientContext); + this.example3TestCallable = + callableFactory.createUnaryCallable( + example3TestTransportSettings, settings.example3TestSettings(), clientContext); + this.example3CTestCallable = + callableFactory.createUnaryCallable( + example3CTestTransportSettings, settings.example3CTestSettings(), clientContext); + this.example4TestCallable = + callableFactory.createUnaryCallable( + example4TestTransportSettings, settings.example4TestSettings(), clientContext); + this.example5TestCallable = + callableFactory.createUnaryCallable( + example5TestTransportSettings, settings.example5TestSettings(), clientContext); + this.example6TestCallable = + callableFactory.createUnaryCallable( + example6TestTransportSettings, settings.example6TestSettings(), clientContext); + this.example7TestCallable = + callableFactory.createUnaryCallable( + example7TestTransportSettings, settings.example7TestSettings(), clientContext); + this.example8TestCallable = + callableFactory.createUnaryCallable( + example8TestTransportSettings, settings.example8TestSettings(), clientContext); + this.example9TestCallable = + callableFactory.createUnaryCallable( + example9TestTransportSettings, settings.example9TestSettings(), clientContext); + this.backwardsCompatible1TestCallable = + callableFactory.createUnaryCallable( + backwardsCompatible1TestTransportSettings, + settings.backwardsCompatible1TestSettings(), + clientContext); + this.backwardsCompatible2TestCallable = + callableFactory.createUnaryCallable( + backwardsCompatible2TestTransportSettings, + settings.backwardsCompatible2TestSettings(), + clientContext); + this.backwardsCompatible3TestCallable = + callableFactory.createUnaryCallable( + backwardsCompatible3TestTransportSettings, + settings.backwardsCompatible3TestSettings(), + clientContext); + this.nestedFieldTestCallable = + callableFactory.createUnaryCallable( + nestedFieldTestTransportSettings, settings.nestedFieldTestSettings(), clientContext); + + this.backgroundResources = + new BackgroundResourceAggregation(clientContext.getBackgroundResources()); + } + + public GrpcOperationsStub getOperationsStub() { + return operationsStub; + } + + @Override + public UnaryCallable example1TestCallable() { + return example1TestCallable; + } + + @Override + public UnaryCallable example2TestCallable() { + return example2TestCallable; + } + + @Override + public UnaryCallable example3TestCallable() { + return example3TestCallable; + } + + @Override + public UnaryCallable example3CTestCallable() { + return example3CTestCallable; + } + + @Override + public UnaryCallable example4TestCallable() { + return example4TestCallable; + } + + @Override + public UnaryCallable example5TestCallable() { + return example5TestCallable; + } + + @Override + public UnaryCallable example6TestCallable() { + return example6TestCallable; + } + + @Override + public UnaryCallable example7TestCallable() { + return example7TestCallable; + } + + @Override + public UnaryCallable example8TestCallable() { + return example8TestCallable; + } + + @Override + public UnaryCallable example9TestCallable() { + return example9TestCallable; + } + + @Override + public UnaryCallable backwardsCompatible1TestCallable() { + return backwardsCompatible1TestCallable; + } + + @Override + public UnaryCallable backwardsCompatible2TestCallable() { + return backwardsCompatible2TestCallable; + } + + @Override + public UnaryCallable backwardsCompatible3TestCallable() { + return backwardsCompatible3TestCallable; + } + + @Override + public UnaryCallable nestedFieldTestCallable() { + return nestedFieldTestCallable; + } + + @Override + public final void close() { + try { + backgroundResources.close(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to close resource", e); + } + } + + @Override + public void shutdown() { + backgroundResources.shutdown(); + } + + @Override + public boolean isShutdown() { + return backgroundResources.isShutdown(); + } + + @Override + public boolean isTerminated() { + return backgroundResources.isTerminated(); + } + + @Override + public void shutdownNow() { + backgroundResources.shutdownNow(); + } + + @Override + public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { + return backgroundResources.awaitTermination(duration, unit); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcTestingStub.golden b/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcTestingStub.golden index 7312b79d37..6c0a667291 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcTestingStub.golden +++ b/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcTestingStub.golden @@ -9,7 +9,9 @@ import com.google.api.gax.core.BackgroundResourceAggregation; import com.google.api.gax.grpc.GrpcCallSettings; import com.google.api.gax.grpc.GrpcStubCallableFactory; import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.pathtemplate.PathTemplate; import com.google.common.collect.ImmutableMap; import com.google.longrunning.stub.GrpcOperationsStub; import com.google.protobuf.Empty; @@ -142,6 +144,11 @@ public class GrpcTestingStub extends TestingStub { private final GrpcOperationsStub operationsStub; private final GrpcStubCallableFactory callableFactory; + private static final PathTemplate GET_TEST_0_PATH_TEMPLATE = + PathTemplate.create("/v1beta1/{rename=tests/*}"); + private static final PathTemplate GET_TEST_1_PATH_TEMPLATE = + PathTemplate.create("/v1beta1/{routing_id=tests/*}"); + public static final GrpcTestingStub create(TestingStubSettings settings) throws IOException { return new GrpcTestingStub(settings, ClientContext.create(settings)); } @@ -220,9 +227,13 @@ public class GrpcTestingStub extends TestingStub { .setMethodDescriptor(getTestMethodDescriptor) .setParamsExtractor( request -> { - ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); - return params.build(); + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add(request.getName(), "rename", GET_TEST_0_PATH_TEMPLATE); + if (request.getRouting() != null) { + builder.add( + request.getRouting().getName(), "routing_id", GET_TEST_1_PATH_TEMPLATE); + } + return builder.build(); }) .build(); GrpcCallSettings listTestsTransportSettings = diff --git a/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel index f2697b2efa..bc62109ad9 100644 --- a/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/model/BUILD.bazel @@ -6,6 +6,8 @@ TESTS = [ "GapicServiceConfigTest", "MethodArgumentTest", "MethodTest", + "MessageTest", + "RoutingHeaderParamTest", ] filegroup( diff --git a/src/test/java/com/google/api/generator/gapic/model/MessageTest.java b/src/test/java/com/google/api/generator/gapic/model/MessageTest.java new file mode 100644 index 0000000000..469c86c212 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/model/MessageTest.java @@ -0,0 +1,151 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.model; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.VaporReference; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Test; + +public class MessageTest { + + private static final String SUB_FIELD_NAME = "table"; + private static final String LEAF_FIELD_NAME = "size"; + private static final String SUB_FIELD_TYPE = "TableFieldType"; + public static final VaporReference FIELD_TYPE = + VaporReference.builder().setPakkage("com.google").setName(SUB_FIELD_TYPE).build(); + private static final String MESSAGE_NAME = "TestMessage"; + private static final Message.Builder testMessageBuilder = + Message.builder() + .setName(MESSAGE_NAME) + .setFullProtoName("com.google.test.TestMessage") + .setType(TypeNode.OBJECT); + + @Test + public void validateField_shouldThrowExceptionIfFieldNameIsEmpty() { + Message message = testMessageBuilder.build(); + IllegalStateException illegalStateException = + assertThrows( + IllegalStateException.class, + () -> message.validateField("", ImmutableMap.of(), TypeNode.STRING)); + assertThat(illegalStateException) + .hasMessageThat() + .isEqualTo(String.format("Null or empty field name found for message %s", MESSAGE_NAME)); + } + + @Test + public void validateField_shouldThrowExceptionIfFieldDoesNotExist() { + Message message = testMessageBuilder.build(); + String fieldName = "doesNotExist"; + NullPointerException nullPointerException = + assertThrows( + NullPointerException.class, + () -> message.validateField(fieldName, ImmutableMap.of(), TypeNode.STRING)); + assertThat(nullPointerException) + .hasMessageThat() + .isEqualTo( + String.format( + "Expected message %s to contain field %s but none found", MESSAGE_NAME, fieldName)); + } + + @Test + public void validateField_shouldThrowExceptionIfMessageDoesNotExist() { + Field subField = + Field.builder() + .setName(SUB_FIELD_NAME) + .setType( + TypeNode.withReference( + VaporReference.builder() + .setPakkage("com.google") + .setName(SUB_FIELD_TYPE) + .build())) + .build(); + Message message = + testMessageBuilder.setFieldMap(ImmutableMap.of(SUB_FIELD_NAME, subField)).build(); + String fieldName = SUB_FIELD_NAME + "." + LEAF_FIELD_NAME; + NullPointerException nullPointerException = + assertThrows( + NullPointerException.class, + () -> message.validateField(fieldName, ImmutableMap.of(), TypeNode.STRING)); + assertThat(nullPointerException) + .hasMessageThat() + .isEqualTo( + String.format( + "No containing message found for field %s with type %s", + SUB_FIELD_NAME, SUB_FIELD_TYPE)); + } + + @Test + public void validateField_shouldThrowExceptionIfFieldIsRepeated() { + Field leafField = + Field.builder() + .setType(TypeNode.STRING) + .setIsRepeated(true) + .setName(LEAF_FIELD_NAME) + .build(); + testLeafField(leafField); + } + + @Test + public void validateField_shouldThrowExceptionIfFieldIsOfWrongType() { + Field leafField = Field.builder().setType(TypeNode.BOOLEAN).setName(LEAF_FIELD_NAME).build(); + testLeafField(leafField); + } + + private void testLeafField(Field leafField) { + Message subMessage = createSubMessage(leafField); + Map messageTypes = ImmutableMap.of(FIELD_TYPE.fullName(), subMessage); + IllegalStateException illegalStateException = + assertThrows( + IllegalStateException.class, + () -> + createdMessage() + .validateField( + SUB_FIELD_NAME + "." + LEAF_FIELD_NAME, messageTypes, TypeNode.STRING)); + assertThat(illegalStateException) + .hasMessageThat() + .isEqualTo( + String.format( + "The type of field %s must be String and not repeated.", LEAF_FIELD_NAME)); + } + + @Test + public void validateField_shouldNotThrowExceptionIfFieldExist() { + Field leafField = Field.builder().setType(TypeNode.STRING).setName(LEAF_FIELD_NAME).build(); + Message subMessage = createSubMessage(leafField); + Map messageTypes = ImmutableMap.of(FIELD_TYPE.fullName(), subMessage); + createdMessage() + .validateField(SUB_FIELD_NAME + "." + LEAF_FIELD_NAME, messageTypes, TypeNode.STRING); + } + + private Message createdMessage() { + Field subField = + Field.builder().setName(SUB_FIELD_NAME).setType(TypeNode.withReference(FIELD_TYPE)).build(); + return testMessageBuilder.setFieldMap(ImmutableMap.of(SUB_FIELD_NAME, subField)).build(); + } + + private Message createSubMessage(Field leafField) { + return Message.builder() + .setName(SUB_FIELD_TYPE) + .setFullProtoName("com.google." + SUB_FIELD_TYPE) + .setType(TypeNode.OBJECT) + .setFieldMap(ImmutableMap.of(LEAF_FIELD_NAME, leafField)) + .build(); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/model/MethodTest.java b/src/test/java/com/google/api/generator/gapic/model/MethodTest.java index 2c245151f4..fd5bba4b49 100644 --- a/src/test/java/com/google/api/generator/gapic/model/MethodTest.java +++ b/src/test/java/com/google/api/generator/gapic/model/MethodTest.java @@ -16,9 +16,29 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; +import com.google.api.generator.gapic.model.HttpBindings.HttpVerb; +import com.google.api.generator.gapic.model.RoutingHeaderRule.RoutingHeaderParam; +import com.google.common.collect.ImmutableSet; import org.junit.Test; public class MethodTest { + + private static final Method METHOD = + Method.builder() + .setName("My method") + .setInputType(TypeNode.STRING) + .setOutputType(TypeNode.STRING) + .build(); + private static final HttpBindings HTTP_BINDINGS = + HttpBindings.builder() + .setPathParameters(ImmutableSet.of(HttpBinding.create("table", true, ""))) + .setPattern("/pattern/test") + .setIsAsteriskBody(false) + .setHttpVerb(HttpVerb.GET) + .build(); + @Test public void toStream() { // Argument order: isClientStreaming, isServerStreaming. @@ -27,4 +47,67 @@ public void toStream() { assertThat(Method.toStream(false, true)).isEqualTo(Method.Stream.SERVER); assertThat(Method.toStream(true, true)).isEqualTo(Method.Stream.BIDI); } + + @Test + public void hasRoutingHeaders_shouldReturnFalseIfRoutingHeadersIsNull() { + assertThat(METHOD.hasRoutingHeaderParams()).isFalse(); + } + + @Test + public void hasRoutingHeaders_shouldReturnFalseIfRoutingHeadersIsEmpty() { + Method method = + METHOD.toBuilder().setRoutingHeaderRule(RoutingHeaderRule.builder().build()).build(); + assertThat(method.hasRoutingHeaderParams()).isFalse(); + } + + @Test + public void hasRoutingHeaders_shouldReturnTrueIfRoutingHeadersIsNotEmpty() { + Method method = + METHOD + .toBuilder() + .setRoutingHeaderRule( + RoutingHeaderRule.builder() + .addParam(RoutingHeaderParam.create("table", "routing_id", "")) + .build()) + .build(); + assertThat(method.hasRoutingHeaderParams()).isTrue(); + } + + @Test + public void shouldSetParamsExtractor_shouldReturnTrueIfHasRoutingHeaders() { + Method method = + METHOD + .toBuilder() + .setRoutingHeaderRule( + RoutingHeaderRule.builder() + .addParam(RoutingHeaderParam.create("table", "routing_id", "")) + .build()) + .build(); + assertThat(method.shouldSetParamsExtractor()).isTrue(); + } + + @Test + public void shouldSetParamsExtractor_shouldReturnTrueIfHasHttpBindingsAndRoutingHeadersIsNull() { + Method method = + METHOD.toBuilder().setHttpBindings(HTTP_BINDINGS).setRoutingHeaderRule(null).build(); + assertThat(method.shouldSetParamsExtractor()).isTrue(); + } + + @Test + public void + shouldSetParamsExtractor_shouldReturnFalseIfHasHttpBindingsAndRoutingHeadersIsEmpty() { + Method method = + METHOD + .toBuilder() + .setHttpBindings(HTTP_BINDINGS) + .setRoutingHeaderRule(RoutingHeaderRule.builder().build()) + .build(); + assertThat(method.shouldSetParamsExtractor()).isFalse(); + } + + @Test + public void shouldSetParamsExtractor_shouldReturnFalseIfHasNoHttpBindingsAndNoRoutingHeaders() { + Method method = METHOD.toBuilder().setHttpBindings(null).setRoutingHeaderRule(null).build(); + assertThat(method.shouldSetParamsExtractor()).isFalse(); + } } diff --git a/src/test/java/com/google/api/generator/gapic/model/RoutingHeaderParamTest.java b/src/test/java/com/google/api/generator/gapic/model/RoutingHeaderParamTest.java new file mode 100644 index 0000000000..8c2d1d361e --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/model/RoutingHeaderParamTest.java @@ -0,0 +1,32 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.model; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.generator.gapic.model.RoutingHeaderRule.RoutingHeaderParam; +import java.util.List; +import org.junit.Test; + +public class RoutingHeaderParamTest { + + @Test + public void getDescendantFieldNames_shouldSplitFieldNameByDot() { + RoutingHeaderParam routingHeaderParam = + RoutingHeaderParam.create("table.name", "name", "/abc/dec"); + List descendantFieldNames = routingHeaderParam.getDescendantFieldNames(); + assertThat(descendantFieldNames).containsExactly("table", "name"); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel index f15a3d1a6e..ead36d5fb0 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/protoparser/BUILD.bazel @@ -16,6 +16,8 @@ TESTS = [ "ServiceYamlParserTest", "SourceCodeInfoParserTest", "TypeParserTest", + "RoutingRuleParserTest", + "PatternParserTest", ] filegroup( @@ -43,6 +45,7 @@ filegroup( "//src/main/java/com/google/api/generator/gapic/utils", "//src/test/java/com/google/api/generator/gapic/testdata:bookshop_java_proto", "//src/test/java/com/google/api/generator/gapic/testdata:showcase_java_proto", + "//src/test/java/com/google/api/generator/gapic/testdata:explicit_dynamic_routing_headers_testing_java_proto", "//src/test/java/com/google/api/generator/gapic/testdata:testgapic_java_proto", "@com_google_api_api_common//jar", "@com_google_googleapis//google/api:api_java_proto", diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/PatternParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/PatternParserTest.java new file mode 100644 index 0000000000..8415ca2b21 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/PatternParserTest.java @@ -0,0 +1,40 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.protoparser; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Set; +import org.junit.Test; + +public class PatternParserTest { + @Test + public void getPattenBindings_shouldReturnEmptySetIfPatternIsEmpty() { + assertThat(PatternParser.getPattenBindings("")).isEmpty(); + } + + @Test + public void getPattenBindings_shouldFilterOutUnboundVariables() { + Set actual = PatternParser.getPattenBindings("{routing_id=projects/*}/**"); + assertThat(actual).hasSize(1); + } + + @Test + public void getPattenBindings_shouldReturnBindingsInNatualOrder() { + Set actual = + PatternParser.getPattenBindings("{routing_id=projects/*}/{name=instance/*}"); + assertThat(actual).containsExactly("name", "routing_id").inOrder(); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java new file mode 100644 index 0000000000..0908d79080 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java @@ -0,0 +1,112 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.protoparser; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.api.generator.gapic.model.Message; +import com.google.api.generator.gapic.model.RoutingHeaderRule; +import com.google.api.generator.gapic.model.RoutingHeaderRule.RoutingHeaderParam; +import com.google.explicit.dynamic.routing.header.RoutingRuleParserTestingOuterClass; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import java.util.Map; +import org.junit.Test; + +public class RoutingRuleParserTest { + + private static final FileDescriptor TESTING_FILE_DESCRIPTOR = + RoutingRuleParserTestingOuterClass.getDescriptor(); + private static final ServiceDescriptor TESTING_SERVICE = + TESTING_FILE_DESCRIPTOR.getServices().get(0); + + @Test + public void parse_shouldReturnNullRoutingHeadersIfMethodHasNoRoutingRules() { + RoutingHeaderRule actual = getRoutingHeaders(0); + assertThat(actual).isNull(); + } + + @Test + public void parse_shouldSetPathTemplateToWildcardIfNotDefined() { + RoutingHeaderRule actual = getRoutingHeaders(1); + RoutingHeaderParam expected = + RoutingHeaderParam.create("name", "name", String.format("{%s=**}", "name")); + assertThat(actual.routingHeaderParams()).containsExactly(expected); + } + + @Test + public void parse_shouldThrowExceptionIfPathTemplateHasZeroNamedSegment() { + IllegalArgumentException illegalArgumentException = + assertThrows(IllegalArgumentException.class, () -> getRoutingHeaders(2)); + assertThat(illegalArgumentException.getMessage()) + .isEqualTo( + String.format( + "There needs to be one and only one named segment in path template %s", + "/v1beta1/tests/*")); + } + + @Test + public void parse_shouldThrowExceptionIfPathTemplateHasMoreThanOneNamedSegment() { + IllegalArgumentException illegalArgumentException = + assertThrows(IllegalArgumentException.class, () -> getRoutingHeaders(3)); + assertThat(illegalArgumentException.getMessage()) + .isEqualTo( + String.format( + "There needs to be one and only one named segment in path template %s", + "/v1beta1/{name=tests/*}/{second_name=*}")); + } + + @Test + public void parse_shouldParseRoutingRulesWithOneParameter() { + RoutingHeaderRule actual = getRoutingHeaders(4); + RoutingHeaderParam expected = + RoutingHeaderParam.create("name", "rename", "/v1beta1/{rename=tests/*}"); + assertThat(actual.routingHeaderParams()).containsExactly(expected); + } + + @Test + public void parse_shouldParseRoutingRulesWithMultipleParameter() { + RoutingHeaderRule actual = getRoutingHeaders(5); + RoutingHeaderParam expectedHeader1 = + RoutingHeaderParam.create("name", "rename", "/v1beta1/{rename=tests/*}"); + RoutingHeaderParam expectedHeader2 = + RoutingHeaderParam.create("routing_id", "id", "/v1beta1/{id=projects/*}/tables/*"); + assertThat(actual.routingHeaderParams()) + .containsExactly(expectedHeader1, expectedHeader2) + .inOrder(); + } + + @Test + public void parse_shouldParseRoutingRulesWithNestedFields() { + RoutingHeaderRule actual = getRoutingHeaders(6); + RoutingHeaderParam expectedHeader1 = + RoutingHeaderParam.create("account.name", "rename", "/v1beta1/{rename=tests/*}"); + assertThat(actual.routingHeaderParams()).containsExactly(expectedHeader1); + } + + @Test + public void parse_shouldThrowExceptionIfFieldValidationFailed() { + assertThrows(Exception.class, () -> getRoutingHeaders(7)); + } + + private RoutingHeaderRule getRoutingHeaders(int testingIndex) { + MethodDescriptor rpcMethod = TESTING_SERVICE.getMethods().get(testingIndex); + Map messages = Parser.parseMessages(TESTING_FILE_DESCRIPTOR); + Message inputMessage = messages.get("com." + rpcMethod.getInputType().getFullName()); + return RoutingRuleParser.parse(rpcMethod, inputMessage, messages); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel index 3991672985..25516cdb84 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel +++ b/src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel @@ -54,6 +54,7 @@ proto_library( ], deps = [ "@com_google_googleapis//google/api:annotations_proto", + "@com_google_googleapis//google/api:routing_proto", "@com_google_googleapis//google/api:client_proto", "@com_google_googleapis//google/api:field_behavior_proto", "@com_google_googleapis//google/api:resource_proto", @@ -68,6 +69,20 @@ proto_library( ], ) +proto_library( + name = "explicit_dynamic_routing_headers_testing_proto", + srcs = [ + "routing_rule_parser_testing.proto", + "explicit_dynamic_routing_header_testing.proto", + ], + deps = [ + "@com_google_googleapis//google/api:annotations_proto", + "@com_google_googleapis//google/api:routing_proto", + "@com_google_googleapis//google/api:client_proto", + "@com_google_protobuf//:empty_proto", + ], +) + proto_library( name = "deprecated_service_proto", srcs = [ @@ -127,3 +142,8 @@ java_proto_library( name = "testgapic_java_proto", deps = [":testgapic_proto"], ) + +java_proto_library( + name = "explicit_dynamic_routing_headers_testing_java_proto", + deps = [":explicit_dynamic_routing_headers_testing_proto"], +) diff --git a/src/test/java/com/google/api/generator/gapic/testdata/explicit_dynamic_routing_header_testing.proto b/src/test/java/com/google/api/generator/gapic/testdata/explicit_dynamic_routing_header_testing.proto new file mode 100644 index 0000000000..cbe9526a7b --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/testdata/explicit_dynamic_routing_header_testing.proto @@ -0,0 +1,304 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/routing.proto"; +import "google/api/client.proto"; +import "google/protobuf/empty.proto"; + +package google.explicit.dynamic.routing.header; + +option java_package = "com.google.explicit.dynamic.routing.header"; +option java_multiple_files = true; + +// This service is meant for testing all scenarios related to +// explicit dynamic routing headers feature, including but not limited to all examples in routing.proto +// All test cases in this proto assumes that the configured routing annotation is well formatted and passes all validation +service ExplicitDynamicRoutingHeaderTesting { + + option (google.api.default_host) = "localhost:7469"; + + //Example 1 + // Extracting a field from the request to put into the routing header + // unchanged, with the key equal to the field name. + rpc Example1Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + //take the `app_profile_id` + routing_parameters { + field: "app_profile_id" + } + }; + } + // Example 2 + // Extracting a field from the request to put into the routing header + // unchanged, with the key different from the field name. + rpc Example2Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // Take the `app_profile_id`, but name it `routing_id` in the header. + routing_parameters { + field: "app_profile_id" + path_template: "{routing_id=**}" + } + }; + } + + // Example 3 + // + // Extracting a field from the request to put into the routing + // header, while matching a path template syntax on the field's value. + rpc Example3Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "table_name" + path_template: "{table_name=projects/*/instances/*/**}" + } + }; + } + + // Example 3c + // + //Multiple alternative conflictingly named path templates are + // specified. The one that matches is used to construct the header. + rpc Example3CTest(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "table_name" + path_template: "{table_name=regions/*/zones/*/**}" + } + routing_parameters { + field: "table_name" + path_template: "{table_name=projects/*/instances/*/**}" + } + }; + } + + // Example 4 + // + // Extracting a single routing header key-value pair by matching a + // template syntax on (a part of) a single request field. + rpc Example4Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*}/**" + } + }; + } + + // Example 5 + // + // Extracting a single routing header key-value pair by matching + // several conflictingly named path templates on (parts of) a single request + // field. The last template to match "wins" the conflict. + rpc Example5Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // If the `table_name` does not have instances information, + // take just the project id for routing. + // Otherwise take project + instance. + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*}/**" + } + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*/instances/*}/**" + } + }; + } + + // Example 6 + // + // Extracting multiple routing header key-value pairs by matching + // several non-conflicting path templates on (parts of) a single request field. + rpc Example6Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // The routing code needs two keys instead of one composite + // but works only for the tables with the "project-instance" name + // syntax. + + routing_parameters { + field: "table_name" + path_template: "{project_id=projects/*}/instances/*/**" + } + routing_parameters { + field: "table_name" + path_template: "projects/*/{instance_id=instances/*}/**" + } + }; + } + + // Example 7 + // + // Extracting multiple routing header key-value pairs by matching + // several path templates on multiple request fields. + rpc Example7Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // The routing needs both `project_id` and `routing_id` + // (from the `app_profile_id` field) for routing. + + routing_parameters { + field: "table_name" + path_template: "{project_id=projects/*}/**" + } + routing_parameters { + field: "app_profile_id" + path_template: "{routing_id=**}" + } + }; + } + + // Example 8 + // + // Extracting a single routing header key-value pair by matching + // several conflictingly named path templates on several request fields. The + // last template to match "wins" the conflict. + rpc Example8Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // The `routing_id` can be a project id or a region id depending on + // the table name format, but only if the `app_profile_id` is not set. + // If `app_profile_id` is set it should be used instead. + + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*}/**" + } + routing_parameters { + field: "table_name" + path_template: "{routing_id=regions/*}/**" + } + routing_parameters { + field: "app_profile_id" + path_template: "{routing_id=**}" + } + }; + } + + // Example 9 + // + // Bringing it all together. + rpc Example9Test(Request) returns (google.protobuf.Empty) { + option (google.api.routing) = { + // For routing both `table_location` and a `routing_id` are needed. + // + // table_location can be either an instance id or a region+zone id. + // + // For `routing_id`, take the value of `app_profile_id` + // - If it's in the format `profiles/`, send + // just the `` part. + // - If it's any other literal, send it as is. + // If the `app_profile_id` is empty, and the `table_name` starts with + // the project_id, send that instead. + + routing_parameters { + field: "table_name" + path_template: "projects/*/{table_location=instances/*}/tables/*" + } + routing_parameters { + field: "table_name" + path_template: "{table_location=regions/*/zones/*}/tables/*" + } + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*}/**" + } + routing_parameters { + field: "app_profile_id" + path_template: "{routing_id=**}" + } + routing_parameters { + field: "app_profile_id" + path_template: "profiles/{routing_id=*}" + } + }; + } + + //We should ignore http annotation if both http and routing header annotations are configured, + rpc BackwardsCompatible1Test(Request) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/v1beta1/{table_name=tests/*}" + }; + + option (google.api.routing) = { + routing_parameters { + field: "table_name" + path_template: "{routing_id=projects/*}/**" + } + }; + } + + //We should ignore http annotation even routing header annotation has no routing parameters + rpc BackwardsCompatible2Test(Request) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/v1beta1/{table_name=tests/*}" + }; + + option (google.api.routing) = { + + }; + } + + //Http annotation should still work for implicit routing headers as before if routing annotation is not configured. + rpc BackwardsCompatible3Test(Request) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/v1beta1/{table_name=tests/*}" + }; + } + + rpc NestedFieldTest(RequestWithNestedField) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "nested_field.another_nested_field.name" + path_template: "{routing_id=projects/*}/**" + } + }; + } +} + + + +// Example message: +// +// { +// table_name: projects/proj_foo/instances/instance_bar/table/table_baz, +// app_profile_id: profiles/prof_qux +// } +message Request { + // The name of the Table + // Values can be of the following formats: + // - `projects//tables/` + // - `projects//instances//tables/
` + // - `region//zones//tables/
` + string table_name = 1; + + // This value specifies routing for replication. + // It can be in the following formats: + // - `profiles/` + // - a legacy `profile_id` that can be any string + string app_profile_id = 2; +} + +message RequestWithNestedField { + NestedField nested_field = 1; +} + +message NestedField { + AnotherNestedField another_nested_field = 1; +} + +message AnotherNestedField { + string name = 1; +} + diff --git a/src/test/java/com/google/api/generator/gapic/testdata/routing_rule_parser_testing.proto b/src/test/java/com/google/api/generator/gapic/testdata/routing_rule_parser_testing.proto new file mode 100644 index 0000000000..a18de524eb --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/testdata/routing_rule_parser_testing.proto @@ -0,0 +1,143 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "google/api/routing.proto"; +import "google/protobuf/empty.proto"; + +package google.explicit.dynamic.routing.header; + +option java_package = "com.google.explicit.dynamic.routing.header"; +option java_multiple_files = true; + +// This service is only meant for unit testing RoutingRuleParser +service RoutingRuleParserTesting { + + // Test case for no routing rule found + rpc NoRoutingRuleTest(NoRoutingRuleTestRequest) returns (google.protobuf.Empty) { + + } + + // Test case for empty path template + rpc EmptyPathTemplateTest(EmptyPathTemplateTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "name" + } + }; + } + + // Test case for path template has zero named segment + rpc ZeroSegmentPathTemplateTest(ZeroSegmentPathTemplateTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "name" + path_template: "/v1beta1/tests/*" + } + }; + } + + // Test case for path template has more than one named segment + rpc MultipleSegmentsPathTemplateTest(MultipleSegmentsPathTemplateTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "name" + path_template: "/v1beta1/{name=tests/*}/{second_name=*}" + } + }; + } + + // Test case for happy path + rpc HappyPathTest(HappyPathTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "name" + path_template: "/v1beta1/{rename=tests/*}" + } + }; + } + + // Test case for happy path with multiple routing parameters + rpc MultipleRoutingParamsHappyPathTest(MultipleRoutingParamsHappyPathTestReqest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "name" + path_template: "/v1beta1/{rename=tests/*}" + } + routing_parameters { + field: "routing_id" + path_template: "/v1beta1/{id=projects/*}/tables/*" + } + }; + } + + // Test case for happy path with nested fields + rpc NestedFieldsHappyPathTest(NestedFieldsHappyPathTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "account.name" + path_template: "/v1beta1/{rename=tests/*}" + } + }; + } + + // Test case for field validation. We already have extensive unit tests for field validation, so only testing one simple case. + rpc FieldDoesNotExistTest(FieldDoesNotExistTestRequest) returns (google.protobuf.Empty) { + option (google.api.routing) = { + routing_parameters { + field: "does_not_exist" + path_template: "/v1beta1/{rename=tests/*}" + } + }; + } + +} + +message NoRoutingRuleTestRequest { +} + +message EmptyPathTemplateTestRequest { + string name = 1; +} + +message ZeroSegmentPathTemplateTestRequest { + string name = 1; +} + +message MultipleSegmentsPathTemplateTestRequest { + string name = 1; +} + +message HappyPathTestRequest { + string name = 1; +} + +message MultipleRoutingParamsHappyPathTestReqest { + string name = 1; + string routing_id = 2; +} + +message NestedFieldsHappyPathTestRequest { + Account account = 1; +} + +message FieldDoesNotExistTestRequest { + string name = 1; +} + +message Account { + string name = 1; +} + diff --git a/src/test/java/com/google/api/generator/gapic/testdata/testing.proto b/src/test/java/com/google/api/generator/gapic/testdata/testing.proto index 490e1d17cc..a7d15261a4 100644 --- a/src/test/java/com/google/api/generator/gapic/testdata/testing.proto +++ b/src/test/java/com/google/api/generator/gapic/testdata/testing.proto @@ -15,6 +15,7 @@ syntax = "proto3"; import "google/api/annotations.proto"; +import "google/api/routing.proto"; import "google/api/client.proto"; import "google/api/resource.proto"; import "google/protobuf/empty.proto"; @@ -78,6 +79,16 @@ service Testing { option (google.api.http) = { get: "/v1beta1/{name=tests/*}" }; + option (google.api.routing) = { + routing_parameters { + field: "name" + path_template: "/v1beta1/{rename=tests/*}" + } + routing_parameters { + field: "routing.name" + path_template: "/v1beta1/{routing_id=tests/*}" + } + }; option (google.api.method_signature) = "name"; } @@ -270,6 +281,11 @@ message GetTestRequest { // The session to be retrieved. string name = 1 [(google.api.resource_reference).type = "showcase.googleapis.com/Test"]; + Routing routing = 2; +} + +message Routing { + string name = 1; } message Test {