-
Notifications
You must be signed in to change notification settings - Fork 107
fix(httpjson): handle message derived query params #1784
Changes from 4 commits
e7a9caa
a61065b
63e0118
16689a9
2edd120
08b655b
46b8bd7
471a134
f7443b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,16 +30,23 @@ | |
package com.google.api.gax.httpjson; | ||
|
||
import com.google.api.core.BetaApi; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.protobuf.ByteString; | ||
import com.google.protobuf.Descriptors.FieldDescriptor; | ||
import com.google.protobuf.GeneratedMessageV3; | ||
import com.google.protobuf.InvalidProtocolBufferException; | ||
import com.google.protobuf.Message; | ||
import com.google.protobuf.MessageOrBuilder; | ||
import com.google.protobuf.TypeRegistry; | ||
import com.google.protobuf.util.JsonFormat; | ||
import com.google.protobuf.util.JsonFormat.Printer; | ||
import java.io.IOException; | ||
import java.io.Reader; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
/** | ||
* This class serializes/deserializes protobuf {@link Message} for REST interactions. It serializes | ||
|
@@ -51,6 +58,29 @@ | |
public class ProtoRestSerializer<RequestT extends Message> { | ||
private final TypeRegistry registry; | ||
|
||
// well-known types obtained from | ||
// https://github.com/googleapis/gapic-showcase/blob/fe414784c18878d704b884348d84c68fd6b87466/util/genrest/resttools/populatefield.go#L27 | ||
private static final Set<Class<GeneratedMessageV3>> jsonSerializableMessages = | ||
new HashSet( | ||
Arrays.asList( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Effective Java 26: Don't use raw types. To do that and make this immutable at the same time, consider: private static final Set<Class<? extends GeneratedMessageV3>> JSON_SERIALIZABLE_MESSAGES =
ImmutableSet.of(
BoolValue.class, Note that the collection could be considered a constant since it is now immutable, thus now worthy of upper snake case. |
||
com.google.protobuf.BoolValue.class, | ||
com.google.protobuf.BytesValue.class, | ||
com.google.protobuf.DoubleValue.class, | ||
com.google.protobuf.Duration.class, | ||
com.google.protobuf.FieldMask.class, | ||
com.google.protobuf.FloatValue.class, | ||
com.google.protobuf.Int32Value.class, | ||
com.google.protobuf.Int64Value.class, | ||
com.google.protobuf.StringValue.class, | ||
com.google.protobuf.Timestamp.class, | ||
com.google.protobuf.UInt32Value.class, | ||
com.google.protobuf.UInt64Value.class)); | ||
|
||
private boolean isNonSerializableMessageValue(Object value) { | ||
return value instanceof GeneratedMessageV3 | ||
&& !jsonSerializableMessages.contains(value.getClass()); | ||
} | ||
|
||
private ProtoRestSerializer(TypeRegistry registry) { | ||
this.registry = registry; | ||
} | ||
|
@@ -75,7 +105,7 @@ static <RequestT extends Message> ProtoRestSerializer<RequestT> create(TypeRegis | |
* @throws InvalidProtocolBufferException if failed to serialize the protobuf message to JSON | ||
* format | ||
*/ | ||
String toJson(RequestT message, boolean numericEnum) { | ||
String toJson(MessageOrBuilder message, boolean numericEnum) { | ||
try { | ||
Printer printer = JsonFormat.printer().usingTypeRegistry(registry); | ||
if (numericEnum) { | ||
|
@@ -118,6 +148,18 @@ public void putPathParam(Map<String, String> fields, String fieldName, Object fi | |
fields.put(fieldName, String.valueOf(fieldValue)); | ||
} | ||
|
||
private void putDecomposedMessageQueryParam( | ||
Map<String, List<String>> fields, String fieldName, Object fieldValue) { | ||
for (Map.Entry<FieldDescriptor, Object> fieldEntry : | ||
((GeneratedMessageV3) fieldValue).getAllFields().entrySet()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cast is only safe due to an instanceof check that occurs elsewhere in the logic. Will the next maintainer know they must call I would highly suggest either performing the check explicitly in this method, or changing the parameter from In general, the type-check ( |
||
Object value = fieldEntry.getValue(); | ||
putQueryParam( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This recursive logic seems complicated and could run into StackOverFlow issue if not careful, also doing all this during runtime could affect the performance for client libraries as well. So now I really think that we should do this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good |
||
fields, | ||
String.format("%s.%s", fieldName, fieldEntry.getKey().toProto().getName()), | ||
fieldEntry.getValue()); | ||
} | ||
} | ||
|
||
/** | ||
* Puts a message field in {@code fields} map which will be used to populate query parameters of a | ||
* request. | ||
|
@@ -127,16 +169,34 @@ public void putPathParam(Map<String, String> fields, String fieldName, Object fi | |
* @param fieldValue a field value | ||
*/ | ||
public void putQueryParam(Map<String, List<String>> fields, String fieldName, Object fieldValue) { | ||
ImmutableList.Builder<String> paramValueList = ImmutableList.builder(); | ||
ArrayList<String> paramValueList = new ArrayList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
if (fieldValue instanceof List<?>) { | ||
boolean hasProcessedMessage = false; | ||
for (Object fieldValueItem : (List<?>) fieldValue) { | ||
paramValueList.add(String.valueOf(fieldValueItem)); | ||
if (isNonSerializableMessageValue(fieldValueItem)) { | ||
putDecomposedMessageQueryParam(fields, fieldName, fieldValueItem); | ||
hasProcessedMessage = true; | ||
} else { | ||
paramValueList.add(toQueryParamValue(fieldValueItem)); | ||
} | ||
} | ||
if (hasProcessedMessage) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than introducing this variable and a secondary exit point for the function, consider instead just performing a |
||
return; | ||
} | ||
} else { | ||
paramValueList.add(String.valueOf(fieldValue)); | ||
if (isNonSerializableMessageValue(fieldValue)) { | ||
putDecomposedMessageQueryParam(fields, fieldName, fieldValue); | ||
return; | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When working with the pattern:
We can often refactor to: Iterable<?> fieldValues =
fieldValue instanceof Iterable
? (Iterable<?>) fieldValue
: Collections.singleton(fieldValue);
foreach i in x
// do A(i) This eliminates the need to study both implementations of A to determine if they're the same or not, with the added benefit of it being easier to test the single code path rather than both. As the complexity of A increases, this simplification becomes more and more beneficial. |
||
paramValueList.add(toQueryParamValue(fieldValue)); | ||
} | ||
} | ||
|
||
fields.put(fieldName, paramValueList.build()); | ||
if (fields.containsKey(fieldName)) { | ||
fields.get(fieldName).addAll(paramValueList); | ||
} else { | ||
fields.put(fieldName, paramValueList); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -159,4 +219,23 @@ public String toBody(String fieldName, RequestT fieldValue) { | |
public String toBody(String fieldName, RequestT fieldValue, boolean numericEnum) { | ||
return toJson(fieldValue, numericEnum); | ||
} | ||
|
||
/** | ||
* Serializes an object to a query parameter Handles the case of a message such as Duration, | ||
* FieldMask or Int32Value to prevent wrong formatting that String.valueOf() would make | ||
* | ||
* @param fieldValue a field value to serialize | ||
*/ | ||
public String toQueryParamValue(Object fieldValue) { | ||
// This will match with message types that are serializable (e.g. FieldMask) | ||
if (fieldValue instanceof GeneratedMessageV3 && !isNonSerializableMessageValue(fieldValue)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just use |
||
return toJson(((GeneratedMessageV3) fieldValue).toBuilder(), false) | ||
.replaceAll("^\"", "") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks suspicious to me, any documentation mentioned that we need to do this manually? What would it looks like if we don't replace them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this to get the actual contents of a string value. For example, a FieldMask would serialize into |
||
.replaceAll("\"$", ""); | ||
} | ||
if (fieldValue instanceof ByteString) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we need to handle ByteString separately here? |
||
return ((ByteString) fieldValue).toStringUtf8(); | ||
} | ||
return String.valueOf(fieldValue); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I proposed the list from gapic-showcase but I don't think we should just use it, showcase is only used for testing so the list may get out of date. We should probably go through the protobuf doc and add any type that have special handling to the list. For now, since Go already implemented it, we can probably follow what they did here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For sure, I can update the list of well-known types following the docs list, but I'm curious if there is a better way to programatically obtain a list of well-known types so we can update it as a dependency or so.
We have this documentation entry as a source of well-known types but we may have to manually check from time to time to keep our hardcoded list up to date.
However, I noted that the official definitions such as duration.proto would specify a special dotnet package
Google.Protobuf.WellKnownTypes
, whereas in java it falls into the commoncom.google.protobuf
package, leaving us with no reliable programatic way to obtain a list of well-known types that would get updated as a dependency. As a last (and bad) source, there is a unit test in the same folder of the duration definition that contains a list of well-known types, but it is a unit test, so I don't think we have too many options