diff --git a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java index 9505cbb8cc..49fa35aaac 100644 --- a/src/main/java/com/google/api/generator/engine/ast/TypeNode.java +++ b/src/main/java/com/google/api/generator/engine/ast/TypeNode.java @@ -186,6 +186,10 @@ public boolean isPrimitiveType() { return isPrimitiveType(typeKind()); } + public boolean isProtoPrimitiveType() { + return isPrimitiveType() || this.equals(TypeNode.STRING); + } + public boolean isSupertypeOrEquals(TypeNode other) { boolean oneTypeIsNull = this.equals(TypeNode.NULL) ^ other.equals(TypeNode.NULL); return !isPrimitiveType() 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 30e94c9ad5..2d78b5f941 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 @@ -45,6 +45,10 @@ public enum Stream { @Nullable public abstract String description(); + // TODO(miraleung): May need to change this to MethodArgument, Field, or some new struct + // HttpBinding pending dotted reference support. + public abstract List httpBindings(); + // Example from Expand in echo.proto: Thet TypeNodes that map to // [["content", "error"], ["content", "error", "info"]]. public abstract ImmutableList> methodSignatures(); @@ -57,10 +61,15 @@ public boolean hasDescription() { return description() != null; } + public boolean hasHttpBindings() { + return !httpBindings().isEmpty(); + } + public static Builder builder() { return new AutoValue_Method.Builder() .setStream(Stream.NONE) .setMethodSignatures(ImmutableList.of()) + .setHttpBindings(ImmutableList.of()) .setIsPaged(false); } @@ -91,6 +100,8 @@ public abstract static class Builder { public abstract Builder setDescription(String description); + public abstract Builder setHttpBindings(List httpBindings); + public abstract Builder setMethodSignatures(List> methodSignature); public abstract Builder setIsPaged(boolean isPaged); 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 new file mode 100644 index 0000000000..e48344afde --- /dev/null +++ b/src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java @@ -0,0 +1,129 @@ +// 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.AnnotationsProto; +import com.google.api.HttpRule; +import com.google.api.HttpRule.PatternCase; +import com.google.api.generator.gapic.model.Field; +import com.google.api.generator.gapic.model.Message; +import com.google.api.pathtemplate.PathTemplate; +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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class HttpRuleParser { + private static final String ASTERISK = "*"; + + public static Optional> parseHttpBindings( + MethodDescriptor protoMethod, Message inputMessage, Map messageTypes) { + MethodOptions methodOptions = protoMethod.getOptions(); + if (!methodOptions.hasExtension(AnnotationsProto.http)) { + return Optional.empty(); + } + + HttpRule httpRule = methodOptions.getExtension(AnnotationsProto.http); + + // Body validation. + if (!Strings.isNullOrEmpty(httpRule.getBody()) && !httpRule.getBody().equals(ASTERISK)) { + checkHttpFieldIsValid(httpRule.getBody(), inputMessage, true); + } + + // Get pattern. + List bindings = getPatternBindings(httpRule); + if (bindings.isEmpty()) { + return Optional.empty(); + } + + // Binding validation. + for (String binding : bindings) { + // Handle foo.bar cases by descending into the subfields. + String[] descendantBindings = binding.split("\\."); + Message containingMessage = inputMessage; + for (int i = 0; i < descendantBindings.length; i++) { + String subField = descendantBindings[i]; + if (i < descendantBindings.length - 1) { + Field field = containingMessage.fieldMap().get(subField); + containingMessage = messageTypes.get(field.type().reference().name()); + } else { + checkHttpFieldIsValid(subField, containingMessage, false); + } + } + } + + return Optional.of(bindings); + } + + private static List getPatternBindings(HttpRule httpRule) { + String pattern = null; + // Assign a temp variable to prevent the formatter from removing the import. + PatternCase patternCase = httpRule.getPatternCase(); + switch (patternCase) { + case GET: + pattern = httpRule.getGet(); + break; + case PUT: + pattern = httpRule.getPut(); + break; + case POST: + pattern = httpRule.getPost(); + break; + case DELETE: + pattern = httpRule.getDelete(); + break; + case PATCH: + pattern = httpRule.getPatch(); + break; + case CUSTOM: // Invalid pattern. + // Fall through. + default: + return Collections.emptyList(); + } + + PathTemplate template = PathTemplate.create(pattern); + List bindings = new ArrayList(template.vars()); + Collections.sort(bindings); + return bindings; + } + + private static void checkHttpFieldIsValid(String binding, Message inputMessage, boolean isBody) { + Preconditions.checkState( + !Strings.isNullOrEmpty(binding), + String.format("DEL: 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(); + } + 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/main/java/com/google/api/generator/gapic/protoparser/Parser.java b/src/main/java/com/google/api/generator/gapic/protoparser/Parser.java index 43e7e7c435..98e7fe5f5d 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 @@ -52,6 +52,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -276,6 +277,12 @@ static List parseMethods( } } + Optional> httpBindingsOpt = + HttpRuleParser.parseHttpBindings( + protoMethod, messageTypes.get(inputType.reference().name()), messageTypes); + List httpBindings = + httpBindingsOpt.isPresent() ? httpBindingsOpt.get() : Collections.emptyList(); + methods.add( methodBuilder .setName(protoMethod.getName()) @@ -292,6 +299,7 @@ static List parseMethods( messageTypes, resourceNames, outputArgResourceNames)) + .setHttpBindings(httpBindings) .setIsPaged(parseIsPaged(protoMethod, messageTypes)) .build()); 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 cc3063d4db..a7df4ab20b 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 @@ -4,6 +4,7 @@ package(default_visibility = ["//visibility:public"]) TESTS = [ "BatchingSettingsConfigParserTest", + "HttpRuleParserTest", "MethodSignatureParserTest", "ParserTest", "PluginArgumentParserTest", diff --git a/src/test/java/com/google/api/generator/gapic/protoparser/HttpRuleParserTest.java b/src/test/java/com/google/api/generator/gapic/protoparser/HttpRuleParserTest.java new file mode 100644 index 0000000000..4f20e23a33 --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/protoparser/HttpRuleParserTest.java @@ -0,0 +1,73 @@ +// 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 junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertThrows; + +import com.google.api.generator.gapic.model.Message; +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 java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; + +public class HttpRuleParserTest { + @Test + public void parseHttpAnnotation_basic() { + FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor(); + ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0); + assertEquals(testingService.getName(), "Testing"); + + Map messages = Parser.parseMessages(testingFileDescriptor); + + // CreateSession method. + MethodDescriptor rpcMethod = testingService.getMethods().get(0); + Message inputMessage = messages.get("CreateSessionRequest"); + Optional> httpBindingsOpt = + HttpRuleParser.parseHttpBindings(rpcMethod, inputMessage, messages); + assertFalse(httpBindingsOpt.isPresent()); + + // VerityTest method. + rpcMethod = testingService.getMethods().get(testingService.getMethods().size() - 1); + inputMessage = messages.get("VerifyTestRequest"); + httpBindingsOpt = HttpRuleParser.parseHttpBindings(rpcMethod, inputMessage, messages); + assertTrue(httpBindingsOpt.isPresent()); + assertThat(httpBindingsOpt.get()).containsExactly("name"); + } + + @Test + public void parseHttpAnnotation_missingFieldFromMessage() { + FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor(); + ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0); + assertEquals(testingService.getName(), "Testing"); + + Map messages = Parser.parseMessages(testingFileDescriptor); + + // VerityTest method. + MethodDescriptor rpcMethod = + testingService.getMethods().get(testingService.getMethods().size() - 1); + Message inputMessage = messages.get("CreateSessionRequest"); + assertThrows( + IllegalStateException.class, + () -> HttpRuleParser.parseHttpBindings(rpcMethod, inputMessage, messages)); + } +}