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 {