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

fix: [gapic-generator-java] handle response and metadata type ambiguity in LRO parsing #1726

Merged
merged 19 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8096dbd
test: uncomment showcase-extended setup
emmileaf May 24, 2023
c5e0b30
test: adjustments for showcase-extended setup
emmileaf May 25, 2023
ca740ba
test: add proto to reproduce type collision issue
emmileaf May 25, 2023
7ce4084
test: add golden generated files for wicked and collisions protos
emmileaf May 31, 2023
76d8fba
fix: update LRO parsing to check package for short-form response and …
emmileaf May 31, 2023
2e1c69e
tests: exclude Location edge case from test proto and update goldens
emmileaf May 31, 2023
e4c4916
Merge branch 'main' into showcase-extended-explore
emmileaf May 31, 2023
7c759a9
test: update showcase-extended goldens after merging changes from main
emmileaf May 31, 2023
67fecec
fix: recursively write generics on methodInvocationExpr to account fo…
emmileaf Jun 1, 2023
30d8a0d
test: add Location edge case back to test proto and update goldens
emmileaf Jun 1, 2023
b73d72a
test: add service description to new test proto
emmileaf Jun 1, 2023
de5e9c3
chore: refactor type name matching into private helper
emmileaf Jun 13, 2023
b7952a4
test: turn off showcase-extended setup
emmileaf Jun 13, 2023
69e3a27
test: remove golden files previously generated by showcase-extended s…
emmileaf Jun 13, 2023
1ff61f4
test: add unit tests to TypeParserTest
emmileaf Jun 13, 2023
3703269
test: add writer unit test for generics import collision fix
emmileaf Jun 14, 2023
06dbf1f
test: undo changes to showcase-extended setup
emmileaf Jun 14, 2023
ba60786
test: update unit tests for lro type parsing
emmileaf Jun 14, 2023
ba1d1af
test: update unit tests for writer import collision fix
emmileaf Jun 14, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ public void visit(MethodInvocationExpr methodInvocationExpr) {
leftAngle();
int numGenerics = methodInvocationExpr.generics().size();
for (int i = 0; i < numGenerics; i++) {
buffer.append(methodInvocationExpr.generics().get(i).name());
Reference r = methodInvocationExpr.generics().get(i);
r.accept(this);
if (i < numGenerics - 1) {
buffer.append(COMMA);
space();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,21 @@ static List<Method> parseMethods(
return methods;
}

private static String fetchTypeFullName(String typeName, MethodDescriptor methodDescriptor) {
// When provided type name is fully qualified, return as-is
// When only shortname is provided, assume same proto package as method (See
// https://aip.dev/151)
int lastDotIndex = typeName.lastIndexOf('.');
boolean isResponseTypeNameShortOnly = lastDotIndex < 0;
String responseTypeShortName =
lastDotIndex >= 0 ? typeName.substring(lastDotIndex + 1) : typeName;
String typeFullName =
isResponseTypeNameShortOnly
? methodDescriptor.getFile().getPackage() + "." + responseTypeShortName
: typeName;
return typeFullName;
}

@VisibleForTesting
static LongrunningOperation parseLro(
String servicePackage, MethodDescriptor methodDescriptor, Map<String, Message> messageTypes) {
Expand Down Expand Up @@ -820,43 +835,19 @@ static LongrunningOperation parseLro(
Message responseMessage = null;
Message metadataMessage = null;

int lastDotIndex = responseTypeName.lastIndexOf('.');
boolean isResponseTypeNameShortOnly = lastDotIndex < 0;
String responseTypeShortName =
lastDotIndex >= 0 ? responseTypeName.substring(lastDotIndex + 1) : responseTypeName;

lastDotIndex = metadataTypeName.lastIndexOf('.');
boolean isMetadataTypeNameShortOnly = lastDotIndex < 0;
String metadataTypeShortName =
lastDotIndex >= 0 ? metadataTypeName.substring(lastDotIndex + 1) : metadataTypeName;
String responseTypeFullName = fetchTypeFullName(responseTypeName, methodDescriptor);
String metadataTypeFullName = fetchTypeFullName(metadataTypeName, methodDescriptor);

// The messageTypes map keys to the Java fully-qualified name.
for (Map.Entry<String, Message> messageEntry : messageTypes.entrySet()) {
String messageKey = messageEntry.getKey();
int messageLastDotIndex = messageEntry.getKey().lastIndexOf('.');
String messageShortName =
messageLastDotIndex >= 0 ? messageKey.substring(messageLastDotIndex + 1) : messageKey;
if (responseMessage == null) {
if (isResponseTypeNameShortOnly && responseTypeName.equals(messageShortName)) {
responseMessage = messageEntry.getValue();
} else if (!isResponseTypeNameShortOnly && responseTypeShortName.equals(messageShortName)) {
// Ensure that the full proto name matches.
Message candidateMessage = messageEntry.getValue();
if (candidateMessage.fullProtoName().equals(responseTypeName)) {
responseMessage = candidateMessage;
}
}
Message candidateMessage = messageEntry.getValue();
if (responseMessage == null
&& candidateMessage.fullProtoName().equals(responseTypeFullName)) {
responseMessage = candidateMessage;
}
if (metadataMessage == null) {
if (isMetadataTypeNameShortOnly && metadataTypeName.equals(messageShortName)) {
metadataMessage = messageEntry.getValue();
} else if (!isMetadataTypeNameShortOnly && metadataTypeShortName.equals(messageShortName)) {
// Ensure that the full proto name matches.
Message candidateMessage = messageEntry.getValue();
if (candidateMessage.fullProtoName().equals(metadataTypeName)) {
metadataMessage = candidateMessage;
}
}
if (metadataMessage == null
&& candidateMessage.fullProtoName().equals(metadataTypeFullName)) {
metadataMessage = candidateMessage;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,71 @@ public void writeClassDefinition_commentsStatementsAndMethods() {
assertEquals(expected, writerVisitor.write());
}

@Test
public void writeClassDefinition_withImportCollision() {

VaporReference firstType =
VaporReference.builder()
.setName("Service")
.setPakkage("com.google.api.generator.gapic.model")
.build();

VaporReference secondType =
VaporReference.builder().setName("Service").setPakkage("com.google.api").build();

Variable secondTypeVar =
Variable.builder()
.setName("anotherServiceVar")
.setType(TypeNode.withReference(secondType))
.build();

MethodInvocationExpr genericMethodInvocation =
MethodInvocationExpr.builder()
.setMethodName("barMethod")
.setStaticReferenceType(TypeNode.withReference(firstType))
.setGenerics(Arrays.asList(secondType))
.setArguments(VariableExpr.withVariable(secondTypeVar))
.setReturnType(TypeNode.STRING)
.build();

List<Statement> statements = Arrays.asList(ExprStatement.withExpr(genericMethodInvocation));

MethodDefinition methodOne =
MethodDefinition.builder()
.setName("doSomething")
.setScope(ScopeNode.PRIVATE)
.setBody(statements)
.setReturnType(TypeNode.VOID)
.build();

List<MethodDefinition> methods = Arrays.asList(methodOne);

ClassDefinition classDef =
ClassDefinition.builder()
.setPackageString("com.google.example")
.setName("FooService")
.setScope(ScopeNode.PUBLIC)
.setMethods(methods)
.build();

classDef.accept(writerVisitor);

String expected =
LineFormatter.lines(
"package com.google.example;\n"
+ "\n"
+ "import com.google.api.generator.gapic.model.Service;\n"
+ "\n"
+ "public class FooService {\n"
+ "\n"
+ " private void doSomething() {\n"
+ " Service.<com.google.api.Service>barMethod(anotherServiceVar);\n"
+ " }\n"
+ "}\n");

assertThat(writerVisitor.write()).isEqualTo(expected);
}

@Test
public void writeReferenceConstructorExpr_thisConstructorWithArguments() {
VaporReference ref =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,37 @@

package com.google.api.generator.gapic.protoparser;

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.engine.ast.Reference;
import com.google.api.generator.gapic.model.LongrunningOperation;
import com.google.api.generator.gapic.model.Message;
import com.google.cloud.location.LocationsProto;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.protobuf.Descriptors.MethodDescriptor;
import com.google.protobuf.Descriptors.ServiceDescriptor;
import com.google.showcase.v1beta1.EchoOuterClass;
import com.google.test.collisions.CollisionsOuterClass;
import com.google.testgapic.v1beta1.NestedMessageProto;
import java.util.Map;
import org.junit.Test;

public class TypeParserTest {
// TODO(miraleung): Backfill with more tests (e.g. field, message, methods) for Parser.java.

private static final FileDescriptor COLLISIONS_FILE_DESCRIPTOR =
CollisionsOuterClass.getDescriptor();
private static final FileDescriptor DESCRIPTOR_PROTOS_FILE_DESCRIPTOR =
DescriptorProtos.getDescriptor();
private static final FileDescriptor LOCATION_PROTO_FILE_DESCRIPTOR =
LocationsProto.getDescriptor();
private static final ServiceDescriptor COLLISIONS_SERVICE =
COLLISIONS_FILE_DESCRIPTOR.getServices().get(0);

@Test
public void parseMessageType_basic() {
FileDescriptor echoFileDescriptor = EchoOuterClass.getDescriptor();
Expand All @@ -51,4 +69,73 @@ public void parseMessageType_nested() {
Reference reference = TypeParser.parseMessageReference(messageDescriptor);
assertEquals("com.google.testgapic.v1beta1.Outer.Middle.Inner", reference.fullName());
}

@Test
public void parseLroResponseMetadataType_shortName_shouldMatchSamePackage() {
Map<String, Message> messageTypes = Parser.parseMessages(COLLISIONS_FILE_DESCRIPTOR);
messageTypes.putAll(Parser.parseMessages(DESCRIPTOR_PROTOS_FILE_DESCRIPTOR));
messageTypes.putAll(Parser.parseMessages(LOCATION_PROTO_FILE_DESCRIPTOR));
MethodDescriptor shouldUseSamePackageTypesLro = COLLISIONS_SERVICE.getMethods().get(0);

assertEquals(COLLISIONS_SERVICE.getName(), "Collisions");
assertThat(messageTypes)
.containsKey("com.google.protobuf.DescriptorProtos.GeneratedCodeInfo.Annotation");
assertThat(messageTypes)
.containsKey("com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location");
assertThat(messageTypes).containsKey("com.google.cloud.location.Location");

LongrunningOperation testLro =
Parser.parseLro(
TypeParser.getPackage(COLLISIONS_FILE_DESCRIPTOR),
shouldUseSamePackageTypesLro,
messageTypes);

assertThat(testLro.responseType().reference().fullName())
.isEqualTo("com.google.test.collisions.Annotation");
assertThat(testLro.metadataType().reference().fullName())
.isEqualTo("com.google.test.collisions.Location");
}

@Test
public void parseLroResponseMetadataType_shortName_shouldNotMatch() {
Map<String, Message> messageTypes = Parser.parseMessages(COLLISIONS_FILE_DESCRIPTOR);
messageTypes.putAll(Parser.parseMessages(DESCRIPTOR_PROTOS_FILE_DESCRIPTOR));
MethodDescriptor shortNameMatchShouldThrowLro = COLLISIONS_SERVICE.getMethods().get(1);

assertEquals(COLLISIONS_SERVICE.getName(), "Collisions");
assertThat(messageTypes)
.containsKey("com.google.protobuf.DescriptorProtos.ExtensionRangeOptions.Declaration");

assertThrows(
NullPointerException.class,
() ->
Parser.parseLro(
TypeParser.getPackage(COLLISIONS_FILE_DESCRIPTOR),
shortNameMatchShouldThrowLro,
messageTypes));
}

@Test
public void parseLroResponseMetadataType_shortName_withFullyQualifiedCollision() {
Map<String, Message> messageTypes = Parser.parseMessages(COLLISIONS_FILE_DESCRIPTOR);
messageTypes.putAll(Parser.parseMessages(DESCRIPTOR_PROTOS_FILE_DESCRIPTOR));
messageTypes.putAll(Parser.parseMessages(LOCATION_PROTO_FILE_DESCRIPTOR));
MethodDescriptor fullNameForDifferentPackageLro = COLLISIONS_SERVICE.getMethods().get(2);

assertEquals(COLLISIONS_SERVICE.getName(), "Collisions");
assertThat(messageTypes).containsKey("com.google.cloud.location.Location");
assertThat(messageTypes)
.containsKey("com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location");

LongrunningOperation testLro =
Parser.parseLro(
TypeParser.getPackage(COLLISIONS_FILE_DESCRIPTOR),
fullNameForDifferentPackageLro,
messageTypes);

assertThat(testLro.responseType().reference().fullName())
.isEqualTo("com.google.cloud.location.Location");
assertThat(testLro.metadataType().reference().fullName())
.isEqualTo("com.google.test.collisions.Location");
}
}
72 changes: 72 additions & 0 deletions gapic-generator-java/src/test/proto/collisions.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2023 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/client.proto";
import "google/longrunning/operations.proto";

package google.showcase.v1beta1;

option java_package = "com.google.test.collisions";
option java_multiple_files = true;

// This service exercises scenarios where short names of types
// exhibit ambiguity or collide with other types
service Collisions {

option (google.api.default_host) = "localhost:7469";

rpc shouldUseSamePackageTypesLro(Request) returns (google.longrunning.Operation) {
option (google.longrunning.operation_info) = {
// collision with google.protobuf.DescriptorProtos.GeneratedCodeInfo.Annotation
response_type: "Annotation"
// collision with google.cloud.location.Location;
metadata_type: "Location"
};
}

rpc shortNameMatchShouldThrowLro(Request) returns (google.longrunning.Operation) {
option (google.longrunning.operation_info) = {
// collision with google.protobuf.DescriptorProtos.GeneratedCodeInfo.Annotation
response_type: "Annotation"
// collision with google.protobuf.DescriptorProtos.ExtensionRangeOptions.Declaration
// not a valid short name specification (no such type exist in same package)
metadata_type: "Declaration"
};
}

rpc fullNameForDifferentPackageLro(Request) returns (google.longrunning.Operation) {
option (google.longrunning.operation_info) = {
// fully qualified name should match google.cloud.location.Location
response_type: "google.cloud.location.Location"
// short name only should match Location message defined below
metadata_type: "Location"
};
}
}

message Request {
string name = 1;
Annotation annotation = 2;
Location location = 3;
}

message Annotation {
string name = 1;
}

message Location {
string name = 1;
}