From 31d4d09e8515447cee52b37092ece29194017497 Mon Sep 17 00:00:00 2001 From: Exlll Date: Thu, 8 Feb 2024 22:47:50 +0100 Subject: [PATCH] WIP: Add support for post-processing via annotated configuration elements --- .../configlib/ConfigurationElementFilter.java | 57 +++ .../configlib/ConfigurationProperties.java | 75 +++- .../configlib/ConfigurationSerializer.java | 42 +- .../java/de/exlll/configlib/PostProcess.java | 2 +- .../de/exlll/configlib/RecordSerializer.java | 43 +- .../de/exlll/configlib/TypeSerializer.java | 111 ++++- .../ConfigurationElementFilterTest.java | 119 ++++++ .../ConfigurationPropertiesTest.java | 35 +- .../ConfigurationSerializerTest.java | 384 +++++++++++++++++- .../configlib/PolymorphicSerializerTest.java | 22 +- .../exlll/configlib/RecordSerializerTest.java | 241 ++++++++++- .../exlll/configlib/TypeSerializerTest.java | 117 +++--- .../java/de/exlll/configlib/TestUtils.java | 2 +- 13 files changed, 1092 insertions(+), 158 deletions(-) create mode 100644 configlib-core/src/main/java/de/exlll/configlib/ConfigurationElementFilter.java create mode 100644 configlib-core/src/test/java/de/exlll/configlib/ConfigurationElementFilterTest.java diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationElementFilter.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationElementFilter.java new file mode 100644 index 0000000..0c2ac8d --- /dev/null +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationElementFilter.java @@ -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> { + @Override + default ConfigurationElementFilter and( + Predicate> 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); + }; + } +} diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java index ae4436d..17019e4 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java @@ -7,6 +7,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; 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. */ public class ConfigurationProperties { - private final Map, Serializer> serializersByType; + private final Map, Serializer> + serializersByType; private final Map, Function>> serializerFactoriesByType; - private final Map, Serializer> serializersByCondition; + private final Map, Serializer> + serializersByCondition; + private final Map>, UnaryOperator> + postProcessorsByCondition; private final NameFormatter formatter; private final FieldFilter filter; private final boolean outputNulls; @@ -33,9 +38,12 @@ public class ConfigurationProperties { protected ConfigurationProperties(Builder builder) { this.serializersByType = Map.copyOf(builder.serializersByType); this.serializerFactoriesByType = Map.copyOf(builder.serializerFactoriesByType); - this.serializersByCondition = Collections.unmodifiableMap(new LinkedHashMap<>( - builder.serializersByCondition - )); + this.serializersByCondition = Collections.unmodifiableMap( + new LinkedHashMap<>(builder.serializersByCondition) + ); + this.postProcessorsByCondition = Collections.unmodifiableMap( + new LinkedHashMap<>(builder.postProcessorsByCondition) + ); this.formatter = requireNonNull(builder.formatter, "name formatter"); this.filter = requireNonNull(builder.filter, "field filter"); this.outputNulls = builder.outputNulls; @@ -79,11 +87,14 @@ public class ConfigurationProperties { * @param the type of builder */ public static abstract class Builder> { - private final Map, Serializer> serializersByType = new HashMap<>(); + private final Map, Serializer> + serializersByType = new HashMap<>(); private final Map, Function>> serializerFactoriesByType = new HashMap<>(); - private final Map, Serializer> serializersByCondition = - new LinkedHashMap<>(); + private final Map, Serializer> + serializersByCondition = new LinkedHashMap<>(); + private final Map>, UnaryOperator> + postProcessorsByCondition = new LinkedHashMap<>(); private NameFormatter formatter = NameFormatters.IDENTITY; private FieldFilter filter = FieldFilters.DEFAULT; private boolean outputNulls = false; @@ -96,6 +107,7 @@ public class ConfigurationProperties { this.serializersByType.putAll(properties.serializersByType); this.serializerFactoriesByType.putAll(properties.serializerFactoriesByType); this.serializersByCondition.putAll(properties.serializersByCondition); + this.postProcessorsByCondition.putAll(properties.postProcessorsByCondition); this.formatter = properties.formatter; this.filter = properties.filter; this.outputNulls = properties.outputNulls; @@ -133,8 +145,9 @@ public class ConfigurationProperties { /** * Adds a serializer for the given type. *

- * If this library already provides a serializer for the given type (e.g. {@code BigInteger}, - * {@code LocalDate}, etc.) the serializer added by this method takes precedence. + * If this library already provides a serializer for the given type + * (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer + * added by this method takes precedence. *

* If a factory is added via the {@link #addSerializerFactory(Class, Function)} method for * 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. *

- * If this library already provides a serializer for the given type (e.g. {@code BigInteger}, - * {@code LocalDate}, etc.) the serializer created by the factory takes precedence. + * If this library already provides a serializer for the given type + * (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer + * created by the factory takes precedence. *

* 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 @@ -204,6 +218,33 @@ public class ConfigurationProperties { 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. + *

+ * NOTE: + * 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> 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 * is null should be output while serializing the configuration. @@ -311,6 +352,16 @@ public class ConfigurationProperties { return serializersByCondition; } + /** + * Returns an unmodifiable map of post-processors by condition. + * + * @return post-processors by condition + */ + public final Map>, UnaryOperator> + getPostProcessorsByCondition() { + return postProcessorsByCondition; + } + /** * Returns whether null values should be output. * diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java index d19ab3e..b1ac4ab 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationSerializer.java @@ -2,7 +2,6 @@ package de.exlll.configlib; import de.exlll.configlib.ConfigurationElements.FieldElement; -import java.lang.reflect.Field; import java.util.List; import java.util.Map; @@ -13,26 +12,13 @@ final class ConfigurationSerializer extends TypeSerializer { @Override public T deserialize(Map serializedConfiguration) { - final T result = Reflect.callNoParamConstructor(type); - - for (final var element : elements()) { - final var formattedName = formatter.format(element.name()); - - if (!serializedConfiguration.containsKey(formattedName)) - 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); - } + final var deserializedElements = deserializeConfigurationElements(serializedConfiguration); + final var elements = elements(); + final T result = newDefaultInstance(); + for (int i = 0; i < deserializedElements.length; i++) { + final FieldElement fieldElement = elements.get(i); + Reflect.setValue(fieldElement.element(), result, deserializedElements[i]); } - return postProcessor.apply(result); } @@ -64,15 +50,15 @@ final class ConfigurationSerializer extends TypeSerializer { 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 getConfigurationType() { 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); + } } diff --git a/configlib-core/src/main/java/de/exlll/configlib/PostProcess.java b/configlib-core/src/main/java/de/exlll/configlib/PostProcess.java index 6bb9f3f..346f753 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/PostProcess.java +++ b/configlib-core/src/main/java/de/exlll/configlib/PostProcess.java @@ -22,7 +22,7 @@ import java.lang.annotation.Target; * method call. If the return type is {@code void}, then the method is simply * called on the given instance. */ -@Target({ElementType.FIELD, ElementType.METHOD}) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.RECORD_COMPONENT}) @Retention(RetentionPolicy.RUNTIME) public @interface PostProcess { String key() default ""; diff --git a/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java index fbca874..57839c6 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java +++ b/configlib-core/src/main/java/de/exlll/configlib/RecordSerializer.java @@ -2,7 +2,6 @@ package de.exlll.configlib; import de.exlll.configlib.ConfigurationElements.RecordComponentElement; -import java.lang.reflect.RecordComponent; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -14,32 +13,8 @@ final class RecordSerializer extends TypeSerializer serializedConfiguration) { - final var elements = elements(); - final var constructorArguments = 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)) { - 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); + final var ctorArgs = deserializeConfigurationElements(serializedConfiguration); + final var result = Reflect.callCanonicalConstructor(type, ctorArgs); return postProcessor.apply(result); } @@ -72,16 +47,12 @@ final class RecordSerializer extends TypeSerializer getRecordType() { return type; } + + @Override + protected Object getDefaultValueOf(RecordComponentElement element) { + return Reflect.getDefaultValue(element.type()); + } } diff --git a/configlib-core/src/main/java/de/exlll/configlib/TypeSerializer.java b/configlib-core/src/main/java/de/exlll/configlib/TypeSerializer.java index f1156cb..0844a30 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/TypeSerializer.java +++ b/configlib-core/src/main/java/de/exlll/configlib/TypeSerializer.java @@ -1,7 +1,12 @@ 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.Modifier; +import java.lang.reflect.RecordComponent; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -101,10 +106,98 @@ sealed abstract class TypeSerializer> 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) 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 createPostProcessorFromAnnotatedMethod() { final List list = Arrays.stream(type.getDeclaredMethods()) - .filter(Predicate.not(Method::isSynthetic)) .filter(method -> method.isAnnotationPresent(PostProcess.class)) + .filter(Predicate.not(Method::isSynthetic)) + .filter(this::isNotAccessorMethod) .toList(); if (list.isEmpty()) @@ -157,11 +250,27 @@ sealed abstract class TypeSerializer> }; } + 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 String baseDeserializeExceptionMessage(E element, Object value); protected abstract List 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(); } diff --git a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationElementFilterTest.java b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationElementFilterTest.java new file mode 100644 index 0000000..e102e50 --- /dev/null +++ b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationElementFilterTest.java @@ -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)); + } +} \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java index 7a75013..9bf5406 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java @@ -8,6 +8,7 @@ import java.awt.Point; import java.lang.reflect.Type; import java.util.Map; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException; import static org.hamcrest.MatcherAssert.assertThat; @@ -18,11 +19,14 @@ class ConfigurationPropertiesTest { private static final NameFormatter FORMATTER = String::toLowerCase; private static final FieldFilter FILTER = field -> field.getName().startsWith("f"); private static final PointSerializer SERIALIZER = new PointSerializer(); - private static final Predicate PREDICATE = type -> true; + private static final Predicate PREDICATE_TYPE = type -> true; + private static final Predicate> PREDICATE_CE = ce -> true; + private static final UnaryOperator POST_PROCESSOR = UnaryOperator.identity(); private static final ConfigurationProperties.Builder BUILDER = ConfigurationProperties.newBuilder() .addSerializer(Point.class, SERIALIZER) .addSerializerFactory(Point.class, ignored -> SERIALIZER) - .addSerializerByCondition(PREDICATE, SERIALIZER) + .addSerializerByCondition(PREDICATE_TYPE, SERIALIZER) + .addPostProcessor(PREDICATE_CE, POST_PROCESSOR) .setNameFormatter(FORMATTER) .setFieldFilter(FILTER) .outputNulls(true) @@ -57,7 +61,8 @@ class ConfigurationPropertiesTest { private static void assertConfigurationProperties(ConfigurationProperties properties) { 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.inputNulls(), is(true)); 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 { private static final ConfigurationProperties.Builder builder = ConfigurationProperties.newBuilder(); @@ -147,5 +163,18 @@ class ConfigurationPropertiesTest { "serializer" ); } + + @Test + void addPostProcessorByConditionRequiresNonNull() { + assertThrowsNullPointerException( + () -> builder.addPostProcessor(null, UnaryOperator.identity()), + "condition" + ); + + assertThrowsNullPointerException( + () -> builder.addPostProcessor(type -> true, null), + "post-processor" + ); + } } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java index 2805085..89ad665 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationSerializerTest.java @@ -8,6 +8,7 @@ import de.exlll.configlib.configurations.ExampleInitializer; import org.junit.jupiter.api.Test; import java.awt.Point; +import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; @@ -18,6 +19,7 @@ import java.util.function.Consumer; import static de.exlll.configlib.TestUtils.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.*; @SuppressWarnings("FieldMayBeFinal") @@ -63,13 +65,14 @@ class ConfigurationSerializerTest { ); } + @Configuration + static final class A { + int value1 = 1; + int someValue2 = 2; + } + @Test void serializeAppliesFormatter() { - @Configuration - class A { - int value1 = 1; - int someValue2 = 2; - } ConfigurationSerializer serializer = newSerializer( A.class, builder -> builder.setNameFormatter(NameFormatters.UPPER_UNDERSCORE) @@ -225,6 +228,35 @@ class ConfigurationSerializerTest { 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 static final class B9 { int i; @@ -311,4 +343,346 @@ class ConfigurationSerializerTest { assertThat(b13.b12.j, is(7)); 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")); + } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java index 0221f15..85be4f7 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/PolymorphicSerializerTest.java @@ -22,19 +22,19 @@ class PolymorphicSerializerTest { assertThat(DEFAULT_PROPERTY, is("type")); } + @Polymorphic + @Configuration + static final class A { + String type = ""; + } + @Configuration + @Polymorphic(property = "prop") + static final class B { + String prop = ""; + } + @Test void serializeDoesNotAllowConfigurationElementWithSameNameAsProperty() { - @Polymorphic - @Configuration - class A { - String type = ""; - } - @Configuration - @Polymorphic(property = "prop") - class B { - String prop = ""; - } - record Config(A a, B b, List as) {} var serializerA = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a")); diff --git a/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java index b5698ab..1457a63 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/RecordSerializerTest.java @@ -1,5 +1,6 @@ package de.exlll.configlib; +import de.exlll.configlib.ConfigurationElements.RecordComponentElement; import org.junit.jupiter.api.Test; import java.awt.Point; @@ -159,7 +160,7 @@ class RecordSerializerTest { } @Test - void deserializeNullValuesAsNullIfInputNullsIsTrueFailsForPrimitiveFields() { + void deserializeNullValuesAsNullIfInputNullsIsTrueFailsForPrimitiveRecordComponents() { RecordSerializer serializer = newSerializer(R2.class, builder -> builder.inputNulls(true)); RecordComponent[] components = R2.class.getRecordComponents(); @@ -230,6 +231,18 @@ class RecordSerializerTest { 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 void postProcessorIsAppliedInRecordDeserializer() { record R(int i, String s) { @@ -280,4 +293,230 @@ class RecordSerializerTest { assertThat(r1.r2.j, is(7)); 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()); + } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java b/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java index eead155..16cc1b5 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException; @@ -299,28 +298,29 @@ class TypeSerializerTest { ); } - @Test - void postProcessMustReturnVoidOrSameType() { - @Configuration - class E { - int i; + @Configuration + static class E { + int i; - @PostProcess - E postProcess() {return null;} - } - class F extends E { - @Override - @PostProcess - E postProcess() {return null;} - } - class G extends E { - @Override - @PostProcess - G postProcess() { - return null; - } + @PostProcess + E postProcess() {return null;} + } + static class F extends E { + @Override + @PostProcess + E postProcess() {return null;} + } + static class G extends E { + @Override + @PostProcess + G postProcess() { + return null; } + } + + @Test + void postProcessMustReturnVoidOrSameType() { // both of these are okay: newTypeSerializer(E.class); newTypeSerializer(G.class); @@ -331,31 +331,29 @@ class TypeSerializerTest { 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. \ 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 - class H1 { - int i; + @Configuration + static final class H1 { + int i; - @PostProcess - void postProcess() {integer.set(20);} - } + @PostProcess + void postProcess() {i += 20;} + } + @Test + void postProcessorInvokesAnnotatedMethodWithVoidReturnType1() { final var serializer = newTypeSerializer(H1.class); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); final H1 h1_1 = new H1(); final H1 h1_2 = postProcessor.apply(h1_1); - assertThat(h1_2, sameInstance(h1_2)); - assertThat(integer.get(), is(20)); + assertThat(h1_2, sameInstance(h1_1)); + assertThat(h1_2.i, is(20)); } static int postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int = 0; @@ -379,20 +377,20 @@ class TypeSerializerTest { assertThat(postProcessorInvokesAnnotatedMethodWithVoidReturnType2_int, is(10)); } - @Test - void postProcessorInvokesAnnotatedMethodWithSameReturnType1() { - @Configuration - class H3 { - int i; - - @PostProcess - H3 postProcess() { - H3 h3 = new H3(); - h3.i = i + 20; - return h3; - } + @Configuration + static final class H3 { + int i; + + @PostProcess + H3 postProcess() { + H3 h3 = new H3(); + h3.i = i + 20; + return h3; } + } + @Test + void postProcessorInvokesAnnotatedMethodWithSameReturnType1() { final var serializer = newTypeSerializer(H3.class); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); @@ -425,12 +423,13 @@ class TypeSerializerTest { assertThat(h4_2.i, is(30)); } + @Configuration + static final class J { + int i; + } + @Test void postProcessorIsIdentityFunctionIfNoPostProcessAnnotationPresent() { - @Configuration - class J { - int i; - } final var serializer = newTypeSerializer(J.class); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); @@ -439,20 +438,20 @@ class TypeSerializerTest { assertThat(j_2, sameInstance(j_1)); } - @Test - void postProcessOfParentClassNotCalled() { - @Configuration - class A { - int i = 10; + @Configuration + static class A { + int i = 10; - @PostProcess - void postProcess() { - this.i = this.i + 10; - } + @PostProcess + void postProcess() { + this.i = this.i + 10; } + } - class B extends A {} + static final class B extends A {} + @Test + void postProcessOfParentClassNotCalled() { final var serializer = newTypeSerializer(B.class); final var postProcessor = serializer.createPostProcessorFromAnnotatedMethod(); diff --git a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java index 2ab1d3c..eb8524e 100644 --- a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java +++ b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java @@ -210,7 +210,7 @@ public final class TestUtils { // Suppressing this warning might lead to an exception. // 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") Map returnResult = (Map) result; return returnResult;