Support optional ctor args in ConstructingObjectParser

You declare them like
```
static {
  PARSER.declareInt(optionalConstructorArg(), new ParseField("animal"));
}
```

Other than being optional they follow all of the rules of regular
`constructorArg()`s. Parsing an object with optional constructor args
is going to be slightly less efficient than parsing an object with
all required args if some of the optional args aren't specified because
ConstructingObjectParser isn't able to build the target before the
end of the json object.
This commit is contained in:
Nik Everett 2016-06-03 09:15:10 -04:00
parent bec26015b2
commit 5161afe5e3
2 changed files with 206 additions and 87 deletions

View File

@ -26,7 +26,6 @@ import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
@ -53,12 +52,13 @@ import java.util.function.Function;
* </p>
* <pre>{@code
* private static final ConstructingObjectParser<Thing, SomeContext> PARSER = new ConstructingObjectParser<>("thing",
* a -> new Thing((String) a[0], (String) a[1]));
* a -> new Thing((String) a[0], (String) a[1], (Integer) a[2]));
* static {
* PARSER.declareString(constructorArg(), new ParseField("animal"));
* PARSER.declareString(constructorArg(), new ParseField("vegetable"));
* PARSER.declareInt(Thing::setMineral, new ParseField("mineral"));
* PARSER.declareInt(optionalConstructorArg(), new ParseField("mineral"));
* PARSER.declareInt(Thing::setFruit, new ParseField("fruit"));
* PARSER.declareInt(Thing::setBug, new ParseField("bug"));
* }
* }</pre>
* <p>
@ -70,19 +70,29 @@ import java.util.function.Function;
* it allocates <code>3 + 2 * param_count</code> objects per parse. If this overhead is too much for you then feel free to have ObjectParser
* parse a secondary object and have that one call the target object's constructor. That ought to be rare though.
* </p>
* <p>
* Note: if optional constructor arguments aren't specified then the number of allocations is always the worst case.
* </p>
*/
public final class ConstructingObjectParser<Value, Context extends ParseFieldMatcherSupplier> extends AbstractObjectParser<Value, Context> {
/**
* Consumer that marks a field as a constructor argument instead of a real object field.
* Consumer that marks a field as a required constructor argument instead of a real object field.
*/
private static final BiConsumer<Object, Object> CONSTRUCTOR_ARG_MARKER = (a, b) -> {
private static final BiConsumer<Object, Object> REQUIRED_CONSTRUCTOR_ARG_MARKER = (a, b) -> {
throw new UnsupportedOperationException("I am just a marker I should never be called.");
};
/**
* Consumer that marks a field as an optional constructor argument instead of a real object field.
*/
private static final BiConsumer<Object, Object> OPTIONAL_CONSTRUCTOR_ARG_MARKER = (a, b) -> {
throw new UnsupportedOperationException("I am just a marker I should never be called.");
};
/**
* List of constructor names used for generating the error message if not all arrive.
*/
private final List<ParseField> constructorArgNames = new ArrayList<>();
private final List<ConstructorArgInfo> constructorArgInfos = new ArrayList<>();
private final ObjectParser<Target, Context> objectParser;
private final Function<Object[], Value> builder;
/**
@ -120,27 +130,39 @@ public final class ConstructingObjectParser<Value, Context extends ParseFieldMat
}
/**
* Pass the {@linkplain BiConsumer} this returns the declare methods to declare a constructor argument. See this class's javadoc for an
* example. The order in which these are declared matters: it is the order that they come in the array passed to {@link #builder} and
* the order that missing arguments are reported to the user if any are missing. When all of these parameters are parsed from the
* {@linkplain XContentParser} the target object is immediately built.
* Pass the {@linkplain BiConsumer} this returns the declare methods to declare a required constructor argument. See this class's
* javadoc for an example. The order in which these are declared matters: it is the order that they come in the array passed to
* {@link #builder} and the order that missing arguments are reported to the user if any are missing. When all of these parameters are
* parsed from the {@linkplain XContentParser} the target object is immediately built.
*/
@SuppressWarnings("unchecked") // Safe because we never call the method. This is just trickery to make the interface pretty.
public static <Value, FieldT> BiConsumer<Value, FieldT> constructorArg() {
return (BiConsumer<Value, FieldT>) CONSTRUCTOR_ARG_MARKER;
return (BiConsumer<Value, FieldT>) REQUIRED_CONSTRUCTOR_ARG_MARKER;
}
/**
* Pass the {@linkplain BiConsumer} this returns the declare methods to declare an optional constructor argument. See this class's
* javadoc for an example. The order in which these are declared matters: it is the order that they come in the array passed to
* {@link #builder} and the order that missing arguments are reported to the user if any are missing. When all of these parameters are
* parsed from the {@linkplain XContentParser} the target object is immediately built.
*/
@SuppressWarnings("unchecked") // Safe because we never call the method. This is just trickery to make the interface pretty.
public static <Value, FieldT> BiConsumer<Value, FieldT> optionalConstructorArg() {
return (BiConsumer<Value, FieldT>) OPTIONAL_CONSTRUCTOR_ARG_MARKER;
}
@Override
public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, ParseField parseField, ValueType type) {
if (consumer == CONSTRUCTOR_ARG_MARKER) {
if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
/*
* 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 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 = constructorArgNames.size();
constructorArgNames.add(parseField);
int position = constructorArgInfos.size();
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
objectParser.declareField((target, v) -> target.constructorArg(position, parseField, v), parser, parseField, type);
} else {
numberOfFields += 1;
@ -186,7 +208,7 @@ public final class ConstructingObjectParser<Value, Context extends ParseFieldMat
/**
* Array of constructor args to be passed to the {@link ConstructingObjectParser#builder}.
*/
private final Object[] constructorArgs = new Object[constructorArgNames.size()];
private final Object[] constructorArgs = new Object[constructorArgInfos.size()];
/**
* The parser this class is working against. We store it here so we can fetch it conveniently when queueing fields to lookup the
* location of each field so that we can give a useful error message when replaying the queue.
@ -224,20 +246,8 @@ public final class ConstructingObjectParser<Value, Context extends ParseFieldMat
}
constructorArgs[position] = value;
constructorArgsCollected++;
if (constructorArgsCollected != constructorArgNames.size()) {
return;
}
try {
targetObject = builder.apply(constructorArgs);
while (queuedFieldsCount > 0) {
queuedFieldsCount -= 1;
queuedFields[queuedFieldsCount].accept(targetObject);
}
} catch (ParsingException e) {
throw new ParsingException(e.getLineNumber(), e.getColumnNumber(),
"failed to build [" + objectParser.getName() + "] after last required field arrived", e);
} catch (Exception e) {
throw new ParsingException(null, "Failed to build [" + objectParser.getName() + "] after last required field arrived", e);
if (constructorArgsCollected == constructorArgInfos.size()) {
buildTarget();
}
}
@ -263,36 +273,62 @@ public final class ConstructingObjectParser<Value, Context extends ParseFieldMat
if (targetObject != null) {
return targetObject;
}
// The object hasn't been built which ought to mean we're missing some constructor arguments.
/*
* The object hasn't been built which ought to mean we're missing some constructor arguments. But they could be optional! We'll
* check if they are all optional and build the error message at the same time - if we don't start the error message then they
* were all optional!
*/
StringBuilder message = null;
for (int i = 0; i < constructorArgs.length; i++) {
if (constructorArgs[i] == null) {
ParseField arg = constructorArgNames.get(i);
if (message == null) {
message = new StringBuilder("Required [").append(arg);
} else {
message.append(", ").append(arg);
}
if (constructorArgs[i] != null) continue;
ConstructorArgInfo arg = constructorArgInfos.get(i);
if (false == arg.required) continue;
if (message == null) {
message = new StringBuilder("Required [").append(arg.field);
} else {
message.append(", ").append(arg.field);
}
}
if (message != null) {
// There were non-optional constructor arguments missing.
throw new IllegalArgumentException(message.append(']').toString());
}
/*
* There won't be if there weren't any constructor arguments declared. That is fine, we'll just throw that error back at the to
* the user. This will happen every time so we can be confident that this'll be caught in testing so we can talk to the user
* like they are a developer. The only time a user will see this is if someone writes a parser and never tests it which seems
* like a bad idea.
* If there weren't any constructor arguments declared at all then we won't get an error message but this isn't really a valid
* use of ConstructingObjectParser. You should be using ObjectParser instead. Since this is more of a programmer error and the
* parser ought to still work we just assert this.
*/
if (constructorArgNames.isEmpty()) {
throw new IllegalStateException("[" + objectParser.getName() + "] must configure at least on constructor argument. If it "
+ "doens't have any it should use ObjectParser instead of ConstructingObjectParser. This is a bug in the parser "
+ "declaration.");
assert false == constructorArgInfos.isEmpty() : "[" + objectParser.getName() + "] must configure at least on constructor "
+ "argument. If it doesn't have any it should use ObjectParser instead of ConstructingObjectParser. This is a bug "
+ "in the parser declaration.";
// All missing constructor arguments were optional. Just build the target and return it.
buildTarget();
return targetObject;
}
private void buildTarget() {
try {
targetObject = builder.apply(constructorArgs);
while (queuedFieldsCount > 0) {
queuedFieldsCount -= 1;
queuedFields[queuedFieldsCount].accept(targetObject);
}
} catch (ParsingException e) {
throw new ParsingException(e.getLineNumber(), e.getColumnNumber(),
"failed to build [" + objectParser.getName() + "] after last required field arrived", e);
} catch (Exception e) {
throw new ParsingException(null, "Failed to build [" + objectParser.getName() + "] after last required field arrived", e);
}
if (message == null) {
throw new IllegalStateException("The targetObject wasn't built but we aren't missing any constructor args. This is a bug "
+ " in ConstructingObjectParser. Here are the constructor arguments " + Arrays.toString(constructorArgs)
+ " and here are is the count [" + constructorArgsCollected + "]. Good luck figuring out what happened."
+ " I'm truly sorry you got here.");
}
throw new IllegalArgumentException(message.append(']').toString());
}
}
private static class ConstructorArgInfo {
final ParseField field;
final boolean required;
public ConstructorArgInfo(ParseField field, boolean required) {
this.field = field;
this.required = required;
}
}
}

View File

@ -19,17 +19,26 @@
package org.elasticsearch.common.xcontent;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.ParseFieldMatcherSupplier;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matcher;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static java.util.Collections.unmodifiableList;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
public class ConstructingObjectParserTests extends ESTestCase {
private static final ParseFieldMatcherSupplier MATCHER = () -> ParseFieldMatcher.STRICT;
@ -38,7 +47,7 @@ public class ConstructingObjectParserTests extends ESTestCase {
* Builds the object in random order and parses it.
*/
public void testRandomOrder() throws Exception {
HasRequiredArguments expected = new HasRequiredArguments(randomAsciiOfLength(5), randomInt());
HasCtorArguments expected = new HasCtorArguments(randomAsciiOfLength(5), randomInt());
expected.setMineral(randomInt());
expected.setFruit(randomInt());
expected.setA(randomBoolean() ? null : randomAsciiOfLength(5));
@ -49,9 +58,8 @@ public class ConstructingObjectParserTests extends ESTestCase {
expected.toXContent(builder, ToXContent.EMPTY_PARAMS);
builder = shuffleXContent(builder);
BytesReference bytes = builder.bytes();
XContentParser parser = XContentFactory.xContent(bytes).createParser(bytes);
try {
HasRequiredArguments parsed = HasRequiredArguments.PARSER.apply(parser, MATCHER);
try (XContentParser parser = XContentFactory.xContent(bytes).createParser(bytes)) {
HasCtorArguments parsed = randomFrom(HasCtorArguments.ALL_PARSERS).apply(parser, MATCHER);
assertEquals(expected.animal, parsed.animal);
assertEquals(expected.vegetable, parsed.vegetable);
assertEquals(expected.mineral, parsed.mineral);
@ -66,44 +74,84 @@ public class ConstructingObjectParserTests extends ESTestCase {
}
}
public void testMissingAllConstructorParams() throws IOException {
public void testMissingAllConstructorArgs() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1\n"
+ "}");
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
assertEquals("Required [animal, vegetable]", e.getMessage());
ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> objectParser = randomBoolean() ? HasCtorArguments.PARSER
: HasCtorArguments.PARSER_VEGETABLE_OPTIONAL;
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, MATCHER));
if (objectParser == HasCtorArguments.PARSER) {
assertEquals("Required [animal, vegetable]", e.getMessage());
} else {
assertEquals("Required [animal]", e.getMessage());
}
}
public void testMissingSecondConstructorParam() throws IOException {
public void testMissingAllConstructorArgsButNotRequired() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1\n"
+ "}");
HasCtorArguments parsed = HasCtorArguments.PARSER_ALL_OPTIONAL.apply(parser, MATCHER);
assertEquals(1, parsed.mineral);
}
public void testMissingSecondConstructorArg() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1,\n"
+ " \"animal\": \"cat\"\n"
+ "}");
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
() -> HasCtorArguments.PARSER.apply(parser, MATCHER));
assertEquals("Required [vegetable]", e.getMessage());
}
public void testMissingFirstConstructorParam() throws IOException {
public void testMissingSecondConstructorArgButNotRequired() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1,\n"
+ " \"animal\": \"cat\"\n"
+ "}");
@SuppressWarnings("unchecked")
HasCtorArguments parsed = randomFrom(HasCtorArguments.PARSER_VEGETABLE_OPTIONAL, HasCtorArguments.PARSER_ALL_OPTIONAL).apply(parser,
MATCHER);
assertEquals(1, parsed.mineral);
assertEquals("cat", parsed.animal);
}
public void testMissingFirstConstructorArg() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1,\n"
+ " \"vegetable\": 2\n"
+ "}");
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
@SuppressWarnings("unchecked")
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> randomFrom(HasCtorArguments.PARSER, HasCtorArguments.PARSER_VEGETABLE_OPTIONAL).apply(parser, MATCHER));
assertEquals("Required [animal]", e.getMessage());
}
public void testMissingFirstConstructorArgButNotRequired() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"mineral\": 1,\n"
+ " \"vegetable\": 2\n"
+ "}");
HasCtorArguments parsed = HasCtorArguments.PARSER_ALL_OPTIONAL.apply(parser, MATCHER);
assertEquals(1, parsed.mineral);
assertEquals((Integer) 2, parsed.vegetable);
}
public void testRepeatedConstructorParam() throws IOException {
XContentParser parser = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"vegetable\": 1,\n"
+ " \"vegetable\": 2\n"
+ "}");
Throwable e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
Throwable e = expectThrows(ParsingException.class, () -> randomFrom(HasCtorArguments.ALL_PARSERS).apply(parser, MATCHER));
assertEquals("[has_required_arguments] failed to parse field [vegetable]", e.getMessage());
e = e.getCause();
assertThat(e, instanceOf(IllegalArgumentException.class));
@ -117,7 +165,7 @@ public class ConstructingObjectParserTests extends ESTestCase {
+ " \"vegetable\": 2,\n"
+ " \"a\": \"supercalifragilisticexpialidocious\"\n"
+ "}");
ParsingException e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
ParsingException e = expectThrows(ParsingException.class, () -> randomFrom(HasCtorArguments.ALL_PARSERS).apply(parser, MATCHER));
assertEquals("[has_required_arguments] failed to parse field [a]", e.getMessage());
assertEquals(4, e.getLineNumber());
assertEquals("[a] must be less than 10 characters in length but was [supercalifragilisticexpialidocious]",
@ -131,7 +179,7 @@ public class ConstructingObjectParserTests extends ESTestCase {
+ " \"animal\": \"cat\"\n,"
+ " \"vegetable\": 2\n"
+ "}");
ParsingException e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER));
ParsingException e = expectThrows(ParsingException.class, () -> randomFrom(HasCtorArguments.ALL_PARSERS).apply(parser, MATCHER));
assertEquals("[has_required_arguments] failed to parse field [vegetable]", e.getMessage());
assertEquals(4, e.getLineNumber());
e = (ParsingException) e.getCause();
@ -149,18 +197,28 @@ public class ConstructingObjectParserTests extends ESTestCase {
}
ConstructingObjectParser<NoConstructorArgs, ParseFieldMatcherSupplier> parser = new ConstructingObjectParser<>(
"constructor_args_required", (a) -> new NoConstructorArgs());
Exception e = expectThrows(IllegalStateException.class, () -> parser.apply(XContentType.JSON.xContent().createParser("{}"), null));
assertEquals("[constructor_args_required] must configure at least on constructor argument. If it doens't have any it "
+ "should use ObjectParser instead of ConstructingObjectParser. This is a bug in the parser declaration.", e.getMessage());
try {
parser.apply(XContentType.JSON.xContent().createParser("{}"), null);
fail("Expected AssertionError");
} catch (AssertionError e) {
assertEquals("[constructor_args_required] must configure at least on constructor argument. If it doesn't have any it should "
+ "use ObjectParser instead of ConstructingObjectParser. This is a bug in the parser declaration.", e.getMessage());
}
}
/**
* Tests the non-constructor fields are only set on time.
*/
public void testCalledOneTime() throws IOException {
boolean ctorArgOptional = randomBoolean();
class CalledOneTime {
public CalledOneTime(String yeah) {
assertEquals("!", yeah);
Matcher<String> yeahMatcher = equalTo("!");
if (ctorArgOptional) {
// either(yeahMatcher).or(nullValue) is broken by https://github.com/hamcrest/JavaHamcrest/issues/49
yeahMatcher = anyOf(yeahMatcher, nullValue());
}
assertThat(yeah, yeahMatcher);
}
boolean fooSet = false;
@ -172,7 +230,7 @@ public class ConstructingObjectParserTests extends ESTestCase {
ConstructingObjectParser<CalledOneTime, ParseFieldMatcherSupplier> parser = new ConstructingObjectParser<>("one_time_test",
(a) -> new CalledOneTime((String) a[0]));
parser.declareString(CalledOneTime::setFoo, new ParseField("foo"));
parser.declareString(constructorArg(), new ParseField("yeah"));
parser.declareString(ctorArgOptional ? optionalConstructorArg() : constructorArg(), new ParseField("yeah"));
// ctor arg first so we can test for the bug we found one time
XContentParser xcontent = XContentType.JSON.xContent().createParser(
@ -191,12 +249,23 @@ public class ConstructingObjectParserTests extends ESTestCase {
+ "}");
result = parser.apply(xcontent, MATCHER);
assertTrue(result.fooSet);
if (ctorArgOptional) {
// and without the constructor arg if we've made it optional
xcontent = XContentType.JSON.xContent().createParser(
"{\n"
+ " \"foo\": \"foo\"\n"
+ "}");
result = parser.apply(xcontent, MATCHER);
}
assertTrue(result.fooSet);
}
private static class HasRequiredArguments implements ToXContent {
private static class HasCtorArguments implements ToXContent {
@Nullable
final String animal;
final int vegetable;
@Nullable
final Integer vegetable;
int mineral;
int fruit;
String a;
@ -204,7 +273,7 @@ public class ConstructingObjectParserTests extends ESTestCase {
String c;
boolean d;
public HasRequiredArguments(String animal, int vegetable) {
public HasCtorArguments(@Nullable String animal, @Nullable Integer vegetable) {
this.animal = animal;
this.vegetable = vegetable;
}
@ -263,17 +332,31 @@ public class ConstructingObjectParserTests extends ESTestCase {
return builder;
}
public static final ConstructingObjectParser<HasRequiredArguments, ParseFieldMatcherSupplier> PARSER =
new ConstructingObjectParser<>("has_required_arguments", a -> new HasRequiredArguments((String) a[0], (Integer) a[1]));
static {
PARSER.declareString(constructorArg(), new ParseField("animal"));
PARSER.declareInt(constructorArg(), new ParseField("vegetable"));
PARSER.declareInt(HasRequiredArguments::setMineral, new ParseField("mineral"));
PARSER.declareInt(HasRequiredArguments::setFruit, new ParseField("fruit"));
PARSER.declareString(HasRequiredArguments::setA, new ParseField("a"));
PARSER.declareString(HasRequiredArguments::setB, new ParseField("b"));
PARSER.declareString(HasRequiredArguments::setC, new ParseField("c"));
PARSER.declareBoolean(HasRequiredArguments::setD, new ParseField("d"));
/*
* It is normal just to declare a single PARSER but we use a couple of different parsers for testing so we have all of these. Don't
* this this style is normal just because it is in the test.
*/
public static final ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> PARSER = buildParser(true, true);
public static final ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> PARSER_VEGETABLE_OPTIONAL = buildParser(
true, false);
public static final ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> PARSER_ALL_OPTIONAL = buildParser(false,
false);
public static final List<ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier>> ALL_PARSERS = unmodifiableList(
Arrays.asList(PARSER, PARSER_VEGETABLE_OPTIONAL, PARSER_ALL_OPTIONAL));
private static ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> buildParser(boolean animalRequired,
boolean vegetableRequired) {
ConstructingObjectParser<HasCtorArguments, ParseFieldMatcherSupplier> parser = new ConstructingObjectParser<>(
"has_required_arguments", a -> new HasCtorArguments((String) a[0], (Integer) a[1]));
parser.declareString(animalRequired ? constructorArg() : optionalConstructorArg(), new ParseField("animal"));
parser.declareInt(vegetableRequired ? constructorArg() : optionalConstructorArg(), new ParseField("vegetable"));
parser.declareInt(HasCtorArguments::setMineral, new ParseField("mineral"));
parser.declareInt(HasCtorArguments::setFruit, new ParseField("fruit"));
parser.declareString(HasCtorArguments::setA, new ParseField("a"));
parser.declareString(HasCtorArguments::setB, new ParseField("b"));
parser.declareString(HasCtorArguments::setC, new ParseField("c"));
parser.declareBoolean(HasCtorArguments::setD, new ParseField("d"));
return parser;
}
}
}