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: Use parent type instead of child_type in method doc sample #862

Merged
merged 8 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.google.api.generator.gapic.model.ResourceName;
import com.google.api.generator.gapic.utils.JavaStyle;
import com.google.api.generator.gapic.utils.ResourceNameConstants;
import com.google.api.generator.gapic.utils.ResourceReferenceUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.longrunning.Operation;
Expand All @@ -48,6 +49,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class DefaultValueComposer {
Expand Down Expand Up @@ -76,6 +78,7 @@ public static Expr createDefaultValue(
Expr defValue =
createDefaultValue(
resourceName,
methodArg.field().resourceReference().isChildType(),
resourceNames.values().stream().collect(Collectors.toList()),
methodArg.field().name());

Expand Down Expand Up @@ -175,17 +178,46 @@ static Expr createDefaultValue(Field f, boolean useExplicitInitTypeInGenerics) {
}

public static Expr createDefaultValue(
ResourceName resourceName, List<ResourceName> resnames, String fieldOrMessageName) {
return createDefaultValueResourceHelper(resourceName, resnames, fieldOrMessageName, true);
ResourceName resourceName,
boolean isChildType,
List<ResourceName> resnames,
String fieldOrMessageName) {
return createDefaultValueResourceHelper(
resourceName, isChildType, resnames, fieldOrMessageName, true);
}

private static Optional<ResourceName> findParentResource(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this whole thing be included in ResourceReferenceParser class? This seems like a general resource names resolution logic, not necessarily specific to sample generation.

ResourceName childResource, List<ResourceName> resourceNames) {
Map<String, ResourceName> patternToResourceName = new HashMap<>();

for (ResourceName resourceName : resourceNames) {
for (String parentPattern : resourceName.patterns()) {
patternToResourceName.put(parentPattern, resourceName);
}
}

for (String childPattern : childResource.patterns()) {
Optional<String> parentPattern = ResourceReferenceUtils.parseParentPattern(childPattern);
Copy link
Contributor

@vam-google vam-google Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Please see the other comment in ResourceReferenceUtils class for context

I could be missing some more complicated interactions, but at first glance:
ResourceReferenceUtils.parseParentPattern(childPattern) is called for every pattern in childResource which is of type ResourceName, created by ResourceReferenceParser. So instead of computing the parentPattern portion here, it may be precomputed and included in ResourceName class itself, which is constructed in ResourceReferenceParser, thus eliminating the need to have the utils class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, ResourceReferenceUtils.parseParentPattern(childPattern) is only called by MethodSigantureParser and by Parser, but only for Google Ads API. So, I don't see a straightforward way to include the mapping to the parent in the ResourceName itself, especially since the class is immutable, and you need to have access to all ResourceNames before computing the mapping from child to parent.

if (parentPattern.isPresent() && patternToResourceName.containsKey(parentPattern.get())) {
return Optional.of(patternToResourceName.get(parentPattern.get()));
}
}

return Optional.empty();
}

@VisibleForTesting
static Expr createDefaultValueResourceHelper(
ResourceName resourceName,
boolean isChildType,
List<ResourceName> resnames,
String fieldOrMessageName,
boolean allowAnonResourceNameClass) {

if (isChildType) {
resourceName = findParentResource(resourceName, resnames).orElse(resourceName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the orElse part mean that if we did not manage to find a parent, the code will behave as before, basically meaning that the child is its own parent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Do you think we should fail instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, just wanted to make sure I understood it right.

}

boolean hasOnePattern = resourceName.patterns().size() == 1;
if (resourceName.isOnlyWildcard()) {
List<ResourceName> unexaminedResnames = new ArrayList<>(resnames);
Expand All @@ -195,7 +227,7 @@ static Expr createDefaultValueResourceHelper(
continue;
}
unexaminedResnames.remove(resname);
return createDefaultValue(resname, unexaminedResnames, fieldOrMessageName);
return createDefaultValue(resname, false, unexaminedResnames, fieldOrMessageName);
}

if (unexaminedResnames.isEmpty()) {
Expand Down Expand Up @@ -283,6 +315,7 @@ public static Expr createSimpleMessageBuilderExpr(
defaultExpr =
createDefaultValueResourceHelper(
resourceNames.get(field.resourceReference().resourceTypeString()),
field.resourceReference().isChildType(),
resourceNames.values().stream().collect(Collectors.toList()),
message.name(),
/* allowAnonResourceNameClass = */ false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,7 @@ private static List<Expr> createRpcMethodArgumentDefaultValueExprs(
.setExprReferenceExpr(
DefaultValueComposer.createDefaultValue(
resourceNames.get(arg.field().resourceReference().resourceTypeString()),
arg.field().resourceReference().isChildType(),
resourceNameList,
arg.field().name()))
.setMethodName("toString")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,11 @@ static String parsePageSizeFieldName(
// with monolith-gnerated libraries.
String pagedFieldName = null;

if (inputMessage.fieldMap().containsKey("page_token")
if (inputMessage != null
&& inputMessage.fieldMap() != null
Copy link
Contributor

@vam-google vam-google Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this if statement get so much bigger? Specifically, looks like inputMessage.fieldMap() was guaranteed to not be null (as we would get NPE otherwise).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on fieldMap() being non-nullible. However, inputMessage can be null as the new messaging test revealed.

&& inputMessage.fieldMap().containsKey("page_token")
&& outputMessage != null
&& outputMessage.fieldMap() != null
&& outputMessage.fieldMap().containsKey("next_page_token")) {
// List of potential field names representing page size.
// page_size gets priority over max_results if both are present
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import com.google.api.generator.gapic.model.ResourceName;
import com.google.api.generator.gapic.model.ResourceReference;
import com.google.api.generator.gapic.utils.JavaStyle;
import com.google.api.generator.gapic.utils.ResourceNameConstants;
import com.google.api.generator.gapic.utils.ResourceReferenceUtils;
import com.google.api.pathtemplate.PathTemplate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
Expand Down Expand Up @@ -108,7 +108,7 @@ static Optional<ResourceName> parseParentResourceName(
String resourceTypeString,
@Nullable String description,
Map<String, ResourceName> patternsToResourceNames) {
Optional<String> parentPatternOpt = parseParentPattern(pattern);
Optional<String> parentPatternOpt = ResourceReferenceUtils.parseParentPattern(pattern);
if (!parentPatternOpt.isPresent()) {
return Optional.empty();
}
Expand Down Expand Up @@ -178,31 +178,6 @@ static Optional<ResourceName> parseParentResourceName(
return Optional.of(parentResourceName);
}

@VisibleForTesting
static Optional<String> parseParentPattern(String pattern) {
String[] tokens = pattern.split(SLASH);
String lastToken = tokens[tokens.length - 1];
if (lastToken.equals(ResourceNameConstants.DELETED_TOPIC_LITERAL)
|| lastToken.equals(ResourceNameConstants.WILDCARD_PATTERN)) {
return Optional.empty();
}

int lastTokenIndex = tokens.length - 2;
int minLengthWithParent = 4;
// Singleton patterns, e.g. projects/{project}/agent.
if (!lastToken.contains("{")) {
minLengthWithParent = 3;
lastTokenIndex = tokens.length - 1;
}

// No fully-formed parent. Expected: ancestors/{ancestor}/childNodes/{child_node}.
if (tokens.length < minLengthWithParent) {
return Optional.empty();
}

return Optional.of(String.join(SLASH, Arrays.asList(tokens).subList(0, lastTokenIndex)));
}

@VisibleForTesting
static String resolvePackages(String resourceNamePackage, String servicePackage) {
return resourceNamePackage.contains(servicePackage) ? resourceNamePackage : servicePackage;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2020 Google LLC
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2021

//
// 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.utils;

import java.util.Arrays;
import java.util.Optional;

public final class ResourceReferenceUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tl;dr; can we keep this in parser class?

It seems we can avoid creating a new util class for this:
It is one-method util class.
The only method is called parse* and we already have ResourceReferenceParser class, so it is hard to justify presence of ResourceReferenceUtils which does the parser's job.
I guess it is so because this new parse method is now used in two places - ResourceReferenceParser and DefaultValueComposer.

WDYT about keeping the parser method in parser class, but make the parent-child resolution logic persisted in ResourceName class (or another/new data structure returned by the parser class) and make the parser class precompute everything only once. If parent-child resolutin is not a property of a resource name but a property of an rpc method using that resource name, then it is not clear why that logic is still called in resource name parser class.


private static final String SLASH = "/";

private ResourceReferenceUtils() {}

public static Optional<String> parseParentPattern(String pattern) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic was there before, but it does not look accurate, as it makes many different assumption, which may be false.

for examle, if there is a pattern /a/{b}/c/e/{f}
when we search for its parent we must check at least 3 possible cases:

/a/{b}/c/e/{f}  - child
/a/{b}/c/e      - parent candidate 1
/a/{b}/c        - parent candidate 2 (if 1 did not match anything)
/a/{b}          - parent candidate 3 (if neither 1 nor 2 matched anything)

Instead this logic always returns only one candidate. Not sure how many (if any) practical cases we have which the current implementation would not satisfy. I guess it is ok to keep the implementaiton as is for now, but maybe put a comment/request for refactoring it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true, but according to https://google.aip.dev/122, this example is not a recommended pattern.

Resource name components should usually alternate between collection identifiers (example: publishers, books, users) and resource IDs (example: 123, les-miserables, vhugo1802).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, but if it was guaranteed they would phrase it as "must" in the AIP. I think it is ok to keep the implementation as is, since it is beyond the scope of this PR, just maybe add a comment about it.

String[] tokens = pattern.split(SLASH);
String lastToken = tokens[tokens.length - 1];
if (lastToken.equals(ResourceNameConstants.DELETED_TOPIC_LITERAL)
|| lastToken.equals(ResourceNameConstants.WILDCARD_PATTERN)) {
return Optional.empty();
}

int lastTokenIndex = tokens.length - 2;
int minLengthWithParent = 4;
// Singleton patterns, e.g. projects/{project}/agent.
if (!lastToken.contains("{")) {
minLengthWithParent = 3;
lastTokenIndex = tokens.length - 1;
}

// No fully-formed parent. Expected: ancestors/{ancestor}/childNodes/{child_node}.
if (tokens.length < minLengthWithParent) {
return Optional.empty();
}

return Optional.of(String.join(SLASH, Arrays.asList(tokens).subList(0, lastTokenIndex)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.google.pubsub.v1.PubsubProto;
import com.google.showcase.v1beta1.EchoOuterClass;
import com.google.showcase.v1beta1.IdentityOuterClass;
import com.google.showcase.v1beta1.MessagingOuterClass;
import com.google.showcase.v1beta1.TestingOuterClass;
import com.google.testdata.v1.DeprecatedServiceOuterClass;
import google.cloud.CommonResources;
Expand All @@ -51,6 +52,7 @@
import java.util.Set;

public class TestProtoLoader {

private static final TestProtoLoader INSTANCE =
new TestProtoLoader(Transport.GRPC, "src/test/java/com/google/api/generator/gapic/testdata/");
private final String testFilesDirectory;
Expand Down Expand Up @@ -170,6 +172,35 @@ public GapicContext parseShowcaseIdentity() {
.build();
}

public GapicContext parseShowcaseMessaging() {
FileDescriptor fileDescriptor = MessagingOuterClass.getDescriptor();
ServiceDescriptor messagingService = fileDescriptor.getServices().get(0);
assertEquals(messagingService.getName(), "Messaging");

Map<String, Message> messageTypes = Parser.parseMessages(fileDescriptor);
Map<String, ResourceName> resourceNames = Parser.parseResourceNames(fileDescriptor);

FileDescriptor identityFileDescriptor = IdentityOuterClass.getDescriptor();
Map<String, ResourceName> identityResourceNames = Parser
.parseResourceNames(identityFileDescriptor);

resourceNames.put("showcase.googleapis.com/User",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? If there is stuff in the messaging.proto file, which just causes troubles but is not essential for the tests, please feel free to remove it. If there are missing imports please just add them on proot/bazel lavel.
Basically any API definition problems are expected to be solved on proto level, but not in the logic which loads/parse the protos.

identityResourceNames.get("showcase.googleapis.com/User"));

Set<ResourceName> outputResourceNames = new HashSet<>();
List<Service> services =
Parser.parseService(
fileDescriptor, messageTypes, resourceNames, Optional.empty(), outputResourceNames);

return GapicContext.builder()
.setMessages(messageTypes)
.setResourceNames(resourceNames)
.setServices(services)
.setHelperResourceNames(outputResourceNames)
.setTransport(transport)
.build();
}

public GapicContext parseShowcaseTesting() {
FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor();
ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ public void defaultValue_resourceNameWithOnePattern() {
Expr expr =
DefaultValueComposer.createDefaultValue(
resourceName,
false,
typeStringsToResourceNames.values().stream().collect(Collectors.toList()),
"ignored");
expr.accept(writerVisitor);
Expand All @@ -177,6 +178,7 @@ public void defaultValue_resourceNameWithMultiplePatterns() {
Expr expr =
DefaultValueComposer.createDefaultValue(
resourceName,
false,
typeStringsToResourceNames.values().stream().collect(Collectors.toList()),
"ignored");
expr.accept(writerVisitor);
Expand All @@ -194,6 +196,7 @@ public void defaultValue_resourceNameWithWildcardPattern() {
Expr expr =
DefaultValueComposer.createDefaultValue(
resourceName,
false,
typeStringsToResourceNames.values().stream().collect(Collectors.toList()),
"ignored");
expr.accept(writerVisitor);
Expand All @@ -216,6 +219,7 @@ public void defaultValue_wildcardResourceNameWithOnlyDeletedTopic() {
Expr expr =
DefaultValueComposer.createDefaultValue(
resourceName,
false,
typeStringsToResourceNames.values().stream().collect(Collectors.toList()),
"ignored");
expr.accept(writerVisitor);
Expand All @@ -236,6 +240,7 @@ public void defaultValue_resourceNameWithOnlyWildcards_valueOnly() {
Expr expr =
DefaultValueComposer.createDefaultValueResourceHelper(
resourceName,
false,
Collections.emptyList(),
fallbackField,
/* allowAnonResourceNameClass = */ false);
Expand All @@ -257,7 +262,7 @@ public void defaultValue_resourceNameWithOnlyWildcards_allowAnonResourceNameClas
String fallbackField = "foobar";
Expr expr =
DefaultValueComposer.createDefaultValue(
resourceName, Collections.emptyList(), fallbackField);
resourceName, false, Collections.emptyList(), fallbackField);
expr.accept(writerVisitor);
String expected =
LineFormatter.lines(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,17 @@ public void generateServiceClasses_bookshopNameConflicts() {
Path goldenFilePath = Paths.get(Utils.getGoldenDir(this.getClass()), "BookshopClient.golden");
assertCodeEquals(goldenFilePath, visitor.write());
}

@Test
public void generateServiceClasses_childTypeParentInJavadoc() {
GapicContext context = GrpcTestProtoLoader.instance().parseShowcaseMessaging();
Service protoService = context.services().get(0);
GapicClass clazz = ServiceClientClassComposer.instance().generate(context, protoService);

JavaWriterVisitor visitor = new JavaWriterVisitor();
clazz.classDefinition().accept(visitor);
Utils.saveCodegenToFile(this.getClass(), "MessagingClient.golden", visitor.write());
Path goldenFilePath = Paths.get(Utils.getGoldenDir(this.getClass()), "MessagingClient.golden");
assertCodeEquals(goldenFilePath, visitor.write());
}
}
Loading