WIP: Add support for post-processing via annotated configuration elements

dev
Exlll 1 year ago
parent 89a2e9057b
commit 31d4d09e85

@ -0,0 +1,57 @@
package de.exlll.configlib;
import java.util.function.Predicate;
import static de.exlll.configlib.Validator.requireNonNull;
/**
* Implementations of this interface test configuration elements for specific
* conditions.
*/
public interface ConfigurationElementFilter
extends Predicate<ConfigurationElement<?>> {
@Override
default ConfigurationElementFilter and(
Predicate<? super ConfigurationElement<?>> other
) {
return element -> test(element) && other.test(element);
}
/**
* Creates a new {@code ConfigurationElementFilter} whose {@code test}
* method returns {@code true} if the tested configuration element is
* of the given type.
*
* @param type the type the filter is looking for
* @return new {@code ConfigurationElementFilter} that tests configuration
* elements for their type
* @throws NullPointerException if {@code type} is null
*/
static ConfigurationElementFilter byType(Class<?> type) {
requireNonNull(type, "type");
return element -> element.type().equals(type);
}
/**
* Creates a new {@code ConfigurationElementFilter} whose {@code test}
* method returns {@code true} if the tested configuration element is
* annotated with a {@code PostProcess} annotation whose key equals
* {@code key}.
*
* @param key the key of the {@code PostProcess} annotation the filter is
* looking for
* @return new {@code ConfigurationElementFilter} that tests configuration
* elements for {@code PostProcess} annotations with the given key
* @throws NullPointerException if {@code key} is null
*/
static ConfigurationElementFilter byPostProcessKey(String key) {
requireNonNull(key, "post-process key");
return element -> {
final PostProcess postProcess = element.annotation(PostProcess.class);
if (postProcess == null) return false;
final String actualKey = postProcess.key();
return actualKey.equals(key);
};
}
}

@ -7,6 +7,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
@ -14,10 +15,14 @@ import static de.exlll.configlib.Validator.requireNonNull;
* A collection of values used to configure the serialization of configurations. * A collection of values used to configure the serialization of configurations.
*/ */
public class ConfigurationProperties { public class ConfigurationProperties {
private final Map<Class<?>, Serializer<?, ?>> serializersByType; private final Map<Class<?>, Serializer<?, ?>>
serializersByType;
private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>> private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>>
serializerFactoriesByType; serializerFactoriesByType;
private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition; private final Map<Predicate<? super Type>, Serializer<?, ?>>
serializersByCondition;
private final Map<Predicate<? super ConfigurationElement<?>>, UnaryOperator<?>>
postProcessorsByCondition;
private final NameFormatter formatter; private final NameFormatter formatter;
private final FieldFilter filter; private final FieldFilter filter;
private final boolean outputNulls; private final boolean outputNulls;
@ -33,9 +38,12 @@ public class ConfigurationProperties {
protected ConfigurationProperties(Builder<?> builder) { protected ConfigurationProperties(Builder<?> builder) {
this.serializersByType = Map.copyOf(builder.serializersByType); this.serializersByType = Map.copyOf(builder.serializersByType);
this.serializerFactoriesByType = Map.copyOf(builder.serializerFactoriesByType); this.serializerFactoriesByType = Map.copyOf(builder.serializerFactoriesByType);
this.serializersByCondition = Collections.unmodifiableMap(new LinkedHashMap<>( this.serializersByCondition = Collections.unmodifiableMap(
builder.serializersByCondition new LinkedHashMap<>(builder.serializersByCondition)
)); );
this.postProcessorsByCondition = Collections.unmodifiableMap(
new LinkedHashMap<>(builder.postProcessorsByCondition)
);
this.formatter = requireNonNull(builder.formatter, "name formatter"); this.formatter = requireNonNull(builder.formatter, "name formatter");
this.filter = requireNonNull(builder.filter, "field filter"); this.filter = requireNonNull(builder.filter, "field filter");
this.outputNulls = builder.outputNulls; this.outputNulls = builder.outputNulls;
@ -79,11 +87,14 @@ public class ConfigurationProperties {
* @param <B> the type of builder * @param <B> the type of builder
*/ */
public static abstract class Builder<B extends Builder<B>> { public static abstract class Builder<B extends Builder<B>> {
private final Map<Class<?>, Serializer<?, ?>> serializersByType = new HashMap<>(); private final Map<Class<?>, Serializer<?, ?>>
serializersByType = new HashMap<>();
private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>> private final Map<Class<?>, Function<? super SerializerContext, ? extends Serializer<?, ?>>>
serializerFactoriesByType = new HashMap<>(); serializerFactoriesByType = new HashMap<>();
private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition = private final Map<Predicate<? super Type>, Serializer<?, ?>>
new LinkedHashMap<>(); serializersByCondition = new LinkedHashMap<>();
private final Map<Predicate<? super ConfigurationElement<?>>, UnaryOperator<?>>
postProcessorsByCondition = new LinkedHashMap<>();
private NameFormatter formatter = NameFormatters.IDENTITY; private NameFormatter formatter = NameFormatters.IDENTITY;
private FieldFilter filter = FieldFilters.DEFAULT; private FieldFilter filter = FieldFilters.DEFAULT;
private boolean outputNulls = false; private boolean outputNulls = false;
@ -96,6 +107,7 @@ public class ConfigurationProperties {
this.serializersByType.putAll(properties.serializersByType); this.serializersByType.putAll(properties.serializersByType);
this.serializerFactoriesByType.putAll(properties.serializerFactoriesByType); this.serializerFactoriesByType.putAll(properties.serializerFactoriesByType);
this.serializersByCondition.putAll(properties.serializersByCondition); this.serializersByCondition.putAll(properties.serializersByCondition);
this.postProcessorsByCondition.putAll(properties.postProcessorsByCondition);
this.formatter = properties.formatter; this.formatter = properties.formatter;
this.filter = properties.filter; this.filter = properties.filter;
this.outputNulls = properties.outputNulls; this.outputNulls = properties.outputNulls;
@ -133,8 +145,9 @@ public class ConfigurationProperties {
/** /**
* Adds a serializer for the given type. * Adds a serializer for the given type.
* <p> * <p>
* If this library already provides a serializer for the given type (e.g. {@code BigInteger}, * If this library already provides a serializer for the given type
* {@code LocalDate}, etc.) the serializer added by this method takes precedence. * (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer
* added by this method takes precedence.
* <p> * <p>
* If a factory is added via the {@link #addSerializerFactory(Class, Function)} method for * If a factory is added via the {@link #addSerializerFactory(Class, Function)} method for
* the same type, the serializer created by that factory takes precedence. * the same type, the serializer created by that factory takes precedence.
@ -159,8 +172,9 @@ public class ConfigurationProperties {
/** /**
* Adds a serializer factory for the given type. * Adds a serializer factory for the given type.
* <p> * <p>
* If this library already provides a serializer for the given type (e.g. {@code BigInteger}, * If this library already provides a serializer for the given type
* {@code LocalDate}, etc.) the serializer created by the factory takes precedence. * (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer
* created by the factory takes precedence.
* <p> * <p>
* If a serializer is added via the {@link #addSerializer(Class, Serializer)} method * If a serializer is added via the {@link #addSerializer(Class, Serializer)} method
* for the same type, the serializer created by the factory that was added by this * for the same type, the serializer created by the factory that was added by this
@ -204,6 +218,33 @@ public class ConfigurationProperties {
return getThis(); return getThis();
} }
/**
* Defines a post-processor for each configuration element that fulfils
* the given condition. Multiple post-processors are applied if an
* element fulfills more than one condition. The conditions are checked
* in the order in which they were added.
* <p>
* <b>NOTE</b>:
* It is the developer's responsibility to ensure that the type of the
* configuration element matches the type the post-processor expects.
*
* @param condition the condition that is checked
* @param postProcessor the post-processor to be applied if the
* condition is true
* @return this builder
* @throws NullPointerException if any argument is null
* @see ConfigurationElementFilter
*/
public final B addPostProcessor(
Predicate<? super ConfigurationElement<?>> condition,
UnaryOperator<?> postProcessor
) {
requireNonNull(condition, "condition");
requireNonNull(postProcessor, "post-processor");
this.postProcessorsByCondition.put(condition, postProcessor);
return getThis();
}
/** /**
* Sets whether configuration elements, or collection elements whose value * Sets whether configuration elements, or collection elements whose value
* is null should be output while serializing the configuration. * is null should be output while serializing the configuration.
@ -311,6 +352,16 @@ public class ConfigurationProperties {
return serializersByCondition; return serializersByCondition;
} }
/**
* Returns an unmodifiable map of post-processors by condition.
*
* @return post-processors by condition
*/
public final Map<Predicate<? super ConfigurationElement<?>>, UnaryOperator<?>>
getPostProcessorsByCondition() {
return postProcessorsByCondition;
}
/** /**
* Returns whether null values should be output. * Returns whether null values should be output.
* *

@ -2,7 +2,6 @@ package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement; import de.exlll.configlib.ConfigurationElements.FieldElement;
import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -13,26 +12,13 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
@Override @Override
public T deserialize(Map<?, ?> serializedConfiguration) { public T deserialize(Map<?, ?> serializedConfiguration) {
final T result = Reflect.callNoParamConstructor(type); final var deserializedElements = deserializeConfigurationElements(serializedConfiguration);
final var elements = elements();
for (final var element : elements()) { final T result = newDefaultInstance();
final var formattedName = formatter.format(element.name()); for (int i = 0; i < deserializedElements.length; i++) {
final FieldElement fieldElement = elements.get(i);
if (!serializedConfiguration.containsKey(formattedName)) Reflect.setValue(fieldElement.element(), result, deserializedElements[i]);
continue;
final var serializedValue = serializedConfiguration.get(formattedName);
final var field = element.element();
if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveFieldType(field);
Reflect.setValue(field, result, null);
} else if (serializedValue != null) {
Object deserializeValue = deserialize(element, serializedValue);
Reflect.setValue(field, result, deserializeValue);
} }
}
return postProcessor.apply(result); return postProcessor.apply(result);
} }
@ -64,15 +50,15 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
return Reflect.callNoParamConstructor(type); return Reflect.callNoParamConstructor(type);
} }
private static void requireNonPrimitiveFieldType(Field field) {
if (field.getType().isPrimitive()) {
String msg = ("Cannot set field '%s' to null value. Primitive types " +
"cannot be assigned null.").formatted(field);
throw new ConfigurationException(msg);
}
}
Class<T> getConfigurationType() { Class<T> getConfigurationType() {
return type; return type;
} }
// This object must only be used for the `getDefaultValueOf` method below.
private final T defaultInstance = newDefaultInstance();
@Override
protected Object getDefaultValueOf(FieldElement element) {
return Reflect.getValue(element.element(), defaultInstance);
}
} }

@ -22,7 +22,7 @@ import java.lang.annotation.Target;
* method call. If the return type is {@code void}, then the method is simply * method call. If the return type is {@code void}, then the method is simply
* called on the given instance. * called on the given instance.
*/ */
@Target({ElementType.FIELD, ElementType.METHOD}) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface PostProcess { public @interface PostProcess {
String key() default ""; String key() default "";

@ -2,7 +2,6 @@ package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement; import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import java.lang.reflect.RecordComponent;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -14,32 +13,8 @@ final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement
@Override @Override
public R deserialize(Map<?, ?> serializedConfiguration) { public R deserialize(Map<?, ?> serializedConfiguration) {
final var elements = elements(); final var ctorArgs = deserializeConfigurationElements(serializedConfiguration);
final var constructorArguments = new Object[elements.size()]; final var result = Reflect.callCanonicalConstructor(type, ctorArgs);
for (int i = 0, size = elements.size(); i < size; i++) {
final var element = elements.get(i);
final var formattedName = formatter.format(element.name());
if (!serializedConfiguration.containsKey(formattedName)) {
constructorArguments[i] = Reflect.getDefaultValue(element.type());
continue;
}
final var serializedValue = serializedConfiguration.get(formattedName);
final var recordComponent = element.element();
if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveComponentType(recordComponent);
constructorArguments[i] = null;
} else if (serializedValue == null) {
constructorArguments[i] = Reflect.getDefaultValue(element.type());
} else {
constructorArguments[i] = deserialize(element, serializedValue);
}
}
final R result = Reflect.callCanonicalConstructor(type, constructorArguments);
return postProcessor.apply(result); return postProcessor.apply(result);
} }
@ -72,16 +47,12 @@ final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement
: Reflect.callCanonicalConstructorWithDefaultValues(type); : Reflect.callCanonicalConstructorWithDefaultValues(type);
} }
private static void requireNonPrimitiveComponentType(RecordComponent component) {
if (component.getType().isPrimitive()) {
String msg = ("Cannot set component '%s' of record type '%s' to null. Primitive types " +
"cannot be assigned null values.")
.formatted(component, component.getDeclaringRecord());
throw new ConfigurationException(msg);
}
}
Class<R> getRecordType() { Class<R> getRecordType() {
return type; return type;
} }
@Override
protected Object getDefaultValueOf(RecordComponentElement element) {
return Reflect.getDefaultValue(element.type());
}
} }

@ -1,7 +1,12 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -101,10 +106,98 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
return deserialized; return deserialized;
} }
protected final Object[] deserializeConfigurationElements(
Map<?, ?> serializedConfiguration
) {
final var elements = elements();
final var result = new Object[elements.size()];
for (int i = 0, size = elements.size(); i < size; i++) {
final var element = elements.get(i);
final var formattedName = formatter.format(element.name());
if (!serializedConfiguration.containsKey(formattedName)) {
final Object defaultValue = getDefaultValueOf(element);
result[i] = applyPostProcessorForElement(element, defaultValue);
// TODO: if (result[i] == null) requireNonPrimitiveType(element);
continue;
}
final var serializedValue = serializedConfiguration.get(formattedName);
if ((serializedValue == null) && properties.inputNulls()) {
result[i] = null;
} else if (serializedValue == null) {
result[i] = getDefaultValueOf(element);
} else {
result[i] = deserialize(element, serializedValue);
}
if (result[i] == null) requireNonPrimitiveType(element);
result[i] = applyPostProcessorForElement(element, result[i]);
// TODO: PostProcessor could return null, check should be done after
}
return result;
}
private Object applyPostProcessorForElement(
ConfigurationElement<?> element,
Object deserializeValue
) {
Object result = deserializeValue;
for (final var entry : properties.getPostProcessorsByCondition().entrySet()) {
final var condition = entry.getKey();
if (condition.test(element)) {
final var postProcessor = entry.getValue();
result = tryApplyPostProcessorForElement(postProcessor, result);
}
}
return result;
}
private static Object tryApplyPostProcessorForElement(
UnaryOperator<?> postProcessor,
Object value
) {
// TODO: Properly throw a ClassCastException
// TODO: Add test: type of element does not match type postprocessor expects
@SuppressWarnings("unchecked")
final var pp = (UnaryOperator<Object>) postProcessor;
return pp.apply(value);
}
private static void requireNonPrimitiveType(ConfigurationElement<?> element) {
if (element instanceof RecordComponentElement recordComponentElement) {
final RecordComponent component = recordComponentElement.element();
if (!component.getType().isPrimitive()) return;
String msg = ("Cannot set component '%s' of record type '%s' to null. " +
"Primitive types cannot be assigned null values.")
.formatted(component, component.getDeclaringRecord());
throw new ConfigurationException(msg);
}
if (element instanceof FieldElement fieldElement) {
final Field field = fieldElement.element();
if (!field.getType().isPrimitive()) return;
String msg = ("Cannot set field '%s' to null value. " +
"Primitive types cannot be assigned null.")
.formatted(field);
throw new ConfigurationException(msg);
}
throw new ConfigurationException("Unhandled ConfigurationElement: " + element);
}
final UnaryOperator<T> createPostProcessorFromAnnotatedMethod() { final UnaryOperator<T> createPostProcessorFromAnnotatedMethod() {
final List<Method> list = Arrays.stream(type.getDeclaredMethods()) final List<Method> list = Arrays.stream(type.getDeclaredMethods())
.filter(Predicate.not(Method::isSynthetic))
.filter(method -> method.isAnnotationPresent(PostProcess.class)) .filter(method -> method.isAnnotationPresent(PostProcess.class))
.filter(Predicate.not(Method::isSynthetic))
.filter(this::isNotAccessorMethod)
.toList(); .toList();
if (list.isEmpty()) if (list.isEmpty())
@ -157,11 +250,27 @@ sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
}; };
} }
private boolean isNotAccessorMethod(Method method) {
if (!type.isRecord()) return true;
return Arrays.stream(type.getRecordComponents())
.map(RecordComponent::getName)
.noneMatch(s -> s.equals(method.getName()));
}
protected abstract void requireSerializableElements(); protected abstract void requireSerializableElements();
protected abstract String baseDeserializeExceptionMessage(E element, Object value); protected abstract String baseDeserializeExceptionMessage(E element, Object value);
protected abstract List<E> elements(); protected abstract List<E> elements();
/**
* Returns the default value of a field or record component before any
* post-processing has been performed.
*
* @param element the configuration element
* @return the default value for that element
*/
protected abstract Object getDefaultValueOf(E element);
abstract T newDefaultInstance(); abstract T newDefaultInstance();
} }

@ -0,0 +1,119 @@
package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import org.junit.jupiter.api.Test;
import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConfigurationElementFilterTest {
static final class FieldElementHolder {
private final int x = 10;
@PostProcess
private final int y = 10;
@PostProcess(key = "key1")
private final int z = 10;
@PostProcess(key = "key2")
private final int w = 10;
private final double dbl = 10;
private final Object obj = new Object();
}
record RecordComponentElementHolder(
int x,
@PostProcess int y,
@PostProcess(key = "key1") int z,
@PostProcess(key = "key2") int w,
double dbl,
Object obj
) {}
private static final Class<?> FE_HOLDER_TYPE = FieldElementHolder.class;
private static final Class<?> RCE_HOLDER_TYPE = RecordComponentElementHolder.class;
private static final FieldElement FE_X;
private static final FieldElement FE_Y;
private static final FieldElement FE_Z;
private static final FieldElement FE_W;
private static final FieldElement FE_DBL;
private static final FieldElement FE_OBJ;
private static final RecordComponentElement RCE_X;
private static final RecordComponentElement RCE_Y;
private static final RecordComponentElement RCE_Z;
private static final RecordComponentElement RCE_W;
private static final RecordComponentElement RCE_DBL;
private static final RecordComponentElement RCE_OBJ;
static {
try {
FE_X = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("x"));
FE_Y = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("y"));
FE_Z = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("z"));
FE_W = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("w"));
FE_DBL = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("dbl"));
FE_OBJ = new FieldElement(FE_HOLDER_TYPE.getDeclaredField("obj"));
RCE_X = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[0]);
RCE_Y = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[1]);
RCE_Z = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[2]);
RCE_W = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[3]);
RCE_DBL = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[4]);
RCE_OBJ = new RecordComponentElement(RCE_HOLDER_TYPE.getRecordComponents()[5]);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
@Test
void byPostProcessKeyRequiresNonNull() {
assertThrowsNullPointerException(
() -> ConfigurationElementFilter.byPostProcessKey(null),
"post-process key"
);
}
@Test
void byPostProcessKeyOnFields() throws NoSuchFieldException {
final var filter = ConfigurationElementFilter.byPostProcessKey("key2");
assertFalse(filter.test(FE_X));
assertFalse(filter.test(FE_Y));
assertFalse(filter.test(FE_Z));
assertTrue(filter.test(FE_W));
assertFalse(filter.test(FE_DBL));
assertFalse(filter.test(FE_OBJ));
}
@Test
void byTypeOnFields() {
final var filter = ConfigurationElementFilter.byType(int.class);
assertTrue(filter.test(FE_X));
assertTrue(filter.test(FE_Y));
assertTrue(filter.test(FE_Z));
assertTrue(filter.test(FE_W));
assertFalse(filter.test(FE_DBL));
assertFalse(filter.test(FE_OBJ));
}
@Test
void byPostProcessKeyOnRecordComponents() {
final var filter = ConfigurationElementFilter.byPostProcessKey("key2");
assertFalse(filter.test(RCE_X));
assertFalse(filter.test(RCE_Y));
assertFalse(filter.test(RCE_Z));
assertTrue(filter.test(RCE_W));
assertFalse(filter.test(RCE_DBL));
assertFalse(filter.test(RCE_OBJ));
}
@Test
void byTypeOnRecordComponents() {
final var filter = ConfigurationElementFilter.byType(Object.class);
assertFalse(filter.test(RCE_X));
assertFalse(filter.test(RCE_Y));
assertFalse(filter.test(RCE_Z));
assertFalse(filter.test(RCE_W));
assertFalse(filter.test(RCE_DBL));
assertTrue(filter.test(RCE_OBJ));
}
}

@ -8,6 +8,7 @@ import java.awt.Point;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException; import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -18,11 +19,14 @@ class ConfigurationPropertiesTest {
private static final NameFormatter FORMATTER = String::toLowerCase; private static final NameFormatter FORMATTER = String::toLowerCase;
private static final FieldFilter FILTER = field -> field.getName().startsWith("f"); private static final FieldFilter FILTER = field -> field.getName().startsWith("f");
private static final PointSerializer SERIALIZER = new PointSerializer(); private static final PointSerializer SERIALIZER = new PointSerializer();
private static final Predicate<? super Type> PREDICATE = type -> true; private static final Predicate<? super Type> PREDICATE_TYPE = type -> true;
private static final Predicate<? super ConfigurationElement<?>> PREDICATE_CE = ce -> true;
private static final UnaryOperator<?> POST_PROCESSOR = UnaryOperator.identity();
private static final ConfigurationProperties.Builder<?> BUILDER = ConfigurationProperties.newBuilder() private static final ConfigurationProperties.Builder<?> BUILDER = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, SERIALIZER) .addSerializer(Point.class, SERIALIZER)
.addSerializerFactory(Point.class, ignored -> SERIALIZER) .addSerializerFactory(Point.class, ignored -> SERIALIZER)
.addSerializerByCondition(PREDICATE, SERIALIZER) .addSerializerByCondition(PREDICATE_TYPE, SERIALIZER)
.addPostProcessor(PREDICATE_CE, POST_PROCESSOR)
.setNameFormatter(FORMATTER) .setNameFormatter(FORMATTER)
.setFieldFilter(FILTER) .setFieldFilter(FILTER)
.outputNulls(true) .outputNulls(true)
@ -57,7 +61,8 @@ class ConfigurationPropertiesTest {
private static void assertConfigurationProperties(ConfigurationProperties properties) { private static void assertConfigurationProperties(ConfigurationProperties properties) {
assertThat(properties.getSerializers(), is(Map.of(Point.class, SERIALIZER))); assertThat(properties.getSerializers(), is(Map.of(Point.class, SERIALIZER)));
assertThat(properties.getSerializersByCondition(), is(Map.of(PREDICATE, SERIALIZER))); assertThat(properties.getSerializersByCondition(), is(Map.of(PREDICATE_TYPE, SERIALIZER)));
assertThat(properties.getPostProcessorsByCondition(), is(Map.of(PREDICATE_CE, POST_PROCESSOR)));
assertThat(properties.outputNulls(), is(true)); assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true)); assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false)); assertThat(properties.serializeSetsAsLists(), is(false));
@ -90,6 +95,17 @@ class ConfigurationPropertiesTest {
); );
} }
@Test
void builderPostProcessorsUnmodifiable() {
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
var postProcessorsByCondition = properties.getPostProcessorsByCondition();
assertThrows(
UnsupportedOperationException.class,
() -> postProcessorsByCondition.put(t -> true, UnaryOperator.identity())
);
}
public static final class BuilderTest { public static final class BuilderTest {
private static final ConfigurationProperties.Builder<?> builder = ConfigurationProperties.newBuilder(); private static final ConfigurationProperties.Builder<?> builder = ConfigurationProperties.newBuilder();
@ -147,5 +163,18 @@ class ConfigurationPropertiesTest {
"serializer" "serializer"
); );
} }
@Test
void addPostProcessorByConditionRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.addPostProcessor(null, UnaryOperator.identity()),
"condition"
);
assertThrowsNullPointerException(
() -> builder.addPostProcessor(type -> true, null),
"post-processor"
);
}
} }
} }

@ -8,6 +8,7 @@ import de.exlll.configlib.configurations.ExampleInitializer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Point; import java.awt.Point;
import java.lang.reflect.Field;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
@ -18,6 +19,7 @@ import java.util.function.Consumer;
import static de.exlll.configlib.TestUtils.*; import static de.exlll.configlib.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@SuppressWarnings("FieldMayBeFinal") @SuppressWarnings("FieldMayBeFinal")
@ -63,13 +65,14 @@ class ConfigurationSerializerTest {
); );
} }
@Test
void serializeAppliesFormatter() {
@Configuration @Configuration
class A { static final class A {
int value1 = 1; int value1 = 1;
int someValue2 = 2; int someValue2 = 2;
} }
@Test
void serializeAppliesFormatter() {
ConfigurationSerializer<A> serializer = newSerializer( ConfigurationSerializer<A> serializer = newSerializer(
A.class, A.class,
builder -> builder.setNameFormatter(NameFormatters.UPPER_UNDERSCORE) builder -> builder.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
@ -225,6 +228,35 @@ class ConfigurationSerializerTest {
int j = 2; int j = 2;
} }
@Configuration
static final class B14 {
private int i1;
private int i2 = 10;
private String s1;
private String s2 = null;
private String s3 = "s3";
}
@Test
void getDefaultValueOf() {
final var serializer = newSerializer(B14.class);
assertThat(serializer.getDefaultValueOf(fieldElementFor(B14.class, "i1")), is(0));
assertThat(serializer.getDefaultValueOf(fieldElementFor(B14.class, "i2")), is(10));
assertThat(serializer.getDefaultValueOf(fieldElementFor(B14.class, "s1")), nullValue());
assertThat(serializer.getDefaultValueOf(fieldElementFor(B14.class, "s2")), nullValue());
assertThat(serializer.getDefaultValueOf(fieldElementFor(B14.class, "s3")), is("s3"));
}
private static ConfigurationElements.FieldElement fieldElementFor(Class<?> type, String fieldName) {
try {
final Field field = type.getDeclaredField(fieldName);
return new ConfigurationElements.FieldElement(field);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
@Configuration @Configuration
static final class B9 { static final class B9 {
int i; int i;
@ -311,4 +343,346 @@ class ConfigurationSerializerTest {
assertThat(b13.b12.j, is(7)); assertThat(b13.b12.j, is(7));
assertThat(b13.b12.b11.k, is(26)); assertThat(b13.b12.b11.k, is(26));
} }
@Configuration
static final class PP_1 {
@PostProcess(key = "key1")
private int a1 = 10;
@PostProcess(key = "key1")
private int a2 = 20;
@PostProcess(key = "key2")
private int b1 = 10;
@PostProcess(key = "key2")
private int b2 = 20;
@PostProcess
private int c1 = 10;
@PostProcess
private int c2 = 20;
private int d1 = 10;
private int d2 = 20;
}
@Test
void postProcessFieldByKey1() {
final var serializer = newSerializer(
PP_1.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key1"),
(Integer x) -> x * 2
)
);
PP_1 deserialized = serializer.deserialize(Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
));
assertThat(deserialized.a1, is(20));
assertThat(deserialized.a2, is(40));
assertThat(deserialized.b1, is(10));
assertThat(deserialized.b2, is(20));
assertThat(deserialized.c1, is(10));
assertThat(deserialized.c2, is(20));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
@Test
void postProcessFieldByEmptyKey() {
final var serializer = newSerializer(
PP_1.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey(""),
(Integer x) -> x * 2
)
);
PP_1 deserialized = serializer.deserialize(Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
));
assertThat(deserialized.a1, is(10));
assertThat(deserialized.a2, is(20));
assertThat(deserialized.b1, is(10));
assertThat(deserialized.b2, is(20));
assertThat(deserialized.c1, is(20));
assertThat(deserialized.c2, is(40));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
@Configuration
static final class PP_2 {
private PP_1 pp1_1 = new PP_1();
@PostProcess(key = "key3")
private PP_1 pp1_2 = new PP_1();
}
@Test
void postProcessNestedFieldByKey2And3() {
final var serializer = newSerializer(
PP_2.class,
builder -> builder
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key2"),
(Integer x) -> x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key3"),
(PP_1 pp1) -> {
pp1.a1 *= 10;
pp1.a2 *= 10;
pp1.b1 *= 10;
pp1.b2 *= 10;
pp1.c1 *= 10;
pp1.c2 *= 10;
pp1.d1 *= 10;
pp1.d2 *= 10;
return pp1;
}
)
);
PP_2 deserialized = serializer.deserialize(Map.of(
"pp1_1", Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
),
"pp1_2", Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
)
));
assertThat(deserialized.pp1_1.a1, is(10));
assertThat(deserialized.pp1_1.a2, is(20));
assertThat(deserialized.pp1_1.b1, is(20));
assertThat(deserialized.pp1_1.b2, is(40));
assertThat(deserialized.pp1_1.c1, is(10));
assertThat(deserialized.pp1_1.c2, is(20));
assertThat(deserialized.pp1_1.d1, is(10));
assertThat(deserialized.pp1_1.d2, is(20));
assertThat(deserialized.pp1_2.a1, is(100));
assertThat(deserialized.pp1_2.a2, is(200));
assertThat(deserialized.pp1_2.b1, is(200));
assertThat(deserialized.pp1_2.b2, is(400));
assertThat(deserialized.pp1_2.c1, is(100));
assertThat(deserialized.pp1_2.c2, is(200));
assertThat(deserialized.pp1_2.d1, is(100));
assertThat(deserialized.pp1_2.d2, is(200));
}
@Test
void postProcessFieldDefaultValueIfSerializationMissing() {
final var serializer = newSerializer(
PP_1.class,
builder -> builder
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key1"),
(Integer x) -> x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key2"),
(Integer x) -> x * 5
)
);
PP_1 deserialized = serializer.deserialize(Map.of(
"a1", 700,
"b1", 800
));
assertThat(deserialized.a1, is(1400));
assertThat(deserialized.a2, is(40));
assertThat(deserialized.b1, is(4000));
assertThat(deserialized.b2, is(100));
assertThat(deserialized.c1, is(10));
assertThat(deserialized.c2, is(20));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
@Configuration
static final class PP_3 {
@PostProcess
private int i;
@PostProcess
private void postProcess() {
i++;
}
}
@Test
void postProcessMethodAppliedAfterPostProcessAnnotation() {
final var serializer = newSerializer(
PP_3.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey(""),
(Integer x) -> x * 2
)
);
PP_3 deserialized = serializer.deserialize(Map.of("i", 10));
assertThat(deserialized.i, is(21));
}
@Configuration
static final class PP_Ignored {
@PostProcess(key = "key1")
@Ignore
private int a1 = 10;
@PostProcess(key = "key1")
@Ignore
private int a2 = 20;
@PostProcess(key = "key2")
private final int b1 = 10;
@PostProcess(key = "key2")
private final int b2 = 20;
@PostProcess
private transient int c1 = 10;
@PostProcess
private transient int c2 = 20;
private int d1 = 10;
private int d2 = 20;
}
@Test
void ignoredFieldsAreNotPostProcessed() {
final var serializer = newSerializer(
PP_Ignored.class,
builder -> builder
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key1"),
(Integer x) -> x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key2"),
(Integer x) -> x * 5
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey(""),
(Integer x) -> x * 7
)
);
PP_Ignored deserialized = serializer.deserialize(Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
));
assertThat(deserialized.a1, is(10));
assertThat(deserialized.a2, is(20));
assertThat(deserialized.b1, is(10));
assertThat(deserialized.b2, is(20));
assertThat(deserialized.c1, is(10));
assertThat(deserialized.c2, is(20));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
@Configuration
static final class PP_Null {
@PostProcess(key = "integer")
private Integer i1;
@PostProcess(key = "integer")
private Integer i2;
@PostProcess(key = "integer")
private Integer i3 = 1;
private Integer i4;
@PostProcess(key = "string")
private String s1;
@PostProcess(key = "string")
private String s2;
@PostProcess(key = "string")
private String s3 = "a";
private String s4;
}
@Test
void postProcessFieldsThatAreAssignedNullValues() {
final var serializer = newSerializer(
PP_Null.class,
builder -> builder
.inputNulls(true)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("integer"),
(Integer x) -> (x == null) ? -1 : x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("string"),
(String s) -> (s == null) ? "empty" : s.repeat(2)
)
);
PP_Null deserialized = serializer.deserialize(TestUtils.asMap(
"i1", null, "i4", null,
"s1", null, "s4", null
));
assertThat(deserialized.i1, is(-1));
assertThat(deserialized.i2, is(-1));
assertThat(deserialized.i3, is(2));
assertThat(deserialized.i4, nullValue());
assertThat(deserialized.s1, is("empty"));
assertThat(deserialized.s2, is("empty"));
assertThat(deserialized.s3, is("aa"));
assertThat(deserialized.s4, nullValue());
}
@Configuration
static final class PP_Null_2 {
@PostProcess(key = "integer")
private Integer i1 = 1;
@PostProcess(key = "integer")
private Integer i2 = 2;
@PostProcess(key = "integer")
private Integer i3 = null;
@PostProcess(key = "integer")
private Integer i4 = null;
@PostProcess(key = "string")
private String s1 = "a";
@PostProcess(key = "string")
private String s2 = "b";
@PostProcess(key = "string")
private String s3 = null;
@PostProcess(key = "string")
private String s4 = null;
}
@Test
void postProcessSerializedNullValuesWithInputNullsBeingFalse() {
final var serializer = newSerializer(
PP_Null_2.class,
builder -> builder
.inputNulls(false)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("integer"),
(Integer x) -> (x == null) ? -1 : x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("string"),
(String s) -> (s == null) ? "empty" : s.repeat(2)
)
);
PP_Null_2 deserialized = serializer.deserialize(TestUtils.asMap(
"i1", null, "i3", null,
"s1", null, "s3", null
));
assertThat(deserialized.i1, is(2));
assertThat(deserialized.i2, is(4));
assertThat(deserialized.i3, is(-1));
assertThat(deserialized.i4, is(-1));
assertThat(deserialized.s1, is("aa"));
assertThat(deserialized.s2, is("bb"));
assertThat(deserialized.s3, is("empty"));
assertThat(deserialized.s4, is("empty"));
}
} }

@ -22,19 +22,19 @@ class PolymorphicSerializerTest {
assertThat(DEFAULT_PROPERTY, is("type")); assertThat(DEFAULT_PROPERTY, is("type"));
} }
@Test
void serializeDoesNotAllowConfigurationElementWithSameNameAsProperty() {
@Polymorphic @Polymorphic
@Configuration @Configuration
class A { static final class A {
String type = ""; String type = "";
} }
@Configuration @Configuration
@Polymorphic(property = "prop") @Polymorphic(property = "prop")
class B { static final class B {
String prop = ""; String prop = "";
} }
@Test
void serializeDoesNotAllowConfigurationElementWithSameNameAsProperty() {
record Config(A a, B b, List<A> as) {} record Config(A a, B b, List<A> as) {}
var serializerA = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a")); var serializerA = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));

@ -1,5 +1,6 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Point; import java.awt.Point;
@ -159,7 +160,7 @@ class RecordSerializerTest {
} }
@Test @Test
void deserializeNullValuesAsNullIfInputNullsIsTrueFailsForPrimitiveFields() { void deserializeNullValuesAsNullIfInputNullsIsTrueFailsForPrimitiveRecordComponents() {
RecordSerializer<R2> serializer = newSerializer(R2.class, builder -> builder.inputNulls(true)); RecordSerializer<R2> serializer = newSerializer(R2.class, builder -> builder.inputNulls(true));
RecordComponent[] components = R2.class.getRecordComponents(); RecordComponent[] components = R2.class.getRecordComponents();
@ -230,6 +231,18 @@ class RecordSerializerTest {
assertThat(r.s, is("s")); assertThat(r.s, is("s"));
} }
@Test
void getDefaultValueOf() {
record R(int i, String s) {
R() {this(10, "s");}
}
final var rc1 = new RecordComponentElement(R.class.getRecordComponents()[0]);
final var rc2 = new RecordComponentElement(R.class.getRecordComponents()[1]);
final var serializer = newSerializer(R.class);
assertThat(serializer.getDefaultValueOf(rc1), is(0));
assertThat(serializer.getDefaultValueOf(rc2), nullValue());
}
@Test @Test
void postProcessorIsAppliedInRecordDeserializer() { void postProcessorIsAppliedInRecordDeserializer() {
record R(int i, String s) { record R(int i, String s) {
@ -280,4 +293,230 @@ class RecordSerializerTest {
assertThat(r1.r2.j, is(7)); assertThat(r1.r2.j, is(7));
assertThat(r1.r2.r3.k, is(26)); assertThat(r1.r2.r3.k, is(26));
} }
record RP(int x, int y) {
@PostProcess
private void doSth1() {}
@PostProcess
private void doSth2() {}
}
@Test
void recordWithMultiplePostProcessMethodsCausesException() {
assertThrowsConfigurationException(
() -> newSerializer(RP.class),
"""
Configuration types must not define more than one method for post-processing but \
type 'class de.exlll.configlib.RecordSerializerTest$RP' defines 2:
private void de.exlll.configlib.RecordSerializerTest$RP.doSth1()
private void de.exlll.configlib.RecordSerializerTest$RP.doSth2()\
"""
);
}
record RR_1(
@PostProcess(key = "key1")
int a1,
@PostProcess(key = "key1")
int a2,
@PostProcess(key = "key2")
int b1,
@PostProcess(key = "key2")
int b2,
@PostProcess
int c1,
@PostProcess
int c2,
int d1,
int d2
) {}
@Test
void postProcessRecordComponentByKey1() {
final var serializer = newSerializer(
RR_1.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key1"),
(Integer x) -> x * 2
)
);
RR_1 deserialized = serializer.deserialize(Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
));
assertThat(deserialized.a1, is(20));
assertThat(deserialized.a2, is(40));
assertThat(deserialized.b1, is(10));
assertThat(deserialized.b2, is(20));
assertThat(deserialized.c1, is(10));
assertThat(deserialized.c2, is(20));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
@Test
void postProcessRecordComponentByEmptyKey() {
final var serializer = newSerializer(
RR_1.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey(""),
(Integer x) -> x * 2
)
);
RR_1 deserialized = serializer.deserialize(Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
));
assertThat(deserialized.a1, is(10));
assertThat(deserialized.a2, is(20));
assertThat(deserialized.b1, is(10));
assertThat(deserialized.b2, is(20));
assertThat(deserialized.c1, is(20));
assertThat(deserialized.c2, is(40));
assertThat(deserialized.d1, is(10));
assertThat(deserialized.d2, is(20));
}
record RR_2(RR_1 pp1_1, @PostProcess(key = "key3") RR_1 pp1_2) {}
@Test
void postProcessNestedRecordComponentByKey2And3() {
final var serializer = newSerializer(
RR_2.class,
builder -> builder
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key2"),
(Integer x) -> x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key3"),
(RR_1 pp1) -> new RR_1(
pp1.a1 * 10,
pp1.a2 * 10,
pp1.b1 * 10,
pp1.b2 * 10,
pp1.c1 * 10,
pp1.c2 * 10,
pp1.d1 * 10,
pp1.d2 * 10
)
)
);
RR_2 deserialized = serializer.deserialize(Map.of(
"pp1_1", Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
),
"pp1_2", Map.of(
"a1", 10, "a2", 20,
"b1", 10, "b2", 20,
"c1", 10, "c2", 20,
"d1", 10, "d2", 20
)
));
assertThat(deserialized.pp1_1.a1, is(10));
assertThat(deserialized.pp1_1.a2, is(20));
assertThat(deserialized.pp1_1.b1, is(20));
assertThat(deserialized.pp1_1.b2, is(40));
assertThat(deserialized.pp1_1.c1, is(10));
assertThat(deserialized.pp1_1.c2, is(20));
assertThat(deserialized.pp1_1.d1, is(10));
assertThat(deserialized.pp1_1.d2, is(20));
assertThat(deserialized.pp1_2.a1, is(100));
assertThat(deserialized.pp1_2.a2, is(200));
assertThat(deserialized.pp1_2.b1, is(200));
assertThat(deserialized.pp1_2.b2, is(400));
assertThat(deserialized.pp1_2.c1, is(100));
assertThat(deserialized.pp1_2.c2, is(200));
assertThat(deserialized.pp1_2.d1, is(100));
assertThat(deserialized.pp1_2.d2, is(200));
}
@Test
void postProcessRecordComponentDefaultValueIfSerializationMissing() {
final var serializer = newSerializer(
RR_1.class,
builder -> builder
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key1"),
(Integer x) -> x + 20
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("key2"),
(Integer x) -> x + 500
)
);
RR_1 deserialized = serializer.deserialize(Map.of(
"a1", 700,
"b1", 800
));
assertThat(deserialized.a1, is(720));
assertThat(deserialized.a2, is(20));
assertThat(deserialized.b1, is(1300));
assertThat(deserialized.b2, is(500));
assertThat(deserialized.c1, is(0));
assertThat(deserialized.c2, is(0));
assertThat(deserialized.d1, is(0));
assertThat(deserialized.d2, is(0));
}
record RR_3(@PostProcess int i) {
@PostProcess
private RR_3 postProcess() {
return new RR_3(i + 1);
}
}
@Test
void postProcessMethodAppliedAfterPostProcessAnnotation() {
final var serializer = newSerializer(
RR_3.class,
builder -> builder.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey(""),
(Integer x) -> x * 2
)
);
RR_3 deserialized = serializer.deserialize(Map.of("i", 10));
assertThat(deserialized.i, is(21));
}
record RR_Null(
@PostProcess(key = "integer")
Integer i1,
Integer i2,
@PostProcess(key = "string")
String s1,
String s2
) {}
@Test
void postProcessRecordComponentsThatAreAssignedNullValues() {
final var serializer = newSerializer(
RR_Null.class,
builder -> builder
.inputNulls(true)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("integer"),
(Integer x) -> (x == null) ? -1 : x * 2
)
.addPostProcessor(
ConfigurationElementFilter.byPostProcessKey("string"),
(String s) -> (s == null) ? "empty" : s.repeat(2)
)
);
RR_Null deserialized = serializer.deserialize(Map.of());
assertThat(deserialized.i1, is(-1));
assertThat(deserialized.i2, nullValue());
assertThat(deserialized.s1, is("empty"));
assertThat(deserialized.s2, nullValue());
}
} }

@ -13,7 +13,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException; import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException;
@ -299,21 +298,19 @@ class TypeSerializerTest {
); );
} }
@Test
void postProcessMustReturnVoidOrSameType() {
@Configuration @Configuration
class E { static class E {
int i; int i;
@PostProcess @PostProcess
E postProcess() {return null;} E postProcess() {return null;}
} }
class F extends E { static class F extends E {
@Override @Override
@PostProcess @PostProcess
E postProcess() {return null;} E postProcess() {return null;}
} }
class G extends E { static class G extends E {
@Override @Override
@PostProcess @PostProcess
G postProcess() { G postProcess() {
@ -321,6 +318,9 @@ class TypeSerializerTest {
} }
} }
@Test
void postProcessMustReturnVoidOrSameType() {
// both of these are okay: // both of these are okay:
newTypeSerializer(E.class); newTypeSerializer(E.class);
newTypeSerializer(G.class); newTypeSerializer(G.class);
@ -331,31 +331,29 @@ class TypeSerializerTest {
The return type of post-processing methods must either be 'void' or the same \ The return type of post-processing methods must either be 'void' or the same \
type as the configuration type in which the post-processing method is defined. \ type as the configuration type in which the post-processing method is defined. \
The return type of the post-processing method of \ The return type of the post-processing method of \
type 'class de.exlll.configlib.TypeSerializerTest$1F' is neither 'void' nor 'F'.\ type 'class de.exlll.configlib.TypeSerializerTest$F' is neither 'void' nor 'F'.\
""" """
); );
} }
@Test
void postProcessorInvokesAnnotatedMethodWithVoidReturnType1() {
final AtomicInteger integer = new AtomicInteger(0);
@Configuration @Configuration
class H1 { static final class H1 {
int i; int i;
@PostProcess @PostProcess
void postProcess() {integer.set(20);} void postProcess() {i += 20;}
} }
@Test
void postProcessorInvokesAnnotatedMethodWithVoidReturnType1() {
final var serializer = newTypeSerializer(H1.class); final var serializer = newTypeSerializer(H1.class);
final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod();
final H1 h1_1 = new H1(); final H1 h1_1 = new H1();
final H1 h1_2 = postProcessor.apply(h1_1); final H1 h1_2 = postProcessor.apply(h1_1);
assertThat(h1_2, sameInstance(h1_2)); assertThat(h1_2, sameInstance(h1_1));
assertThat(integer.get(), is(20)); assertThat(h1_2.i, is(20));
} }
static int postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int = 0; static int postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int = 0;
@ -379,10 +377,8 @@ class TypeSerializerTest {
assertThat(postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int, is(10)); assertThat(postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int, is(10));
} }
@Test
void postProcessorInvokesAnnotatedMethodWithSameReturnType1() {
@Configuration @Configuration
class H3 { static final class H3 {
int i; int i;
@PostProcess @PostProcess
@ -393,6 +389,8 @@ class TypeSerializerTest {
} }
} }
@Test
void postProcessorInvokesAnnotatedMethodWithSameReturnType1() {
final var serializer = newTypeSerializer(H3.class); final var serializer = newTypeSerializer(H3.class);
final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod();
@ -425,12 +423,13 @@ class TypeSerializerTest {
assertThat(h4_2.i, is(30)); assertThat(h4_2.i, is(30));
} }
@Test
void postProcessorIsIdentityFunctionIfNoPostProcessAnnotationPresent() {
@Configuration @Configuration
class J { static final class J {
int i; int i;
} }
@Test
void postProcessorIsIdentityFunctionIfNoPostProcessAnnotationPresent() {
final var serializer = newTypeSerializer(J.class); final var serializer = newTypeSerializer(J.class);
final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod();
@ -439,10 +438,8 @@ class TypeSerializerTest {
assertThat(j_2, sameInstance(j_1)); assertThat(j_2, sameInstance(j_1));
} }
@Test
void postProcessOfParentClassNotCalled() {
@Configuration @Configuration
class A { static class A {
int i = 10; int i = 10;
@PostProcess @PostProcess
@ -451,8 +448,10 @@ class TypeSerializerTest {
} }
} }
class B extends A {} static final class B extends A {}
@Test
void postProcessOfParentClassNotCalled() {
final var serializer = newTypeSerializer(B.class); final var serializer = newTypeSerializer(B.class);
final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod();

@ -210,7 +210,7 @@ public final class TestUtils {
// Suppressing this warning might lead to an exception. // Suppressing this warning might lead to an exception.
// Using proper generics for the MEntry class is possible. // Using proper generics for the MEntry class is possible.
// However, doing so increases the compilation type by several seconds // However, doing so increases the compilation time by several seconds
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<K, V> returnResult = (Map<K, V>) result; Map<K, V> returnResult = (Map<K, V>) result;
return returnResult; return returnResult;

Loading…
Cancel
Save