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

Add optional info for google.proto types #6153

Merged
merged 16 commits into from
Mar 13, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,13 @@ private static ServiceInfo buildHttpServiceInfo(String serviceName,
builder = FieldInfo.builder(paramName, toTypeSignature(parameter));
}

fieldInfosBuilder.add(builder.requirement(FieldRequirement.REQUIRED)
.location(fieldLocation)
if (parameter.isOptional() || parameter.isRepeated()) {
builder.requirement(FieldRequirement.OPTIONAL);
} else {
builder.requirement(FieldRequirement.REQUIRED);
}

fieldInfosBuilder.add(builder.location(fieldLocation)
.build());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,12 @@ public String toString() {
public static class Parameter {
private final JavaType type;
private final boolean isRepeated;
private final boolean isOptional;

public Parameter(JavaType type, boolean isRepeated) {
public Parameter(JavaType type, boolean isRepeated, boolean isOptional) {
this.type = requireNonNull(type, "type");
this.isRepeated = isRepeated;
this.isOptional = isOptional;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably we can do this.isOptional = isOptional || isRepeated; here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great. I've applied it 👍

}

public JavaType type() {
Expand All @@ -194,6 +196,10 @@ public boolean isRepeated() {
return isRepeated;
}

public boolean isOptional() {
return isOptional;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,13 +368,14 @@ private static Map<String, Field> buildFields(Descriptor desc,
case BYTE_STRING:
case ENUM:
// Use field name which is specified in proto file.
builder.put(key, new Field(field, parentNames, field.getJavaType()));
builder.put(key, new Field(field, parentNames, field.getJavaType(), false));
break;
case MESSAGE:
@Nullable
final JavaType wellKnownFieldType = getJavaTypeForWellKnownTypes(field);
final boolean isOptional = isOptionalFieldType(field);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we use field.isOptional(); here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

field.isOptional() only checks if the field has LABEL_OPTIONAL set, which is not sufficient to determine whether it’s a google.protobuf.StringValue or similar wrapper type. Since wrapper types in Protobuf do not explicitly have LABEL_OPTIONAL, we need a separate function to correctly identify them.

Let me know if I might be misunderstanding something 🙇

Copy link
Contributor

@minwoox minwoox Mar 11, 2025

Choose a reason for hiding this comment

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

I realized that all fields are optional in proto3:
https://protobuf.dev/programming-guides/proto3/#field-labels
What do you think of making the fields required only when [(google.api.field_behavior) = REQUIRED]; used instead?
https://google.aip.dev/203

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for providing the documentation references.
Since maintaining backward compatibility is important, using [(google.api.field_behavior) = REQUIRED] as the criteria for required fields makes sense.
I’ve updated the implementation accordingly 👍

if (wellKnownFieldType != null) {
builder.put(key, new Field(field, parentNames, wellKnownFieldType));
builder.put(key, new Field(field, parentNames, wellKnownFieldType, isOptional));
break;
}

Expand Down Expand Up @@ -408,7 +409,7 @@ private static Map<String, Field> buildFields(Descriptor desc,
throw e;
}

builder.put(key, new Field(field, parentNames, JavaType.MESSAGE));
builder.put(key, new Field(field, parentNames, JavaType.MESSAGE, isOptional));
}
break;
}
Expand All @@ -418,6 +419,22 @@ private static Map<String, Field> buildFields(Descriptor desc,
return builder.buildKeepingLast();
}

private static boolean isOptionalFieldType(FieldDescriptor field) {
Copy link
Contributor

Choose a reason for hiding this comment

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

probably isScalarValueWrapperMessage?
https://protobuf.dev/programming-guides/proto3/#scalar
Could you also call this method from the line 454?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That name is much better.
I’ve applied it to line 454 as well to remove duplicate code.
Thanks for the suggestion.

final String fullName = field.getFullName();
if (DoubleValue.getDescriptor().getFullName().equals(fullName) ||
FloatValue.getDescriptor().getFullName().equals(fullName) ||
Int64Value.getDescriptor().getFullName().equals(fullName) ||
UInt64Value.getDescriptor().getFullName().equals(fullName) ||
Int32Value.getDescriptor().getFullName().equals(fullName) ||
UInt32Value.getDescriptor().getFullName().equals(fullName) ||
BoolValue.getDescriptor().getFullName().equals(fullName) ||
StringValue.getDescriptor().getFullName().equals(fullName) ||
BytesValue.getDescriptor().getFullName().equals(fullName)) {
return true;
}
return false;
}

@Nullable
private static JavaType getJavaTypeForWellKnownTypes(FieldDescriptor fd) {
// MapField can be sent only via HTTP body.
Expand Down Expand Up @@ -606,7 +623,8 @@ public HttpEndpointSpecification httpEndpointSpecification(Route route) {
spec.originalFields.entrySet().stream().collect(
toImmutableMap(Entry::getKey,
fieldEntry -> new Parameter(fieldEntry.getValue().type(),
fieldEntry.getValue().isRepeated())));
fieldEntry.getValue().isRepeated(),
fieldEntry.getValue().isOptional())));
return new HttpEndpointSpecification(spec.order,
route,
paramNames,
Expand Down Expand Up @@ -981,11 +999,17 @@ static final class Field {
private final FieldDescriptor descriptor;
private final List<String> parentNames;
private final JavaType javaType;
private final boolean isOptional;

private Field(FieldDescriptor descriptor, List<String> parentNames, JavaType javaType) {
private Field(FieldDescriptor descriptor,
List<String> parentNames,
JavaType javaType,
boolean isOptional
) {
this.descriptor = descriptor;
this.parentNames = parentNames;
this.javaType = javaType;
this.isOptional = isOptional;
}

JavaType type() {
Expand All @@ -999,6 +1023,10 @@ String name() {
boolean isRepeated() {
return descriptor.isRepeated();
}

boolean isOptional() {
return isOptional;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ void httpEndpoint() {
.location(FieldLocation.PATH).requirement(FieldRequirement.REQUIRED).build(),
FieldInfo.builder("revision",
TypeSignature.ofList(TypeSignature.ofBase(JavaType.LONG.name())))
.location(FieldLocation.QUERY).requirement(FieldRequirement.REQUIRED).build()));
.location(FieldLocation.QUERY).requirement(FieldRequirement.OPTIONAL).build()));
assertThat(getMessageV3.useParameterAsRoot()).isFalse();

// Check HTTP PATCH method.
Expand Down
Loading