Skip to content

Commit

Permalink
[ggj][codegen] feat: add HTTP annotation parsing/validation (#401)
Browse files Browse the repository at this point in the history
* fix: refactor requestBuilder into separate method in ServiceClientClassComposer

* feat: add varargs to AnonClass and ref setter methods

* feat: add HTTP annotation parsing/validation
  • Loading branch information
miraleung authored Oct 24, 2020
1 parent d0a3e3e commit bbf070c
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/google/api/generator/gapic/model/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> httpBindings();

// Example from Expand in echo.proto: Thet TypeNodes that map to
// [["content", "error"], ["content", "error", "info"]].
public abstract ImmutableList<List<MethodArgument>> methodSignatures();
Expand All @@ -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);
}

Expand Down Expand Up @@ -91,6 +100,8 @@ public abstract static class Builder {

public abstract Builder setDescription(String description);

public abstract Builder setHttpBindings(List<String> httpBindings);

public abstract Builder setMethodSignatures(List<List<MethodArgument>> methodSignature);

public abstract Builder setIsPaged(boolean isPaged);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<String>> parseHttpBindings(
MethodDescriptor protoMethod, Message inputMessage, Map<String, Message> 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<String> 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<String> 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<String> bindings = new ArrayList<String>(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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -276,6 +277,12 @@ static List<Method> parseMethods(
}
}

Optional<List<String>> httpBindingsOpt =
HttpRuleParser.parseHttpBindings(
protoMethod, messageTypes.get(inputType.reference().name()), messageTypes);
List<String> httpBindings =
httpBindingsOpt.isPresent() ? httpBindingsOpt.get() : Collections.emptyList();

methods.add(
methodBuilder
.setName(protoMethod.getName())
Expand All @@ -292,6 +299,7 @@ static List<Method> parseMethods(
messageTypes,
resourceNames,
outputArgResourceNames))
.setHttpBindings(httpBindings)
.setIsPaged(parseIsPaged(protoMethod, messageTypes))
.build());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package(default_visibility = ["//visibility:public"])

TESTS = [
"BatchingSettingsConfigParserTest",
"HttpRuleParserTest",
"MethodSignatureParserTest",
"ParserTest",
"PluginArgumentParserTest",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Message> messages = Parser.parseMessages(testingFileDescriptor);

// CreateSession method.
MethodDescriptor rpcMethod = testingService.getMethods().get(0);
Message inputMessage = messages.get("CreateSessionRequest");
Optional<List<String>> 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<String, Message> 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));
}
}

0 comments on commit bbf070c

Please sign in to comment.