mirror of
https://github.com/elastic/logstash.git
synced 2025-04-23 22:27:21 -04:00
Add ConstructingObjectParser for building an object from a configuration map.
The idea behind ConstructingObjectParser was largely lifted from Elasticsearch's class by the same name. The reasons I liked this approach was that it provided a type-safe way to define how user input (a Map, in Logstash's case) is parsed to create an object like an input plugin. The ConstructingObjectParser roughly implements in Java what `config/mixin.rb` provides for Ruby plugins. This class allows us to take a Map that comes from the config parser and build a pipeline plugin (input, filter, output). Differences from the ruby api: * Custom object validation is possible. Example, `http_poller` has a map of `urls` which are `name => { configuration }`, and we can achieve type-safe validation of this `urls` thing which benefits us as plugin authors and benefits users through better error reporting. * Explicit `default` value is not required because Java class fields already allow you to set default values :) Some rationale on IllegalArgumentException: * Object construction will throw IllegalArgumentException upon validation failure. This was chosen because IllegalArgumentException is an unchecked exception we can throw while still satisfying the constraints of the java.util.function interfaces (BiFunction, etc)
This commit is contained in:
parent
0179bfce2c
commit
c275a608ce
6 changed files with 645 additions and 15 deletions
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
26
gradlew
vendored
26
gradlew
vendored
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
|
@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS=""
|
|||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
|
@ -154,11 +154,19 @@ if $cygwin ; then
|
|||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
6
gradlew.bat
vendored
6
gradlew.bat
vendored
|
@ -49,7 +49,6 @@ goto fail
|
|||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
|
@ -60,11 +59,6 @@ set _SKIP=2
|
|||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
package org.logstash.plugin;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A functional class which constructs an object from a given configuration map.
|
||||
*
|
||||
* History: This is idea is taken largely from Elasticsearch's ConstructingObjectParser
|
||||
*
|
||||
* @param <Value> The object type to construct when `parse` is called.
|
||||
*/
|
||||
public class ConstructingObjectParser<Value> implements Function<Map<String, Object>, Value> {
|
||||
private final Function<Object[], Value> builder;
|
||||
private final Map<String, BiConsumer<Value, Object>> parsers = new LinkedHashMap<>();
|
||||
private final Map<String, BiConsumer<Object[], Object>> constructorArgs;
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public ConstructingObjectParser(Supplier<Value> supplier) {
|
||||
this.builder = args -> supplier.get();
|
||||
|
||||
// Reject any attempts to add constructor fields with an immutable map.
|
||||
constructorArgs = Collections.emptyMap();
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public ConstructingObjectParser(Function<Object[], Value> builder) {
|
||||
this.builder = builder;
|
||||
constructorArgs = new TreeMap<>();
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static Integer integerTransform(Object object) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).intValue();
|
||||
} else if (object instanceof String) {
|
||||
return Integer.parseInt((String) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a number, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static Float floatTransform(Object object) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).floatValue();
|
||||
} else if (object instanceof String) {
|
||||
return Float.parseFloat((String) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a number, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static Double doubleTransform(Object object) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).doubleValue();
|
||||
} else if (object instanceof String) {
|
||||
return Double.parseDouble((String) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a number, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static Long longTransform(Object object) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).longValue();
|
||||
} else if (object instanceof String) {
|
||||
return Long.parseLong((String) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a number, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static String stringTransform(Object object) {
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
} else if (object instanceof Number) {
|
||||
return object.toString();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a string, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static Boolean booleanTransform(Object object) {
|
||||
if (object instanceof Boolean) {
|
||||
return (Boolean) object;
|
||||
} else if (object instanceof String) {
|
||||
switch ((String) object) {
|
||||
case "true":
|
||||
return true;
|
||||
case "false":
|
||||
return false;
|
||||
default:
|
||||
throw new IllegalArgumentException("Value must be a boolean 'true' or 'false', but is " + object);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Value must be a boolean, but is " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public static <T> T objectTransform(Object object, ConstructingObjectParser<T> parser) {
|
||||
if (object instanceof Map) {
|
||||
// XXX: Fix this unchecked cast.
|
||||
return parser.apply((Map<String, Object>) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Object value must be a Map, but is a " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an field with an long value.
|
||||
*
|
||||
* @param name the name of this field
|
||||
* @param consumer the function to call once the value is available
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareLong(String name, BiConsumer<Value, Long> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::longTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare an long constructor argument.
|
||||
*
|
||||
* @param name the name of the field.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareLong(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::longTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an field with an integer value.
|
||||
*
|
||||
* @param name the name of this field
|
||||
* @param consumer the function to call once the value is available
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareInteger(String name, BiConsumer<Value, Integer> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::integerTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare an integer constructor argument.
|
||||
*
|
||||
* @param name the name of the field.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareInteger(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::integerTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field with a string value.
|
||||
*
|
||||
* @param name the name of this field
|
||||
* @param consumer the function to call once the value is available
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareString(String name, BiConsumer<Value, String> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::stringTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument that is a string.
|
||||
*
|
||||
* @param name the name of this field.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareString(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::stringTransform);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareFloat(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::floatTransform);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <T> void declareField(String name, BiConsumer<Value, T> consumer, Function<Object, T> transform) {
|
||||
BiConsumer<Value, Object> objConsumer = (value, object) -> consumer.accept(value, transform.apply(object));
|
||||
parsers.put(name, objConsumer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <T> void declareConstructorArg(String name, Function<Object, T> transform) {
|
||||
final int position = constructorArgs.size();
|
||||
BiConsumer<Object[], Object> objConsumer = (array, object) -> array[position] = transform.apply(object);
|
||||
constructorArgs.put(name, objConsumer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareFloat(String name, BiConsumer<Value, Float> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::floatTransform);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareDouble(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::doubleTransform);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareDouble(String name, BiConsumer<Value, Double> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::doubleTransform);
|
||||
}
|
||||
|
||||
private Value construct(Map<String, Object> config) {
|
||||
// XXX: Maybe this can just be an Object[]
|
||||
Object[] args = new Object[constructorArgs.size()];
|
||||
|
||||
// Constructor arguments. Any constructor argument is a *required* setting.
|
||||
for (Map.Entry<String, BiConsumer<Object[], Object>> argInfo : constructorArgs.entrySet()) {
|
||||
String name = argInfo.getKey();
|
||||
BiConsumer<Object[], Object> argsBuilder = argInfo.getValue();
|
||||
if (config.containsKey(name)) {
|
||||
argsBuilder.accept(args, config.get(name));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Missing required argument '" + name + "' for " + getClass());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.apply(args);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareBoolean(String name) {
|
||||
declareConstructorArg(name, ConstructingObjectParser::booleanTransform);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public void declareBoolean(String name, BiConsumer<Value, Boolean> consumer) {
|
||||
declareField(name, consumer, ConstructingObjectParser::booleanTransform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field with an object value
|
||||
*
|
||||
* @param name the name of this field
|
||||
* @param consumer the function to call once the value is available
|
||||
* @param parser The ConstructingObjectParser that will build the object
|
||||
* @param <T> The type of object to store as the value.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <T> void declareObject(String name, BiConsumer<Value, T> consumer, ConstructingObjectParser<T> parser) {
|
||||
declareField(name, consumer, (t) -> objectTransform(t, parser));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare a constructor argument that is an object.
|
||||
*
|
||||
* @param name the name of the field which represents this constructor argument
|
||||
* @param parser the ConstructingObjectParser that builds the object
|
||||
* @param <T> The type of object created by the parser.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") // Public Interface
|
||||
public <T> void declareObject(String name, ConstructingObjectParser<T> parser) {
|
||||
declareConstructorArg(name, (t) -> objectTransform(t, parser));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an object using the given config.
|
||||
*
|
||||
* The intent is that a config map, such as one from a Logstash pipeline config:
|
||||
*
|
||||
* input {
|
||||
* example {
|
||||
* some => "setting"
|
||||
* goes => "here"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ... will know how to build an object for the above "example" input plugin.
|
||||
*/
|
||||
public Value apply(Map<String, Object> config) {
|
||||
rejectUnknownFields(config.keySet());
|
||||
|
||||
Value value = construct(config);
|
||||
|
||||
// Now call all the object setters/etc
|
||||
for (Map.Entry<String, Object> entry : config.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (constructorArgs.containsKey(name)) {
|
||||
// Skip constructor arguments
|
||||
continue;
|
||||
}
|
||||
|
||||
BiConsumer<Value, Object> parser = parsers.get(name);
|
||||
assert parser != null;
|
||||
|
||||
try {
|
||||
parser.accept(value, entry.getValue());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Field " + name + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isKnownField(String name) {
|
||||
return (parsers.containsKey(name) || constructorArgs.containsKey(name));
|
||||
}
|
||||
|
||||
private void rejectUnknownFields(Set<String> configNames) {
|
||||
// Check for any unknown parameters.
|
||||
List<String> unknown = configNames.stream().filter(name -> !isKnownField(name)).collect(Collectors.toList());
|
||||
|
||||
if (!unknown.isEmpty()) {
|
||||
throw new IllegalArgumentException("Unknown settings: " + unknown);
|
||||
}
|
||||
}
|
||||
}
|
10
logstash-core/src/test/java/org/logstash/TestUtil.java
Normal file
10
logstash-core/src/test/java/org/logstash/TestUtil.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package org.logstash;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class TestUtil {
|
||||
/**
|
||||
* A random instance to share in the test suite.
|
||||
*/
|
||||
public static final Random random = new Random();
|
||||
}
|
|
@ -0,0 +1,299 @@
|
|||
package org.logstash.plugin;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class ConstructingObjectParserTest {
|
||||
public static class FieldIntegrationTest {
|
||||
static final ConstructingObjectParser<Example> EXAMPLE_BUILDER = new ConstructingObjectParser<>(Example::new);
|
||||
static final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<>(args -> Paths.get((String) args[0]));
|
||||
|
||||
static {
|
||||
PATH_BUILDER.declareString("path");
|
||||
|
||||
EXAMPLE_BUILDER.declareFloat("float", Example::setF);
|
||||
EXAMPLE_BUILDER.declareInteger("integer", Example::setI);
|
||||
EXAMPLE_BUILDER.declareLong("long", Example::setL);
|
||||
EXAMPLE_BUILDER.declareDouble("double", Example::setD);
|
||||
EXAMPLE_BUILDER.declareBoolean("boolean", Example::setB);
|
||||
EXAMPLE_BUILDER.declareString("string", Example::setS);
|
||||
|
||||
// Custom transform (Object => Path)
|
||||
EXAMPLE_BUILDER.declareString("path", (example, path) -> example.setPath(Paths.get(path)));
|
||||
|
||||
// Custom nested object constructor: { "object": { "path": "some path" } }
|
||||
EXAMPLE_BUILDER.declareObject("object", Example::setPath, PATH_BUILDER);
|
||||
}
|
||||
|
||||
private final Map<String, Object> config = new HashMap<>();
|
||||
|
||||
public FieldIntegrationTest() {
|
||||
config.put("float", 1F);
|
||||
config.put("integer", 1);
|
||||
config.put("long", 1L);
|
||||
config.put("double", 1D);
|
||||
config.put("boolean", true);
|
||||
config.put("string", "hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsing() {
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(1F, e.getF(), 0.1);
|
||||
assertEquals(1D, e.getD(), 0.1);
|
||||
assertEquals(1, e.getI());
|
||||
assertEquals(1L, e.getL());
|
||||
assertEquals(true, e.isB());
|
||||
assertEquals("hello", e.getS());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomTransform() {
|
||||
config.put("path", "example");
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(Paths.get("example"), e.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedObject() {
|
||||
config.put("object", Collections.singletonMap("path", "example"));
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(Paths.get("example"), e.getPath());
|
||||
}
|
||||
|
||||
private static class Example {
|
||||
private int i;
|
||||
private float f;
|
||||
private double d;
|
||||
private boolean b;
|
||||
|
||||
private long l;
|
||||
private String s;
|
||||
|
||||
private Path path;
|
||||
|
||||
Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
void setPath(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
long getL() {
|
||||
return l;
|
||||
}
|
||||
|
||||
void setL(long l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
int getI() {
|
||||
return i;
|
||||
}
|
||||
|
||||
void setI(int i) {
|
||||
this.i = i;
|
||||
}
|
||||
|
||||
float getF() {
|
||||
return f;
|
||||
}
|
||||
|
||||
void setF(float f) {
|
||||
this.f = f;
|
||||
}
|
||||
|
||||
double getD() {
|
||||
return d;
|
||||
}
|
||||
|
||||
void setD(double d) {
|
||||
this.d = d;
|
||||
}
|
||||
|
||||
boolean isB() {
|
||||
return b;
|
||||
}
|
||||
|
||||
void setB(boolean b) {
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
String getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
void setS(String s) {
|
||||
this.s = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConstructorIntegrationTest {
|
||||
static final ConstructingObjectParser<Example> EXAMPLE_BUILDER = new ConstructingObjectParser<>(args -> new Example((int) args[0], (float) args[1], (long) args[2], (double) args[3], (boolean) args[4], (String) args[5], (Path) args[6], (Path) args[7]));
|
||||
static final ConstructingObjectParser<Path> PATH_BUILDER = new ConstructingObjectParser<>(args -> Paths.get((String) args[0]));
|
||||
|
||||
static {
|
||||
PATH_BUILDER.declareString("path");
|
||||
|
||||
EXAMPLE_BUILDER.declareInteger("integer");
|
||||
EXAMPLE_BUILDER.declareFloat("float");
|
||||
EXAMPLE_BUILDER.declareLong("long");
|
||||
EXAMPLE_BUILDER.declareDouble("double");
|
||||
EXAMPLE_BUILDER.declareBoolean("boolean");
|
||||
EXAMPLE_BUILDER.declareString("string");
|
||||
|
||||
// Custom transform (Object => Path)
|
||||
EXAMPLE_BUILDER.declareConstructorArg("path", (object) -> Paths.get((String) object));
|
||||
|
||||
// Custom nested object constructor: { "object": { "path": "some path" } }
|
||||
EXAMPLE_BUILDER.declareObject("object", PATH_BUILDER);
|
||||
|
||||
}
|
||||
|
||||
private final Map<String, Object> config = new LinkedHashMap<>();
|
||||
|
||||
public ConstructorIntegrationTest() {
|
||||
config.put("float", 1F);
|
||||
config.put("integer", 1);
|
||||
config.put("long", 1L);
|
||||
config.put("double", 1D);
|
||||
config.put("boolean", true);
|
||||
config.put("string", "hello");
|
||||
config.put("path", "path1");
|
||||
config.put("object", Collections.singletonMap("path", "path2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsing() {
|
||||
Example e = EXAMPLE_BUILDER.apply(config);
|
||||
assertEquals(1F, e.getF(), 0.1);
|
||||
assertEquals(1D, e.getD(), 0.1);
|
||||
assertEquals(1, e.getI());
|
||||
assertEquals(1L, e.getL());
|
||||
assertEquals(true, e.isB());
|
||||
assertEquals("hello", e.getS());
|
||||
assertEquals(Paths.get("path1"), e.getP1());
|
||||
assertEquals(Paths.get("path2"), e.getP2());
|
||||
}
|
||||
|
||||
private static class Example {
|
||||
private final int i;
|
||||
private final float f;
|
||||
private final double d;
|
||||
private final boolean b;
|
||||
|
||||
private final long l;
|
||||
private final String s;
|
||||
|
||||
private final Path p1;
|
||||
private final Path p2;
|
||||
|
||||
Example(int i, float f, long l, double d, boolean b, String s, Path p1, Path p2) {
|
||||
this.i = i;
|
||||
this.f = f;
|
||||
this.l = l;
|
||||
this.d = d;
|
||||
this.b = b;
|
||||
this.s = s;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
}
|
||||
|
||||
int getI() {
|
||||
return i;
|
||||
}
|
||||
|
||||
float getF() {
|
||||
return f;
|
||||
}
|
||||
|
||||
double getD() {
|
||||
return d;
|
||||
}
|
||||
|
||||
boolean isB() {
|
||||
return b;
|
||||
}
|
||||
|
||||
long getL() {
|
||||
return l;
|
||||
}
|
||||
|
||||
String getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
Path getP1() {
|
||||
return p1;
|
||||
}
|
||||
|
||||
Path getP2() {
|
||||
return p2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class StringAccepts {
|
||||
private final Object input;
|
||||
private final Object expected;
|
||||
|
||||
public StringAccepts(Object input, Object expected) {
|
||||
this.input = input;
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"1", "1"},
|
||||
{1, "1"},
|
||||
{1L, "1"},
|
||||
{1F, "1.0"},
|
||||
{1D, "1.0"},
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringTransform() {
|
||||
String value = ConstructingObjectParser.stringTransform(input);
|
||||
assertEquals(expected, value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class StringRejections {
|
||||
private final Object input;
|
||||
|
||||
public StringRejections(Object input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Parameters
|
||||
public static List<Object> data() {
|
||||
return Arrays.asList(
|
||||
new Object(),
|
||||
Collections.emptyMap(),
|
||||
Collections.emptyList()
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testFailure() {
|
||||
ConstructingObjectParser.stringTransform(input);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue