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 49c0aa0c87..9605e6bf32 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,15 +16,16 @@ import com.google.api.gax.grpc.GrpcCallSettings; import com.google.api.gax.grpc.GrpcStubCallableFactory; -import com.google.api.gax.rpc.RequestParamsExtractor; 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.MethodDefinition; import com.google.api.generator.engine.ast.MethodInvocationExpr; -import com.google.api.generator.engine.ast.NewObjectExpr; +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; @@ -34,13 +35,16 @@ import com.google.api.generator.engine.ast.VariableExpr; import com.google.api.generator.gapic.composer.common.AbstractTransportServiceStubClassComposer; import com.google.api.generator.gapic.composer.store.TypeStore; +import com.google.api.generator.gapic.model.GapicContext; 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.RoutingHeaders.RoutingHeader; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.gapic.utils.JavaStyle; +import com.google.api.pathtemplate.PathTemplate; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.longrunning.stub.GrpcOperationsStub; import io.grpc.MethodDescriptor; @@ -56,6 +60,7 @@ 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 @@ -212,19 +217,10 @@ protected Expr createTransportSettingsInitExpr( .build(); if (method.hasHttpBindings()) { - NewObjectExpr newExtractorExpr = - NewObjectExpr.builder() - .setType( - TypeNode.withReference( - ConcreteReference.withClazz(ExplicitRoutingHeaderExtractor.class))) - .setArguments() - .build(); callSettingsBuilderExpr = MethodInvocationExpr.builder() .setExprReferenceExpr(callSettingsBuilderExpr) .setMethodName("setParamsExtractor") - // set custom extractor - // .setArguments(newExtractorExpr) .setArguments(createRequestParamsExtractorClassInstance(method)) .build(); } @@ -241,6 +237,161 @@ protected Expr createTransportSettingsInitExpr( .build(); } + @Override + protected List createClassMethods( + GapicContext context, + Service service, + TypeStore typeStore, + Map classMemberVarExprs, + Map callableClassMemberVarExprs, + Map protoMethodNameToDescriptorVarExprs) { + List classMethods = + super.createClassMethods( + context, + service, + typeStore, + classMemberVarExprs, + callableClassMemberVarExprs, + protoMethodNameToDescriptorVarExprs); + // TODO: need a way to check do we need to create this method or not, or make it an inner method + classMethods.add(createAddParamsMethod()); + return classMethods; + } + + private MethodDefinition createAddParamsMethod() { + TypeNode paramsVarType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(ImmutableMap.Builder.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + VariableExpr paramsVarExpr = + VariableExpr.builder() + .setVariable(Variable.builder().setName("params").setType(paramsVarType).build()) + .setIsDecl(true) + .build(); + VariableExpr fieldValueExpr = + VariableExpr.builder() + .setVariable(Variable.builder().setName("fieldValue").setType(TypeNode.STRING).build()) + .setIsDecl(true) + .build(); + VariableExpr keyExpr = + VariableExpr.builder() + .setVariable(Variable.builder().setName("headerKey").setType(TypeNode.STRING).build()) + .setIsDecl(true) + .build(); + VariableExpr patternExpr = + VariableExpr.builder() + .setVariable(Variable.builder().setName("pattern").setType(TypeNode.STRING).build()) + .setIsDecl(true) + .build(); + List methodBody = new ArrayList<>(); + TypeNode pathTemplateType = + TypeNode.withReference(ConcreteReference.builder().setClazz(PathTemplate.class).build()); + VariableExpr pathTemplateVar = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("pathTemplate").setType(pathTemplateType).build()) + .setIsDecl(true) + .build(); + VariableExpr patternExprNonDecl = + VariableExpr.builder() + .setVariable(Variable.builder().setName("pattern").setType(TypeNode.STRING).build()) + .build(); + Expr pathTemplateExpr = + AssignmentExpr.builder() + .setVariableExpr(pathTemplateVar) + .setValueExpr( + MethodInvocationExpr.builder() + .setStaticReferenceType(pathTemplateType) + .setMethodName("create") + .setArguments(patternExprNonDecl) + .setReturnType(pathTemplateType) + .build()) + .build(); + ExprStatement exprStatement1 = ExprStatement.withExpr(pathTemplateExpr); + methodBody.add(exprStatement1); + + VariableExpr pathTemplateVarNonDecl = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("pathTemplate").setType(pathTemplateType).build()) + .build(); + TypeNode matchedValuesType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(Map.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + VariableExpr matchedValuesVar = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("matchedValues").setType(matchedValuesType).build()) + .setIsDecl(true) + .build(); + VariableExpr fieldValueExprNonDecl = + VariableExpr.builder() + .setVariable(Variable.builder().setName("fieldValue").setType(pathTemplateType).build()) + .build(); + Expr matchedValuesExpr = + AssignmentExpr.builder() + .setVariableExpr(matchedValuesVar) + .setValueExpr( + MethodInvocationExpr.builder() + .setExprReferenceExpr(pathTemplateVarNonDecl) + .setMethodName("match") + .setArguments(fieldValueExprNonDecl) + .setReturnType(matchedValuesType) + .build()) + .build(); + ExprStatement exprStatement2 = ExprStatement.withExpr(matchedValuesExpr); + methodBody.add(exprStatement2); + + VariableExpr matchedValuesVarNonDecl = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("matchedValues").setType(matchedValuesType).build()) + .build(); + Expr checkMatchedValuesNull = + RelationalOperationExpr.notEqualToWithExprs( + matchedValuesVarNonDecl, ValueExpr.createNullExpr()); + VariableExpr paramsVarExprNonDecl = + VariableExpr.builder() + .setVariable(Variable.builder().setName("params").setType(paramsVarType).build()) + .build(); + VariableExpr keyExprNonDecl = + VariableExpr.builder() + .setVariable(Variable.builder().setName("headerKey").setType(TypeNode.STRING).build()) + .build(); + Expr getMatchedValueExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(matchedValuesVarNonDecl) + .setMethodName("get") + .setArguments(keyExprNonDecl) + .build(); + Expr putParamsExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(paramsVarExprNonDecl) + .setMethodName("put") + .setArguments(keyExprNonDecl, getMatchedValueExpr) + .build(); + ExprStatement putParamsStatement = ExprStatement.withExpr(putParamsExpr); + IfStatement ifStatement = + IfStatement.builder() + .setConditionExpr(checkMatchedValuesNull) + .setBody(ImmutableList.of(putParamsStatement)) + .build(); + methodBody.add(ifStatement); + + return MethodDefinition.builder() + .setScope(ScopeNode.PROTECTED) + .setReturnType(TypeNode.VOID) + .setName("addParams") + .setBody(methodBody) + .setArguments(paramsVarExpr, fieldValueExpr, keyExpr, patternExpr) + .build(); + } + @Override protected String getProtoRpcFullMethodName(Service protoService, Method protoMethod) { if (protoMethod.isMixin()) { @@ -290,23 +441,9 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { 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 = + createRequestFieldGetterExpr(requestVarExpr, httpBindingFieldBinding.name()); - MethodInvocationExpr requestBuilderExpr = requestFieldGetterExprBuilder.build(); Expr valueOfExpr = MethodInvocationExpr.builder() .setStaticReferenceType(TypeNode.STRING) @@ -314,46 +451,39 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { .setArguments(requestBuilderExpr) .build(); - // TODO: completely remove this part if routing headers is not null? + // Comment out for now. TODO: completely remove this part if routing headers is not null? // Are these params used for anything else other than implicit dynamic routing? - Expr paramsPutExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(paramsVarExpr) - .setMethodName("put") - .setArguments( - ValueExpr.withValue(StringObjectValue.withValue(httpBindingFieldBinding.name())), - valueOfExpr) - .build(); - bodyExprs.add(paramsPutExpr); + // Expr paramsPutExpr = + // MethodInvocationExpr.builder() + // .setExprReferenceExpr(paramsVarExpr) + // .setMethodName("put") + // .setArguments( + // + // ValueExpr.withValue(StringObjectValue.withValue(httpBindingFieldBinding.name())), + // valueOfExpr) + // .build(); + // bodyExprs.add(paramsPutExpr); } - for (RoutingHeader routingHeader : method.routingHeaders().routingHeadersSet()) { - MethodInvocationExpr.Builder requestFieldGetterExprBuilder = - MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); - // TODO: support nested field - String currFieldName = routingHeader.field(); - String bindingFieldMethodName = - String.format("get%s", JavaStyle.toUpperCamelCase(currFieldName)); - requestFieldGetterExprBuilder = - requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); - - MethodInvocationExpr requestBuilderExpr = requestFieldGetterExprBuilder.build(); - Expr valueOfExpr = - MethodInvocationExpr.builder() - .setStaticReferenceType(TypeNode.STRING) - .setMethodName("valueOf") - .setArguments(requestBuilderExpr) - .build(); + for (RoutingHeader routingHeader : method.routingHeaders().routingHeadersList()) { + MethodInvocationExpr requestFieldGetterExpr = + createRequestFieldGetterExpr(requestVarExpr, routingHeader.field()); - Expr paramsPutExpr = + Expr routingHeaderKeyExpr = + ValueExpr.withValue(StringObjectValue.withValue(routingHeader.name())); + Expr routingHeaderPatternExpr = + ValueExpr.withValue(StringObjectValue.withValue(routingHeader.pattern())); + MethodInvocationExpr addParamsMethodExpr = MethodInvocationExpr.builder() - .setExprReferenceExpr(paramsVarExpr) - .setMethodName("put") + .setMethodName("addParams") .setArguments( - ValueExpr.withValue(StringObjectValue.withValue(routingHeader.name())), - valueOfExpr) + paramsVarExpr, + requestFieldGetterExpr, + routingHeaderKeyExpr, + routingHeaderPatternExpr) .build(); - bodyExprs.add(paramsPutExpr); + + bodyExprs.add(addParamsMethodExpr); } TypeNode returnType = @@ -379,12 +509,23 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) { .build(); } - class ExplicitRoutingHeaderExtractor implements RequestParamsExtractor { - - @Override - public Map extract(Object request) { - // no way to extract the field value since request type is dynamic - return null; + private MethodInvocationExpr createRequestFieldGetterExpr( + VariableExpr requestVarExpr, String fieldName) { + MethodInvocationExpr.Builder requestFieldGetterExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); + String[] descendantFields = fieldName.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()); + } } + return requestFieldGetterExprBuilder.build(); } } 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..d7eb5a1a2c 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,8 @@ 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.Strings; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; @@ -32,6 +34,13 @@ @AutoValue public abstract class Message { + + static final String EMPTY_FIELD_ERROR_MESSAGE = "Null or empty field name found for message %s"; + static final String FIELD_DOES_NOT_EXIST_ERROR_MESSAGE = + "Expected message %s to contain field %s but none found"; + static final String MESSAGE_NOT_FOUND_ERROR_MESSAGE = + "No containing message found for field %s with type %s"; + public abstract String name(); // The fully-qualified proto name, which differs from the Java fully-qualified name. @@ -80,6 +89,32 @@ public boolean hasResource() { return resource() != null; } + public void validateField(String fieldName, Map messageTypes) { + String[] subFields = fieldName.split("\\."); + Message nestedMessage = this; + for (int i = 0; i < subFields.length; i++) { + String subFieldName = subFields[i]; + if (i < subFields.length - 1) { + Field field = nestedMessage.fieldMap().get(subFieldName); + nestedMessage = messageTypes.get(field.type().reference().fullName()); + Preconditions.checkNotNull( + nestedMessage, + String.format( + MESSAGE_NOT_FOUND_ERROR_MESSAGE, + field.name(), + field.type().reference().simpleName())); + } else { + Preconditions.checkState( + !Strings.isNullOrEmpty(subFieldName), + String.format(EMPTY_FIELD_ERROR_MESSAGE, nestedMessage.name())); + Preconditions.checkState( + nestedMessage.fieldMap().containsKey(subFieldName), + String.format(FIELD_DOES_NOT_EXIST_ERROR_MESSAGE, nestedMessage.name(), subFieldName)); + // TODO: Add type check for String only? + } + } + } + /** 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/RoutingHeaders.java b/src/main/java/com/google/api/generator/gapic/model/RoutingHeaders.java index ee84b56cd6..c4c260970c 100644 --- a/src/main/java/com/google/api/generator/gapic/model/RoutingHeaders.java +++ b/src/main/java/com/google/api/generator/gapic/model/RoutingHeaders.java @@ -15,16 +15,15 @@ package com.google.api.generator.gapic.model; import com.google.auto.value.AutoValue; -import java.util.List; +import com.google.common.collect.ImmutableList; @AutoValue public abstract class RoutingHeaders { - public abstract List routingHeadersSet(); + public abstract ImmutableList routingHeadersList(); - // TODO: change to a builder and expose only add method - public static RoutingHeaders create(List routingHeaderList) { - return new AutoValue_RoutingHeaders(routingHeaderList); + public static Builder builder() { + return new AutoValue_RoutingHeaders.Builder().setRoutingHeadersList(ImmutableList.of()); } @AutoValue @@ -40,4 +39,18 @@ public static RoutingHeaders.RoutingHeader create(String field, String name, Str return new AutoValue_RoutingHeaders_RoutingHeader(field, name, pattern); } } + + @AutoValue.Builder + public abstract static class Builder { + abstract ImmutableList.Builder routingHeadersListBuilder(); + + public final Builder addRoutingHeader(RoutingHeader routingHeader) { + routingHeadersListBuilder().add(routingHeader); + return this; + } + + public abstract Builder setRoutingHeadersList(ImmutableList routingHeadersList); + + public abstract RoutingHeaders 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 index 8386b8dce2..a96e0203a2 100644 --- a/src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java +++ b/src/main/java/com/google/api/generator/gapic/protoparser/RoutingRuleParser.java @@ -17,7 +17,6 @@ import com.google.api.RoutingParameter; import com.google.api.RoutingProto; import com.google.api.RoutingRule; -import com.google.api.generator.gapic.model.Field; import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.RoutingHeaders; import com.google.api.generator.gapic.model.RoutingHeaders.RoutingHeader; @@ -27,69 +26,53 @@ import com.google.common.collect.ImmutableSortedSet; import com.google.protobuf.DescriptorProtos.MethodOptions; import com.google.protobuf.Descriptors.MethodDescriptor; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; public class RoutingRuleParser { - private static final String ASTERISK = "**"; + static final String WILDCARD_PATTERN = "{%s=**}"; + static final String PATH_TEMPLATE_WRONG_NUMBER_OF_NAMED_SEGMENT_ERROR_MESSAGE = + "There needs to be one and only one named segment in path template %s"; public static RoutingHeaders parse( MethodDescriptor protoMethod, Message inputMessage, Map messageTypes) { MethodOptions methodOptions = protoMethod.getOptions(); + RoutingHeaders.Builder builder = RoutingHeaders.builder(); if (!methodOptions.hasExtension(RoutingProto.routing)) { - return RoutingHeaders.create(Collections.emptyList()); + return builder.build(); } RoutingRule routingRule = methodOptions.getExtension(RoutingProto.routing); - return parseHttpRuleHelper(routingRule, Optional.of(inputMessage), messageTypes); - } - - private static RoutingHeaders parseHttpRuleHelper( - RoutingRule routingRule, - Optional inputMessageOpt, - Map messageTypes) { - - // one field may map to multiple headers, Example 6 - // multiple fields may map to one header as well, Example 8, last wins - // one combination of field/name may have multiple patterns, the one matches win, Example 3c. - List routingHeaderSet = new ArrayList<>(); for (RoutingParameter routingParameter : routingRule.getRoutingParametersList()) { String pathTemplate = routingParameter.getPathTemplate(); String field = routingParameter.getField(); - // TODO: Validate if field exist in Message or nested Messages - // If failed, stop or ignore? stop? - checkHttpFieldIsValid(field, inputMessageOpt.get(), false); - - // TODO: Validate the pattern, if specified and not **, the pattern must contain one and - // only one named segment - // If failed, stop or ignore? stop? - Set params = getPatternBindings(pathTemplate).build(); - String name = field; - // set name to field if empty, Example 1 - if (!params.isEmpty()) { + // validate if field exist in Message or nested Messages + inputMessage.validateField(field, messageTypes); + String name; + // if specified, the pattern must contain one and only one named segment + if (Strings.isNullOrEmpty(pathTemplate)) { + name = field; + pathTemplate = String.format(WILDCARD_PATTERN, name); + } else { + Set params = getPatternBindings(pathTemplate).build(); + Preconditions.checkArgument( + params.size() == 1, + String.format(PATH_TEMPLATE_WRONG_NUMBER_OF_NAMED_SEGMENT_ERROR_MESSAGE, pathTemplate)); name = params.iterator().next(); } - // set path to ** if empty, Example 1 - if (pathTemplate.isEmpty()) { - pathTemplate = ASTERISK; - } - RoutingHeader routingHeader = RoutingHeader.create(field, name, pathTemplate); - routingHeaderSet.add(routingHeader); + builder.addRoutingHeader(routingHeader); } - return RoutingHeaders.create(routingHeaderSet); + return builder.build(); } + // TODO: duplicate of HttpRuleParser.getPatternBindings, move to a helper class private static ImmutableSortedSet.Builder getPatternBindings(String pattern) { ImmutableSortedSet.Builder bindings = ImmutableSortedSet.naturalOrder(); if (pattern.isEmpty()) { @@ -102,29 +85,4 @@ private static ImmutableSortedSet.Builder getPatternBindings(String patt template.vars().stream().filter(s -> !s.contains("$")).collect(Collectors.toSet())); return bindings; } - - // TODO:Move to Message.java, also need to handle nested fields. The field has to be of type - // String - private static void checkHttpFieldIsValid(String binding, Message inputMessage, boolean isBody) { - Preconditions.checkState( - !Strings.isNullOrEmpty(binding), - String.format("Null or empty binding for " + inputMessage.name())); - Preconditions.checkState( - inputMessage.fieldMap().containsKey(binding), - String.format( - "Expected message %s to contain field %s but none found", - inputMessage.name(), binding)); - Field field = inputMessage.fieldMap().get(binding); - boolean fieldCondition = !field.isRepeated(); - if (!isBody) { - fieldCondition &= field.type().isProtoPrimitiveType() || field.isEnum(); - } - String messageFormat = - "Expected a non-repeated " - + (isBody ? "" : "primitive ") - + "type for field %s in message %s but got type %s"; - Preconditions.checkState( - fieldCondition, - String.format(messageFormat, field.name(), inputMessage.name(), field.type())); - } } 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 b2eee7cc0b..a8d8ad52a5 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 @@ -10,6 +10,7 @@ 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.UnaryCallable; +import com.google.api.pathtemplate.PathTemplate; import com.google.common.collect.ImmutableMap; import com.google.longrunning.stub.GrpcOperationsStub; import com.google.protobuf.Empty; @@ -187,7 +188,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); return params.build(); }) .build(); @@ -201,7 +201,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); return params.build(); }) .build(); @@ -211,7 +210,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); return params.build(); }) .build(); @@ -221,8 +219,12 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); - params.put("rename", String.valueOf(request.getName())); + addParams(params, request.getName(), "rename", "/v1beta1/{rename=tests/*}"); + addParams( + params, + request.getTableName(), + "routing_id", + "/v1beta1/{routing_id=tests/*}"); return params.build(); }) .build(); @@ -232,7 +234,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("parent", String.valueOf(request.getParent())); return params.build(); }) .build(); @@ -242,7 +243,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); return params.build(); }) .build(); @@ -252,12 +252,6 @@ public class GrpcTestingStub extends TestingStub { .setParamsExtractor( request -> { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("answer", String.valueOf(request.getAnswer())); - params.put("foo", String.valueOf(request.getFoo())); - params.put("name", String.valueOf(request.getName())); - params.put( - "test_to_verify.name", String.valueOf(request.getTestToVerify().getName())); - params.put("type", String.valueOf(request.getType())); return params.build(); }) .build(); @@ -394,4 +388,16 @@ public class GrpcTestingStub extends TestingStub { public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { return backgroundResources.awaitTermination(duration, unit); } + + protected void addParams( + ImmutableMap.Builder params, + String fieldValue, + String headerKey, + String pattern) { + PathTemplate pathTemplate = PathTemplate.create(pattern); + Map matchedValues = pathTemplate.match(fieldValue); + if (matchedValues != null) { + params.put(headerKey, matchedValues.get(headerKey)); + } + } } 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..5bcebf9047 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,7 @@ TESTS = [ "GapicServiceConfigTest", "MethodArgumentTest", "MethodTest", + "MessageTest", ] 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..fe5c54085a --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/model/MessageTest.java @@ -0,0 +1,108 @@ +// 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.api.generator.gapic.model.Message.EMPTY_FIELD_ERROR_MESSAGE; +import static com.google.api.generator.gapic.model.Message.FIELD_DOES_NOT_EXIST_ERROR_MESSAGE; +import static com.google.api.generator.gapic.model.Message.MESSAGE_NOT_FOUND_ERROR_MESSAGE; +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 { + + public static final String MESSAGE_NAME = "TestMessage"; + public static final Message.Builder TEST_MESSAGE_BUILDER = + Message.builder() + .setName(MESSAGE_NAME) + .setFullProtoName("com.google.test.TestMessage") + .setType(TypeNode.OBJECT); + + @Test + public void shouldThrowExceptionIfFieldNameIsEmpty() { + Message message = TEST_MESSAGE_BUILDER.build(); + IllegalStateException illegalStateException = + assertThrows( + IllegalStateException.class, () -> message.validateField("", ImmutableMap.of())); + assertThat(illegalStateException.getMessage()) + .isEqualTo(String.format(EMPTY_FIELD_ERROR_MESSAGE, MESSAGE_NAME)); + } + + @Test + public void shouldThrowExceptionIfFieldDoesNotExist() { + Message message = TEST_MESSAGE_BUILDER.build(); + String fieldName = "doesNotExist"; + IllegalStateException illegalStateException = + assertThrows( + IllegalStateException.class, () -> message.validateField(fieldName, ImmutableMap.of())); + assertThat(illegalStateException.getMessage()) + .isEqualTo(String.format(FIELD_DOES_NOT_EXIST_ERROR_MESSAGE, MESSAGE_NAME, fieldName)); + } + + @Test + public void shouldThrowExceptionIfMessageDoesNotExist() { + String subFieldName = "table"; + String fieldTypeName = "doesNotMatter"; + Field subField = + Field.builder() + .setName(subFieldName) + .setType( + TypeNode.withReference( + VaporReference.builder() + .setPakkage("com.google") + .setName(fieldTypeName) + .build())) + .build(); + Message message = + TEST_MESSAGE_BUILDER.setFieldMap(ImmutableMap.of(subFieldName, subField)).build(); + String fieldName = subFieldName + "." + "size"; + NullPointerException illegalStateException = + assertThrows( + NullPointerException.class, () -> message.validateField(fieldName, ImmutableMap.of())); + assertThat(illegalStateException.getMessage()) + .isEqualTo(String.format(MESSAGE_NOT_FOUND_ERROR_MESSAGE, subFieldName, fieldTypeName)); + } + + @Test + public void shouldNotThrowExceptionIfFieldExist() { + String subFieldName = "table"; + String fieldTypeName = "TableFieldType"; + VaporReference fieldType = + VaporReference.builder().setPakkage("com.google").setName(fieldTypeName).build(); + Field subField = + Field.builder().setName(subFieldName).setType(TypeNode.withReference(fieldType)).build(); + String subFieldName2 = "size"; + String fieldName = subFieldName + "." + subFieldName2; + Message subMessage = + Message.builder() + .setName(fieldTypeName) + .setFullProtoName("com.google." + fieldTypeName) + .setType(TypeNode.OBJECT) + .setFieldMap( + ImmutableMap.of( + subFieldName2, + Field.builder().setType(TypeNode.STRING).setName(subFieldName2).build())) + .build(); + Map messageTypes = ImmutableMap.of(fieldType.fullName(), subMessage); + Message message = + TEST_MESSAGE_BUILDER.setFieldMap(ImmutableMap.of(subFieldName, subField)).build(); + message.validateField(fieldName, messageTypes); + } +} 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 index e3039f5e33..c3465d921b 100644 --- a/src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java +++ b/src/test/java/com/google/api/generator/gapic/protoparser/RoutingRuleParserTest.java @@ -14,40 +14,74 @@ package com.google.api.generator.gapic.protoparser; +import static com.google.api.generator.gapic.protoparser.RoutingRuleParser.PATH_TEMPLATE_WRONG_NUMBER_OF_NAMED_SEGMENT_ERROR_MESSAGE; +import static com.google.api.generator.gapic.protoparser.RoutingRuleParser.WILDCARD_PATTERN; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.RoutingHeaders; +import com.google.api.generator.gapic.model.RoutingHeaders.RoutingHeader; import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.Descriptors.ServiceDescriptor; -import com.google.showcase.v1beta1.TestingOuterClass; +import com.google.showcase.v1beta1.RoutingRuleParserTestingOuterClass; import java.util.Map; -import java.util.stream.Collectors; import org.junit.Test; public class RoutingRuleParserTest { - // Do we have to test against a real proto file - // Maybe manually create a MethodDescriptor and test against it? more flexibility and precise. - // testing.proto is being used by other tests as well + private static final FileDescriptor TESTING_FILE_DESCRIPTOR = + RoutingRuleParserTestingOuterClass.getDescriptor(); + private static final Map MESSAGES = + Parser.parseMessages(TESTING_FILE_DESCRIPTOR); + private static final ServiceDescriptor TESTING_SERVICE = + TESTING_FILE_DESCRIPTOR.getServices().get(0); + + @Test + public void shouldReturnEmptyRoutingHeadersIfMethodHasNoRoutingRules() { + MethodDescriptor rpcMethod = TESTING_SERVICE.getMethods().get(0); + Message inputMessage = MESSAGES.get("com." + rpcMethod.getInputType().getFullName()); + RoutingHeaders routingHeaders = RoutingRuleParser.parse(rpcMethod, inputMessage, MESSAGES); + assertThat(routingHeaders.routingHeadersList()).isEmpty(); + } + + @Test + public void shouldSetPathTemplateToWildcardIfNotDefined() { + MethodDescriptor rpcMethod = TESTING_SERVICE.getMethods().get(1); + Message inputMessage = MESSAGES.get("com." + rpcMethod.getInputType().getFullName()); + RoutingHeaders routingHeaders = RoutingRuleParser.parse(rpcMethod, inputMessage, MESSAGES); + RoutingHeader routingHeader = + RoutingHeader.create("name", "name", String.format(WILDCARD_PATTERN, "name")); + assertThat(routingHeaders.routingHeadersList()).containsExactly(routingHeader); + } + + @Test + public void shouldThrowExceptionIfPathTemplateHasZeroNamedSegment() { + MethodDescriptor rpcMethod = TESTING_SERVICE.getMethods().get(2); + Message inputMessage = MESSAGES.get("com." + rpcMethod.getInputType().getFullName()); + IllegalArgumentException illegalArgumentException = + assertThrows( + IllegalArgumentException.class, + () -> RoutingRuleParser.parse(rpcMethod, inputMessage, MESSAGES)); + assertThat(illegalArgumentException.getMessage()) + .isEqualTo( + String.format( + PATH_TEMPLATE_WRONG_NUMBER_OF_NAMED_SEGMENT_ERROR_MESSAGE, "/v1beta1/tests/*")); + } + @Test - public void parseRoutingRuleAnnotation() { - FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor(); - ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0); - assertEquals("Testing", testingService.getName()); - - Map messages = Parser.parseMessages(testingFileDescriptor); - - // GetTest method. - MethodDescriptor rpcMethod = testingService.getMethods().get(5); - Message inputMessage = messages.get("com.google.showcase.v1beta1.GetTestRequest"); - RoutingHeaders routingHeaders = RoutingRuleParser.parse(rpcMethod, inputMessage, messages); - assertThat( - routingHeaders.routingHeadersSet().stream() - .map(routingHeader -> routingHeader.field() + " -> " + routingHeader.name()) - .collect(Collectors.toList())) - .containsExactly("name -> rename"); + public void shouldThrowExceptionIfPathTemplateHasMoreThanOneNamedSegment() { + MethodDescriptor rpcMethod = TESTING_SERVICE.getMethods().get(3); + Message inputMessage = MESSAGES.get("com." + rpcMethod.getInputType().getFullName()); + IllegalArgumentException illegalArgumentException = + assertThrows( + IllegalArgumentException.class, + () -> RoutingRuleParser.parse(rpcMethod, inputMessage, MESSAGES)); + assertThat(illegalArgumentException.getMessage()) + .isEqualTo( + String.format( + PATH_TEMPLATE_WRONG_NUMBER_OF_NAMED_SEGMENT_ERROR_MESSAGE, + "/v1beta1/{name=tests/*}/{second_name=*}")); } } 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 9bc783cdaa..faea4cf584 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 @@ -51,6 +51,7 @@ proto_library( "identity.proto", "testing.proto", "messaging.proto", + "routing_rule_parser_testing.proto", ], deps = [ "@com_google_googleapis//google/api:annotations_proto", 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..031ea45641 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/testdata/routing_rule_parser_testing.proto @@ -0,0 +1,130 @@ +// Copyright 2018 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.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; + +// This service is meant to only run 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/*}" + } + }; + } + +} + +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 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 ef4cc2cc1c..8ef4fd2448 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 @@ -84,6 +84,10 @@ service Testing { field: "name" path_template: "/v1beta1/{rename=tests/*}" } + routing_parameters { + field: "table_name" + path_template: "/v1beta1/{routing_id=tests/*}" + } }; option (google.api.method_signature) = "name"; } @@ -277,6 +281,7 @@ message GetTestRequest { // The session to be retrieved. string name = 1 [(google.api.resource_reference).type = "showcase.googleapis.com/Test"]; + string table_name = 2; } message Test {