Skip to content

Commit b90d2c6

Browse files
yiyuabcclaude
andcommitted
Document null handling behavior in declareField to match REST
Add comprehensive javadoc explaining null handling behavior: 1. If value is null: - Parser and consumer NOT called (no-op) - Matches REST behavior when field not present in JSON 2. If value is not null: - Parser transforms the value - Consumer called with transformed value 3. If parser returns null: - Consumer STILL called with null - Setter is responsible for null validation (matches REST) This clarifies that the null-check behavior perfectly mirrors REST's ObjectParser pattern, where absent fields result in no parser/consumer invocation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cb8a4ed commit b90d2c6

File tree

9 files changed

+14
-690
lines changed

9 files changed

+14
-690
lines changed

modules/transport-grpc/min_max_aggregation_comparison_report.md

Lines changed: 0 additions & 642 deletions
This file was deleted.

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/common/ObjectParserProtoUtils.java

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,6 @@
1616
* <p>This class provides declarative field parsing methods that explicitly mirror REST's ObjectParser pattern,
1717
* making it easy to compare gRPC and REST implementations side-by-side.
1818
*
19-
* <p>Key principles:
20-
* <ul>
21-
* <li>Proto uses hasField() booleans instead of checking for field presence in JSON</li>
22-
* <li>Validation happens in the setter/consumer, just like REST (setters throw with aggregation name)</li>
23-
* <li>ObjectParserProtoUtils just dispatches - it doesn't validate</li>
24-
* </ul>
25-
*
26-
* <p>Example usage mirroring REST:
27-
* <pre>
28-
* // REST:
29-
* objectParser.declareField(
30-
* ValuesSourceAggregationBuilder::format,
31-
* XContentParser::text,
32-
* ParseField.CommonFields.FORMAT,
33-
* ObjectParser.ValueType.STRING
34-
* );
35-
*
36-
* // gRPC equivalent:
37-
* ObjectParserProtoUtils.declareField(
38-
* builder,
39-
* ValuesSourceAggregationBuilder::format, // Setter validates!
40-
* format, // null if not set
41-
* Function.identity(), // No transformation needed
42-
* "format"
43-
* );
44-
* </pre>
45-
*
4619
* @see org.opensearch.core.xcontent.ObjectParser
4720
*/
4821
public class ObjectParserProtoUtils {
@@ -61,6 +34,15 @@ private ObjectParserProtoUtils() {
6134
* extracted from proto, so the parser is used for transformation only. For simple fields that
6235
* need no transformation, use {@link Function#identity()}.
6336
*
37+
* <p><b>Null handling (matches REST):</b>
38+
* <ul>
39+
* <li>If value is null: parser and consumer are NOT called (no-op), matching REST behavior
40+
* when a field is not present in JSON</li>
41+
* <li>If value is not null: parser transforms it, then consumer is called with the result</li>
42+
* <li>If parser returns null: consumer is STILL called - the setter is responsible for
43+
* null validation, just like REST</li>
44+
* </ul>
45+
*
6446
* @param builder The builder to set the field on
6547
* @param consumer The consumer to set the field value (e.g., Builder::field, Builder::missing)
6648
* @param value The proto value (if null, the field is not set)
@@ -87,12 +69,15 @@ public static <T, P, V> void declareField(
8769
if (parser == null) {
8870
throw new IllegalArgumentException("[parser] is required");
8971
}
72+
// If value is null, do nothing - matches REST behavior when field not present in JSON
9073
if (value != null) {
9174
try {
9275
V transformedValue = parser.apply(value);
93-
consumer.accept(builder, transformedValue); // Setter validates
76+
// Note: Even if transformedValue is null, we still call consumer
77+
// The consumer is responsible for null validation, just like REST
78+
consumer.accept(builder, transformedValue);
9479
} catch (IllegalArgumentException e) {
95-
throw e; // Re-throw validation exceptions
80+
throw e;
9681
} catch (Exception e) {
9782
throw new IllegalArgumentException(
9883
"Failed to parse [" + fieldName + "]",

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/AggregationBuilderProtoConverterRegistryImpl.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,20 @@ public class AggregationBuilderProtoConverterRegistryImpl implements Aggregation
3030

3131
@Inject
3232
public AggregationBuilderProtoConverterRegistryImpl() {
33-
// Create the SPI registry which will hold all converters
3433
this.delegate = new AggregationBuilderProtoConverterSpiRegistry();
3534

36-
// Register built-in converters for this module
3735
registerBuiltInConverters();
3836
}
3937

4038
protected void registerBuiltInConverters() {
4139
// Register metric aggregation converters
42-
// These are simple aggregations that compute a single metric value
4340
delegate.registerConverter(new MinAggregationBuilderProtoConverter());
4441
delegate.registerConverter(new MaxAggregationBuilderProtoConverter());
4542

4643
// Future: Register bucket aggregation converters here
4744
// Example: delegate.registerConverter(new TermsAggregationBuilderProtoConverter());
4845

4946
// Set the registry on all converters so they can access each other
50-
// This is critical for bucket aggregations that need to parse nested aggregations
5147
delegate.setRegistryOnAllConverters(this);
5248

5349
logger.info("Registered {} built-in aggregation converter(s)", delegate.size());

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/AggregationBuilderProtoConverterSpiRegistry.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ public AggregationBuilder fromProto(String name, AggregationContainer container)
5151
throw new IllegalArgumentException("Aggregation name cannot be null or empty");
5252
}
5353

54-
// Use direct map lookup for better performance
5554
AggregationContainer.AggregationContainerCase aggregationCase = container.getAggregationContainerCase();
5655
AggregationBuilderProtoConverter converter = converterMap.get(aggregationCase);
5756

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/metrics/MaxAggregationBuilderProtoConverter.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
/**
1515
* Converter for Max aggregation. Delegates to {@link MaxAggregationProtoUtils}.
1616
* Mirrors REST parsing via {@link org.opensearch.search.aggregations.metrics.MaxAggregationBuilder#PARSER}.
17-
*
18-
* @see org.opensearch.search.aggregations.metrics.MaxAggregationBuilder
1917
*/
2018
public class MaxAggregationBuilderProtoConverter implements AggregationBuilderProtoConverter {
2119

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/metrics/MaxAggregationProtoUtils.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ private MaxAggregationProtoUtils() {
3737
* @param maxAggProto The Protocol Buffer MaxAggregation to convert
3838
* @return A configured MaxAggregationBuilder
3939
* @throws IllegalArgumentException if required fields are missing or validation fails
40-
* @see MaxAggregationBuilder#PARSER
4140
*/
4241
public static MaxAggregationBuilder fromProto(String name, MaxAggregation maxAggProto) {
4342
if (maxAggProto == null) {

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/metrics/MinAggregationBuilderProtoConverter.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
/**
1515
* Converter for Min aggregation. Delegates to {@link MinAggregationProtoUtils}.
1616
* Mirrors REST parsing via {@link org.opensearch.search.aggregations.metrics.MinAggregationBuilder#PARSER}.
17-
*
18-
* @see org.opensearch.search.aggregations.metrics.MinAggregationBuilder
1917
*/
2018
public class MinAggregationBuilderProtoConverter implements AggregationBuilderProtoConverter {
2119

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/metrics/MinAggregationProtoUtils.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ private MinAggregationProtoUtils() {
3737
* @param minAggProto The Protocol Buffer MinAggregation to convert
3838
* @return A configured MinAggregationBuilder
3939
* @throws IllegalArgumentException if required fields are missing or validation fails
40-
* @see MinAggregationBuilder#PARSER
4140
*/
4241
public static MinAggregationBuilder fromProto(String name, MinAggregation minAggProto) {
4342
if (minAggProto == null) {

modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/aggregation/support/ValuesSourceAggregationProtoUtils.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public static void declareFields(
6969
boolean timezoneAware,
7070
boolean fieldRequired
7171
) {
72-
// Field - mirrors: objectParser.declareField(Builder::field, XContentParser::text, FIELD, STRING)
7372
ObjectParserProtoUtils.declareField(
7473
builder,
7574
ValuesSourceAggregationBuilder::field,
@@ -78,7 +77,6 @@ public static void declareFields(
7877
ParseField.CommonFields.FIELD.getPreferredName()
7978
);
8079

81-
// Missing - mirrors: objectParser.declareField(Builder::missing, XContentParser::objectText, MISSING, VALUE)
8280
ObjectParserProtoUtils.declareField(
8381
builder,
8482
ValuesSourceAggregationBuilder::missing,
@@ -87,7 +85,6 @@ public static void declareFields(
8785
ParseField.CommonFields.MISSING.getPreferredName()
8886
);
8987

90-
// Value type - mirrors: objectParser.declareField(Builder::userValueTypeHint, parser, VALUE_TYPE, STRING)
9188
ObjectParserProtoUtils.declareField(
9289
builder,
9390
ValuesSourceAggregationBuilder::userValueTypeHint,
@@ -106,7 +103,6 @@ public static void declareFields(
106103
ValueType.VALUE_TYPE.getPreferredName()
107104
);
108105

109-
// Format - mirrors: objectParser.declareField(Builder::format, XContentParser::text, FORMAT, STRING)
110106
if (formattable) {
111107
ObjectParserProtoUtils.declareField(
112108
builder,
@@ -117,7 +113,6 @@ public static void declareFields(
117113
);
118114
}
119115

120-
// Script - mirrors: objectParser.declareField(Builder::script, Script::parse, SCRIPT, OBJECT_OR_STRING)
121116
if (scriptable) {
122117
ObjectParserProtoUtils.declareField(
123118
builder,
@@ -127,7 +122,6 @@ public static void declareFields(
127122
Script.SCRIPT_PARSE_FIELD.getPreferredName()
128123
);
129124

130-
// Field requirement validation - mirrors: objectParser.declareRequiredFieldSet(...)
131125
if (fieldRequired) {
132126
if (fields.getField() == null && fields.getScript() == null) {
133127
throw new IllegalArgumentException(
@@ -140,7 +134,6 @@ public static void declareFields(
140134
}
141135
}
142136
} else {
143-
// Non-scriptable aggregations
144137
if (fieldRequired) {
145138
if (fields.getField() == null) {
146139
throw new IllegalArgumentException(
@@ -150,7 +143,6 @@ public static void declareFields(
150143
}
151144
}
152145

153-
// Timezone - not yet supported
154146
if (timezoneAware) {
155147
throw new UnsupportedOperationException(
156148
"Timezone field is not yet supported in gRPC aggregations"

0 commit comments

Comments
 (0)