Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ggj][codegen] feat: add HTTP annotation parsing/validation #401

Merged
merged 6 commits into from
Oct 24, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add HTTP annotation parsing/validation
  • Loading branch information
miraleung committed Oct 21, 2020
commit e15e1157ac8e8d4f2273c83a919a5c064ff62084
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));
}
}