Skip to content

Add ObjectParser.declareNamedObject (singular) method #53017

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

Merged
merged 2 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -47,6 +47,31 @@ public abstract class AbstractObjectParser<Value, Context>
public abstract <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, ParseField parseField,
ValueType type);

/**
* Declares a single named object.
*
* <pre>
* <code>
* {
* "object_name": {
* "instance_name": { "field1": "value1", ... }
* }
* }
* }
* </code>
* </pre>
*
* @param consumer
* sets the value once it has been parsed
* @param namedObjectParser
* parses the named object
* @param parseField
* the field to parse
*/
public abstract <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
ParseField parseField);


/**
* Declares named objects in the style of aggregations. These are named
* inside and object like this:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,26 +206,55 @@ public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Contex
throw new IllegalArgumentException("[type] is required");
}

if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
if (isConstructorArg(consumer)) {
/*
* Constructor arguments are detected by these "marker" consumers. It keeps the API looking clean even if it is a bit sleezy. We
* then build a new consumer directly against the object parser that triggers the "constructor arg just arrived behavior" of the
* parser. Conveniently, we can close over the position of the constructor in the argument list so we don't need to do any fancy
* Build a new consumer directly against the object parser that
* triggers the "constructor arg just arrived behavior" of the
* parser. Conveniently, we can close over the position of the
* constructor in the argument list so we don't need to do any fancy
* or expensive lookups whenever the constructor args come in.
*/
int position = constructorArgInfos.size();
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
int position = addConstructorArg(consumer, parseField);
objectParser.declareField((target, v) -> target.constructorArg(position, v), parser, parseField, type);
} else {
numberOfFields += 1;
objectParser.declareField(queueingConsumer(consumer, parseField), parser, parseField, type);
}
}

@Override
public <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
ParseField parseField) {
if (consumer == null) {
throw new IllegalArgumentException("[consumer] is required");
}
if (namedObjectParser == null) {
throw new IllegalArgumentException("[parser] is required");
}
if (parseField == null) {
throw new IllegalArgumentException("[parseField] is required");
}

if (isConstructorArg(consumer)) {
/*
* Build a new consumer directly against the object parser that
* triggers the "constructor arg just arrived behavior" of the
* parser. Conveniently, we can close over the position of the
* constructor in the argument list so we don't need to do any fancy
* or expensive lookups whenever the constructor args come in.
*/
int position = addConstructorArg(consumer, parseField);
objectParser.declareNamedObject((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
} else {
numberOfFields += 1;
objectParser.declareNamedObject(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
}
}

@Override
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
ParseField parseField) {

if (consumer == null) {
throw new IllegalArgumentException("[consumer] is required");
}
Expand All @@ -236,19 +265,15 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
throw new IllegalArgumentException("[parseField] is required");
}

if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
if (isConstructorArg(consumer)) {
/*
* Constructor arguments are detected by this "marker" consumer. It
* keeps the API looking clean even if it is a bit sleezy. We then
* build a new consumer directly against the object parser that
* Build a new consumer directly against the object parser that
* triggers the "constructor arg just arrived behavior" of the
* parser. Conveniently, we can close over the position of the
* constructor in the argument list so we don't need to do any fancy
* or expensive lookups whenever the constructor args come in.
*/
int position = constructorArgInfos.size();
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
int position = addConstructorArg(consumer, parseField);
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
} else {
numberOfFields += 1;
Expand All @@ -272,19 +297,15 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
throw new IllegalArgumentException("[parseField] is required");
}

if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
if (isConstructorArg(consumer)) {
/*
* Constructor arguments are detected by this "marker" consumer. It
* keeps the API looking clean even if it is a bit sleezy. We then
* build a new consumer directly against the object parser that
* Build a new consumer directly against the object parser that
* triggers the "constructor arg just arrived behavior" of the
* parser. Conveniently, we can close over the position of the
* constructor in the argument list so we don't need to do any fancy
* or expensive lookups whenever the constructor args come in.
*/
int position = constructorArgInfos.size();
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
int position = addConstructorArg(consumer, parseField);
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser,
wrapOrderedModeCallBack(orderedModeCallback), parseField);
} else {
Expand All @@ -294,6 +315,27 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
}
}

/**
* Constructor arguments are detected by this "marker" consumer. It
* keeps the API looking clean even if it is a bit sleezy.
*/
private boolean isConstructorArg(BiConsumer<?, ?> consumer) {
return consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER;
}

/**
* Add a constructor argument
* @param consumer Either {@link #REQUIRED_CONSTRUCTOR_ARG_MARKER} or {@link #REQUIRED_CONSTRUCTOR_ARG_MARKER}
* @param parseField Parse field
* @return The argument position
*/
private int addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
int position = constructorArgInfos.size();
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
return position;
}

@Override
public String getName() {
return objectParser.getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,28 @@ public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction
}, field, ValueType.OBJECT_OR_BOOLEAN);
}

@Override
public <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
ParseField field) {

BiFunction<XContentParser, Context, T> objectParser = (XContentParser p, Context c) -> {
try {
XContentParser.Token token = p.nextToken();
assert token == XContentParser.Token.FIELD_NAME;
String name = p.currentName();
try {
return namedObjectParser.parse(p, c, name);
} catch (Exception e) {
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
}
} catch (IOException e) {
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] error while parsing named object", e);
}
};

declareField((XContentParser p, Value v, Context c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT);
}

@Override
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
Consumer<Value> orderedModeCallback, ParseField field) {
Expand All @@ -403,7 +425,7 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
+ "fields or an array where each entry is an object with a single field");
}
// This messy exception nesting has the nice side effect of telling the use which field failed to parse
// This messy exception nesting has the nice side effect of telling the user which field failed to parse
Copy link
Member

Choose a reason for hiding this comment

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

❤️

try {
String name = p.currentName();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,55 +500,68 @@ public void setString_or_null(String string_or_null) {
}

public void testParseNamedObject() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {\"foo\" : 11} }}");
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
assertEquals("a", h.named.name);
assertEquals(11, h.named.foo);
}

public void testParseNamedObjectUnexpectedArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"a\": {\"foo\" : 11} }]");
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_object_holder] named doesn't support values of type: START_ARRAY"));
}

public void testParseNamedObjects() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
assertThat(h.named, hasSize(1));
assertEquals("a", h.named.get(0).name);
assertFalse(h.namedSuppliedInOrder);
}

public void testParseNamedObjectInOrder() throws IOException {
public void testParseNamedObjectsInOrder() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
assertThat(h.named, hasSize(1));
assertEquals("a", h.named.get(0).name);
assertTrue(h.namedSuppliedInOrder);
}

public void testParseNamedObjectTwoFieldsInArray() throws IOException {
public void testParseNamedObjectsTwoFieldsInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}, \"b\": {}}]}");
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
assertThat(e.getCause().getMessage(),
containsString("[named] can be a single object with any number of fields " +
"or an array where each entry is an object with a single field"));
}

public void testParseNamedObjectNoFieldsInArray() throws IOException {
public void testParseNamedObjectsNoFieldsInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {} ]}");
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
assertThat(e.getCause().getMessage(),
containsString("[named] can be a single object with any number of fields " +
"or an array where each entry is an object with a single field"));
}

public void testParseNamedObjectJunkInArray() throws IOException {
public void testParseNamedObjectsJunkInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"junk\" ] }");
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
assertThat(e.getCause().getMessage(),
containsString("[named] can be a single object with any number of fields " +
"or an array where each entry is an object with a single field"));
}

public void testParseNamedObjectInOrderNotSupported() throws IOException {
public void testParseNamedObjectsInOrderNotSupported() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");

// Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above
ObjectParser<NamedObjectHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
NamedObjectHolder::new);
objectParser.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
ObjectParser<NamedObjectsHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
NamedObjectsHolder::new);
objectParser.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, new ParseField("named"));

// Now firing the xml through it fails
XContentParseException e = expectThrows(XContentParseException.class, () -> objectParser.apply(parser, null));
Expand Down Expand Up @@ -728,11 +741,27 @@ public void testNoopDeclareObjectArray() throws IOException {
assertEquals("expected value but got [FIELD_NAME]", sneakyError.getCause().getMessage());
}

// singular
static class NamedObjectHolder {
public static final ObjectParser<NamedObjectHolder, Void> PARSER = new ObjectParser<>("named_object_holder",
NamedObjectHolder::new);
static {
PARSER.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, NamedObjectHolder::keepNamedInOrder,
PARSER.declareNamedObject(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
}

private NamedObject named;

public void setNamed(NamedObject named) {
this.named = named;
}
}

// plural
static class NamedObjectsHolder {
public static final ObjectParser<NamedObjectsHolder, Void> PARSER = new ObjectParser<>("named_objects_holder",
NamedObjectsHolder::new);
static {
PARSER.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, NamedObjectsHolder::keepNamedInOrder,
new ParseField("named"));
}

Expand Down