Add support for polymorphic serialization

This commit adds the Polymorphic annotation that can be used on types.
Serializers for polymorphic types are not selected based on the
compile-time types of configuration elements, but instead are chosen at
runtime based on the actual types of their values. This enables adding
instances of subclasses / implementations of a polymorphic type to
collections.
dev
Exlll 2 years ago
parent 30430527a1
commit 9f4999c726

@ -541,10 +541,11 @@ instances of `B` (or some other subclass of `A`) in it.
</details>
You can override the default selection by annotating a configuration
element with [`@SerializeWith`](#the-serializewith-annotation) or by adding your own serializer
for `S` to the configuration properties. When you do so, it can happen that there multiple
serializers available for a particular configuration element and its type. In that case, one of them
chosen according to the following precedence rules:
element with [`@SerializeWith`](#the-serializewith-annotation), by annotating a type
with `@SerializeWith`, or by adding your own serializer for `S` to the configuration properties.
When you do so, it can happen that there multiple serializers available for a particular
configuration element and its type. In that case, one of them chosen according to the following
precedence rules:
1. If the element is annotated with `@SerializeWith` and the `nesting` matches, the serializer
referenced by the annotation is selected.
@ -552,28 +553,38 @@ chosen according to the following precedence rules:
serializer is returned.
* Serializers created by factories that were added through `addSerializerFactory` for some type
take precedence over serializers added by `addSerializer` for the same type.
3. Otherwise, if this library defines a serializer for that type, that serializer is selected.
4. Ultimately, if no serializer can be found, an exception is thrown.
3. If the type is annotated `@SerializeWith`, the serializer referenced by the annotation is
selected.
4. If the type is annotated with an annotation which is annotated with `@SerializeWith`, the
serializer referenced by `@SerializeWith` is returned.
5. If this library defines a serializer for that type, that serializer is selected.
6. Ultimately, if no serializer can be found, an exception is thrown.
For lists, sets, and maps, the algorithm is recursively applied to their generic type arguments
recursively first.
For lists, sets, and maps, the algorithm is applied to their generic type arguments recursively
first.
##### The `SerializeWith` annotation
The `SerializeWith` annotation can be applied to configuration elements (i.e. class fields and
record components) and enforces the use of the specified serializer for that element.
The `SerializeWith` annotation enforces the use of the specified serializer for a configuration
element or type. It can be applied to configuration elements (i.e. class fields and
record components), to types, and to other annotations.
```java
@SerializeWith(serializer = MyPointSerializer.class)
Point point;
```
The serializer referenced by this annotation is selected regardless of whether the type of the
configuration element matches the type the serializer expects.
```java
@SerializeWith(serializer = SomeClassSerializer.class)
public final class SomeClass {/* ... */}
```
The serializer referenced by this annotation is selected regardless of whether the annotated type or
type of configuration element matches the type the serializer expects.
If the configuration element is an array, list, set, or map a nesting level can be set to apply the
serializer not to the top-level type but to its elements. For maps, the serializer is applied to
the values and not the keys.
If the annotation is applied to a configuration element and that element is an array, list, set, or
map, a nesting level can be set to apply the serializer not to the top-level type but to its
elements. For maps, the serializer is applied to the values and not the keys.
```java
@SerializeWith(serializer = MySetSerializer.class, nesting = 1)
@ -581,7 +592,8 @@ List<Set<String>> list;
```
Setting `nesting` to an invalid value, i.e. a negative one or one that is greater than the number
of levels the element actually has, results in the serializer not being selected.
of levels the element actually has, results in the serializer not being selected. For type
annotations, the `nesting` has no effect.
<details>
<summary>More <code>nesting</code> examples</summary>
@ -643,10 +655,10 @@ interface you have to make sure that you convert your source type into one of th
listed in [type conversion](#type-conversion-and-serializer-selection) section.
The serializer then has to be registered through a `ConfigurationProperties` object or alternatively
be applied to a configuration element with [`@SerializeWith`](#the-serializewith-annotation). If you
want to use the `@SerializeWith` annotation, your serializer class must either have a constructor
with no parameters or one with exactly one parameter of
type [`SerializerContext`](#the-serializercontext-interface).
be applied to a configuration element or type
with [`@SerializeWith`](#the-serializewith-annotation). If you want to use the `@SerializeWith`
annotation, your serializer class must either have a constructor with no parameters or one with
exactly one parameter of type [`SerializerContext`](#the-serializercontext-interface).
The following `Serializer` serializes instances of `java.awt.Point` into strings and vice versa.

@ -6,7 +6,7 @@ import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import java.lang.reflect.AnnotatedElement;
import java.util.*;
import static de.exlll.configlib.Validator.requireConfigurationOrRecord;
import static de.exlll.configlib.Validator.requireConfigurationType;
import static de.exlll.configlib.Validator.requireNonNull;
final class CommentNodeExtractor {
@ -35,7 +35,7 @@ final class CommentNodeExtractor {
* @throws NullPointerException if {@code elementHolder} is null
*/
public Queue<CommentNode> extractCommentNodes(final Object elementHolder) {
requireConfigurationOrRecord(elementHolder.getClass());
requireConfigurationType(elementHolder.getClass());
final Queue<CommentNode> result = new ArrayDeque<>();
final var elementNameStack = new ArrayDeque<>(List.of(""));
final var stateStack = new ArrayDeque<>(List.of(stateFromObject(elementHolder)));
@ -61,9 +61,7 @@ final class CommentNodeExtractor {
commentNode.ifPresent(result::add);
final var elementType = element.type();
if ((elementValue != null) &&
(Reflect.isConfiguration(elementType) ||
elementType.isRecord())) {
if ((elementValue != null) && Reflect.isConfigurationType(elementType)) {
stateStack.addLast(state);
elementNameStack.addLast(nameFormatter.format(elementName));
state = stateFromObject(elementValue);

@ -8,7 +8,7 @@ import java.util.Map;
final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) {
super(Validator.requireConfiguration(configurationType), properties);
super(Validator.requireConfigurationClass(configurationType), properties);
}
@Override

@ -16,9 +16,9 @@ enum FieldExtractors implements FieldExtractor {
@Override
public Stream<Field> extract(Class<?> cls) {
Validator.requireNonNull(cls, "configuration class");
Validator.requireConfiguration(cls);
Validator.requireConfigurationClass(cls);
List<Class<?>> classes = extractClassesWhile(cls, Reflect::isConfiguration);
List<Class<?>> classes = extractClassesWhile(cls, Reflect::isConfigurationClass);
List<Field> fields = classes.stream()
.flatMap(c -> Arrays.stream(c.getDeclaredFields()))
.filter(FieldFilters.DEFAULT)

@ -0,0 +1,62 @@
package de.exlll.configlib;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated type is polymorphic. Serializers for polymorphic types are not
* selected based on the compile-time types of configuration elements, but instead are chosen at
* runtime based on the actual types of their values. This enables adding instances of subclasses /
* implementations of a polymorphic type to collections. The subtypes must be valid configurations.
* <p>
* For correct deserialization, if an instance of polymorphic type (or one of its implementations /
* subclasses) is serialized, an additional property that holds type information is added to its
* serialization.
*
* <pre>
* {@code
* // Example 1
* @Polymorphic
* @Configuration
* static abstract class A { ... }
*
* static final class Impl1 extends A { ... }
* static final class Impl2 extends A { ... }
*
* List<A> as = List.of(new Impl1(...), new Impl2(...), ...);
*
* // Example 2
* @Polymorphic
* interface B { ... }
*
* record Impl1() implements B { ... }
*
* @Configuration
* static final class Impl2 implements B { ... }
*
* List<B> bs = List.of(new Impl1(...), new Impl2(...), ...);
* }
* </pre>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SerializeWith(serializer = PolymorphicSerializer.class)
public @interface Polymorphic {
/**
* The default name of the property that holds the type information.
*/
String DEFAULT_PROPERTY = "type";
/**
* Returns the name of the property that holds the type information.
* <p>
* The property returned by this method must neither be blank nor be the
* name of a configuration element.
*
* @return name of the property that holds the type information
* @see String#isBlank()
*/
String property() default DEFAULT_PROPERTY;
}

@ -0,0 +1,113 @@
package de.exlll.configlib;
import java.util.LinkedHashMap;
import java.util.Map;
final class PolymorphicSerializer implements Serializer<Object, Map<?, ?>> {
private final SerializerContext context;
private final Class<?> polymorphicType;
private final Polymorphic annotation;
public PolymorphicSerializer(SerializerContext context) {
this.context = context;
// we know it's a class because of SerializerSelector#findMetaSerializerOnType
this.polymorphicType = (Class<?>) context.annotatedType().getType();
this.annotation = polymorphicType.getAnnotation(Polymorphic.class);
requireNonBlankProperty();
}
private void requireNonBlankProperty() {
if (annotation.property().isBlank()) {
String msg = "The @Polymorphic annotation does not allow a blank property name but " +
"type '%s' uses one.".formatted(polymorphicType.getName());
throw new ConfigurationException(msg);
}
}
@Override
public Map<?, ?> serialize(Object element) {
// this cast won't cause any exceptions as we only pass objects of types the
// serializer expects
@SuppressWarnings("unchecked")
final var serializer = (TypeSerializer<Object, ?>) TypeSerializer.newSerializerFor(
element.getClass(),
context.properties()
);
final var serialization = serializer.serialize(element);
requireSerializationNotContainsProperty(serialization);
final var result = new LinkedHashMap<>();
result.put(annotation.property(), element.getClass().getName());
result.putAll(serialization);
return result;
}
private void requireSerializationNotContainsProperty(Map<?, ?> serialization) {
if (serialization.containsKey(annotation.property())) {
String msg = ("Polymorphic serialization for type '%s' failed. The type contains a " +
"configuration element with name '%s' but that name is " +
"used by the @Polymorphic property.")
.formatted(polymorphicType.getName(), annotation.property());
throw new ConfigurationException(msg);
}
}
@Override
public Object deserialize(Map<?, ?> element) {
requirePropertyPresent(element);
final var typeIdentifier = element.get(annotation.property());
requireTypeIdentifierString(typeIdentifier);
Class<?> type = tryFindClass((String) typeIdentifier);
TypeSerializer<?, ?> serializer = TypeSerializer.newSerializerFor(type, context.properties());
return serializer.deserialize(element);
}
private Class<?> tryFindClass(String className) {
try {
return Reflect.getClassByName(className);
} catch (RuntimeException e) {
String msg = ("Polymorphic deserialization for type '%s' failed. " +
"The class '%s' does not exist.")
.formatted(polymorphicType.getName(), className);
throw new ConfigurationException(msg, e);
}
}
private void requirePropertyPresent(Map<?, ?> element) {
if (element.get(annotation.property()) != null)
return;
String msg = """
Polymorphic deserialization for type '%s' failed. \
The property '%s' which holds the type is missing. \
Value to be deserialized:
%s\
"""
.formatted(
polymorphicType.getName(),
annotation.property(),
element
);
throw new ConfigurationException(msg);
}
private void requireTypeIdentifierString(Object typeIdentifier) {
if (typeIdentifier instanceof String)
return;
String msg = ("Polymorphic deserialization for type '%s' failed. The type identifier '%s' " +
"which should hold the type is not a string but of type '%s'.")
.formatted(
polymorphicType.getName(),
typeIdentifier,
typeIdentifier.getClass().getName()
);
throw new ConfigurationException(msg);
}
Class<?> getPolymorphicType() {
return polymorphicType;
}
}

@ -223,11 +223,23 @@ final class Reflect {
return Map.class.isAssignableFrom(cls);
}
static boolean isConfiguration(Class<?> cls) {
static boolean isConfigurationClass(Class<?> cls) {
return cls.getAnnotation(Configuration.class) != null;
}
static boolean isConfigurationType(Class<?> type) {
return type.isRecord() || (type.getAnnotation(Configuration.class) != null);
}
static boolean isIgnored(Field field) {
return field.getAnnotation(Ignore.class) != null;
}
static Class<?> getClassByName(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

@ -3,12 +3,12 @@ package de.exlll.configlib;
import java.lang.annotation.*;
/**
* Indicates that the annotated element should be serialized with the given serializer. Serializers
* provided by this annotation take precedence over all other serializers.
* Indicates that the annotated configuration element or type should be serialized using the
* referenced serializer.
* <p>
* If the annotated element is an array, list, set, or map a nesting level can be set to apply the
* serializer not to the top-level type but to its elements. For maps, the serializer is applied to
* the values and not the keys.
* If this annotation is applied to a configuration element, and that element is an array, list,
* set, or map a nesting level can be set to apply the serializer not to the top-level type but to
* its elements. For maps, the serializer is applied to the values and not the keys.
* <p>
* The following example shows how {@code nesting} can be used to apply the serializer at
* different levels.
@ -58,6 +58,9 @@ public @interface SerializeWith {
/**
* Returns the nesting level at which to apply the serializer.
* <p>
* If this annotation is applied to a type or another annotation, the value
* returned by this method has no effect.
*
* @return the nesting level
*/

@ -194,21 +194,14 @@ final class SerializerSelector {
final var enumType = (Class<? extends Enum<?>>) cls;
return new Serializers.EnumSerializer(enumType);
}
if (Reflect.isArrayType(cls)) {
if (Reflect.isArrayType(cls))
return selectForArray((AnnotatedArrayType) annotatedType);
}
if (cls.isRecord()) {
// The following cast won't fail because we just checked that it's a record.
@SuppressWarnings("unchecked")
final var recordType = (Class<? extends Record>) cls;
return new RecordSerializer<>(recordType, properties);
}
if (Reflect.isConfiguration(cls))
return new ConfigurationSerializer<>(cls, properties);
if (Reflect.isConfigurationType(cls))
return TypeSerializer.newSerializerFor(cls, properties);
String msg = "Missing serializer for type " + cls + ".\n" +
"Either annotate the type with @Configuration or provide a custom " +
"serializer by adding it to the properties.";
"Either annotate the type with @Configuration, make it a Java record, " +
"or provide a custom serializer for it.";
throw new ConfigurationException(msg);
}

@ -18,9 +18,9 @@ final class Validator {
return element;
}
static <T> Class<T> requireConfiguration(Class<T> cls) {
static <T> Class<T> requireConfigurationClass(Class<T> cls) {
requireNonNull(cls, "type");
if (!Reflect.isConfiguration(cls)) {
if (!Reflect.isConfigurationClass(cls)) {
String msg = "Class '" + cls.getSimpleName() + "' must be a configuration.";
throw new ConfigurationException(msg);
}
@ -36,13 +36,13 @@ final class Validator {
return cls;
}
static <T> Class<T> requireConfigurationOrRecord(Class<T> cls) {
requireNonNull(cls, "type");
if (!Reflect.isConfiguration(cls) && !cls.isRecord()) {
String msg = "Class '" + cls.getSimpleName() + "' must be a configuration or record.";
static <T> Class<T> requireConfigurationType(Class<T> type) {
requireNonNull(type, "type");
if (!Reflect.isConfigurationType(type)) {
String msg = "Class '" + type.getSimpleName() + "' must be a configuration or record.";
throw new ConfigurationException(msg);
}
return cls;
return type;
}
static void requirePrimitiveOrWrapperNumberType(Class<?> cls) {

@ -0,0 +1,413 @@
package de.exlll.configlib;
import de.exlll.configlib.Serializers.ListSerializer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static de.exlll.configlib.Polymorphic.DEFAULT_PROPERTY;
import static de.exlll.configlib.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
class PolymorphicSerializerTest {
private static final ConfigurationProperties PROPERTIES = ConfigurationProperties.newBuilder().build();
private static final SerializerSelector SELECTOR = new SerializerSelector(PROPERTIES);
@Test
void defaultPropertyNameIsType() {
assertThat(DEFAULT_PROPERTY, is("type"));
}
@Test
void serializeDoesNotAllowConfigurationElementWithSameNameAsProperty() {
@Polymorphic
@Configuration
class A {
String type = "";
}
@Configuration
@Polymorphic(property = "prop")
class B {
String prop = "";
}
record Config(A a, B b, List<A> as) {}
var serializerA = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
var serializerB = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "b"));
@SuppressWarnings("unchecked")
var serializerAs = (ListSerializer<A, Object>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
String msg = "Polymorphic serialization for type '%s' failed. " +
"The type contains a configuration element with name '%s' but that name is " +
"used by the @Polymorphic property.";
assertThrowsConfigurationException(
() -> serializerA.serialize(new A()),
msg.formatted(A.class.getName(), "type")
);
assertThrowsConfigurationException(
() -> serializerB.serialize(new B()),
msg.formatted(B.class.getName(), "prop")
);
assertThrowsConfigurationException(
() -> serializerAs.serialize(List.of(new A())),
msg.formatted(A.class.getName(), "type")
);
}
@Test
void deserializeMissingTypeFails() {
@Polymorphic
interface A {}
record R() implements A {}
record Config(A a) {}
var invalidClassName = R.class.getName() + "_INVALID";
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
assertThrowsConfigurationException(
() -> serializer.deserialize(Map.of(DEFAULT_PROPERTY, invalidClassName)),
("Polymorphic deserialization for type '%s' failed. " +
"The class '%s' does not exist.")
.formatted(A.class.getName(), invalidClassName)
);
}
static final class PolymorphicSerializerPropertyTest {
private static final String CUSTOM_PROPERTY = DEFAULT_PROPERTY + DEFAULT_PROPERTY;
@Polymorphic(property = CUSTOM_PROPERTY)
interface A {}
@Polymorphic(property = "")
interface B {}
record R(int i) implements A {}
record S(int i) implements B {}
record Config(A a, B b, List<A> as, List<B> bs) {}
@Test
void requirePropertyNameNonBlank() {
RuntimeException exception = Assertions.assertThrows(
RuntimeException.class,
() -> SELECTOR.select(fieldAsElement(Config.class, "b"))
);
ConfigurationException configurationException = (ConfigurationException)
exception.getCause().getCause();
Assertions.assertEquals(
"The @Polymorphic annotation does not allow a blank property name but " +
"type '%s' uses one.".formatted(B.class.getName()),
configurationException.getMessage()
);
}
@Test
void requirePropertyNameNonBlankNested() {
RuntimeException exception = Assertions.assertThrows(
RuntimeException.class,
() -> SELECTOR.select(fieldAsElement(Config.class, "bs"))
);
ConfigurationException configurationException = (ConfigurationException)
exception.getCause().getCause();
Assertions.assertEquals(
"The @Polymorphic annotation does not allow a blank property name but " +
"type '%s' uses one.".formatted(B.class.getName()),
configurationException.getMessage()
);
}
@Test
void serializeUsesPropertyName() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
Map<?, ?> serialize = serializer.serialize(new R(10));
assertThat(serialize, is(Map.of(CUSTOM_PROPERTY, R.class.getName(), "i", 10L)));
}
@Test
void deserializeUsesPropertyName() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
R deserialize = (R) serializer.deserialize(Map.of(
CUSTOM_PROPERTY, R.class.getName(),
"i", 20L
));
assertThat(deserialize, is(new R(20)));
}
@Test
void deserializeThrowsExceptionIfPropertyMissing() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
var serialized = Map.of("i", 20L);
assertThrowsConfigurationException(
() -> serializer.deserialize(serialized),
("Polymorphic deserialization for type '%s' failed. The property '%s' which " +
"holds the type is missing. Value to be deserialized:\n%s")
.formatted(A.class.getName(), CUSTOM_PROPERTY, serialized)
);
}
@Test
void deserializeThrowsExceptionIfPropertyMissingNested() {
@SuppressWarnings("unchecked")
var serializer = (ListSerializer<?, Map<?, ?>>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
var serialized = Map.of("i", 20L);
assertThrowsConfigurationException(
() -> serializer.deserialize(List.of(serialized)),
("Polymorphic deserialization for type '%s' failed. The property '%s' which " +
"holds the type is missing. Value to be deserialized:\n%s")
.formatted(A.class.getName(), CUSTOM_PROPERTY, serialized)
);
}
@Test
void deserializeThrowsExceptionIfPropertyHasWrongType() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a"));
var serialized = Map.of(CUSTOM_PROPERTY, 1, "i", 20L);
assertThrowsConfigurationException(
() -> serializer.deserialize(serialized),
("Polymorphic deserialization for type '%s' failed. The type identifier '1' " +
"which should hold the type is not a string but of type 'java.lang.Integer'.")
.formatted(A.class.getName())
);
}
@Test
void deserializeThrowsExceptionIfPropertyHasWrongTypeNested() {
@SuppressWarnings("unchecked")
var serializer = (ListSerializer<?, Map<?, ?>>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
var serialized = Map.of(CUSTOM_PROPERTY, 1, "i", 20L);
assertThrowsConfigurationException(
() -> serializer.deserialize(List.of(serialized)),
("Polymorphic deserialization for type '%s' failed. The type identifier '1' " +
"which should hold the type is not a string but of type 'java.lang.Integer'.")
.formatted(A.class.getName())
);
}
}
static final class PolymorphicInterfaceSerializerTest {
@Polymorphic
interface A {}
@Configuration
static final class Impl1 implements A {
int i = 10;
}
record Impl2(double d) implements A {}
static final class Config {
A a1 = new Impl1();
A a2 = new Impl2(20d);
List<A> as = List.of(a1, a2);
}
static final Config CONFIG = new Config();
@Test
void getPolymorphicType() {
var serializer1 = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
var serializer2 = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
assertThat(serializer1.getPolymorphicType(), equalTo(A.class));
assertThat(serializer2.getPolymorphicType(), equalTo(A.class));
}
@Test
void serializeA1() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
Map<?, ?> serialize = serializer.serialize(CONFIG.a1);
assertThat(serialize, is(asMap(
DEFAULT_PROPERTY, Impl1.class.getName(),
"i", 10L
)));
}
@Test
void deserializeA1() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
Impl1 deserialize = (Impl1) serializer.deserialize(asMap(
DEFAULT_PROPERTY, Impl1.class.getName(),
"i", 20L
));
assertThat(deserialize.i, is(20));
}
@Test
void serializeA2() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
Map<?, ?> serialize = serializer.serialize(CONFIG.a2);
assertThat(serialize, is(asMap(
DEFAULT_PROPERTY, Impl2.class.getName(),
"d", 20d
)));
}
@Test
void deserializeA2() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
Impl2 deserialize = (Impl2) serializer.deserialize(asMap(
DEFAULT_PROPERTY, Impl2.class.getName(),
"d", 30d
));
assertThat(deserialize.d, is(30d));
}
@Test
void serializeAs() {
@SuppressWarnings("unchecked")
var serializer = (ListSerializer<A, Object>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<?> serialize = serializer.serialize(CONFIG.as);
assertThat(serialize, is(List.of(
asMap(DEFAULT_PROPERTY, Impl1.class.getName(), "i", 10L),
asMap(DEFAULT_PROPERTY, Impl2.class.getName(), "d", 20d)
)));
}
@Test
void deserializeAs() {
@SuppressWarnings("unchecked")
ListSerializer<A, Map<?, ?>> serializer = (ListSerializer<A, Map<?, ?>>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<A> deserialize = serializer.deserialize(List.of(
asMap(DEFAULT_PROPERTY, Impl1.class.getName(), "i", 20L),
asMap(DEFAULT_PROPERTY, Impl2.class.getName(), "d", 30d)
));
assertThat(deserialize.size(), is(2));
Impl1 actual1 = (Impl1) deserialize.get(0);
Impl2 actual2 = (Impl2) deserialize.get(1);
assertThat(actual1.i, is(20));
assertThat(actual2, is(new Impl2(30)));
}
}
static final class PolymorphicAbstractClassSerializerTest {
@Polymorphic
@Configuration
static abstract class A {
String s1 = "s1";
}
static class B extends A {
String s2 = "s2";
}
static final class Impl1 extends A {
int i = 10;
}
static final class Impl2 extends B {
double d = 20d;
}
static final class Config {
A a1 = new Impl1();
A a2 = new Impl2();
List<A> as = List.of(a1, a2);
}
static final Config CONFIG = new Config();
@Test
void getPolymorphicType() {
var serializer1 = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
var serializer2 = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
assertThat(serializer1.getPolymorphicType(), equalTo(A.class));
assertThat(serializer2.getPolymorphicType(), equalTo(A.class));
}
@Test
void serializeA1() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
Map<?, ?> serialize = serializer.serialize(CONFIG.a1);
assertThat(serialize, is(asMap(
DEFAULT_PROPERTY, Impl1.class.getName(),
"s1", "s1",
"i", 10L
)));
}
@Test
void deserializeA1() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a1"));
Impl1 deserialize = (Impl1) serializer.deserialize(asMap(
DEFAULT_PROPERTY, Impl1.class.getName(),
"s1", "sa",
"i", 20L
));
assertThat(deserialize.i, is(20));
assertThat(deserialize.s1, is("sa"));
}
@Test
void serializeA2() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
Map<?, ?> serialize = serializer.serialize(CONFIG.a2);
assertThat(serialize, is(asMap(
DEFAULT_PROPERTY, Impl2.class.getName(),
"s1", "s1",
"s2", "s2",
"d", 20d
)));
}
@Test
void deserializeA2() {
var serializer = (PolymorphicSerializer) SELECTOR.select(fieldAsElement(Config.class, "a2"));
Impl2 deserialize = (Impl2) serializer.deserialize(asMap(
DEFAULT_PROPERTY, Impl2.class.getName(),
"s1", "sa",
"s2", "sb",
"d", 30d
));
assertThat(deserialize.d, is(30d));
assertThat(deserialize.s1, is("sa"));
assertThat(deserialize.s2, is("sb"));
}
@Test
void serializeAs() {
@SuppressWarnings("unchecked")
ListSerializer<A, ?> serializer = (ListSerializer<A, Object>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<?> serialize = serializer.serialize(CONFIG.as);
assertThat(serialize, is(List.of(
asMap(DEFAULT_PROPERTY, Impl1.class.getName(), "s1", "s1", "i", 10L),
asMap(DEFAULT_PROPERTY, Impl2.class.getName(), "s1", "s1", "s2", "s2", "d", 20d)
)));
}
@Test
void deserializeAs() {
@SuppressWarnings("unchecked")
ListSerializer<A, Map<?, ?>> serializer = (ListSerializer<A, Map<?, ?>>)
SELECTOR.select(fieldAsElement(Config.class, "as"));
List<A> deserialize = serializer.deserialize(List.of(
asMap(DEFAULT_PROPERTY, Impl1.class.getName(), "s1", "sa", "i", 20L),
asMap(DEFAULT_PROPERTY, Impl2.class.getName(), "s1", "sa", "s2", "sb", "d", 30d)
));
assertThat(deserialize.size(), is(2));
Impl1 actual1 = (Impl1) deserialize.get(0);
assertThat(actual1.i, is(20));
assertThat(actual1.s1, is("sa"));
Impl2 actual2 = (Impl2) deserialize.get(1);
assertThat(actual2.d, is(30d));
assertThat(actual2.s1, is("sa"));
assertThat(actual2.s2, is("sb"));
}
}
}

@ -233,11 +233,29 @@ class ReflectTest {
@Configuration
class C {}
class D extends C {}
record R() {}
assertThat(Reflect.isConfiguration(A.class), is(false));
assertThat(Reflect.isConfiguration(B.class), is(false));
assertThat(Reflect.isConfiguration(C.class), is(true));
assertThat(Reflect.isConfiguration(D.class), is(true));
assertThat(Reflect.isConfigurationClass(A.class), is(false));
assertThat(Reflect.isConfigurationClass(B.class), is(false));
assertThat(Reflect.isConfigurationClass(C.class), is(true));
assertThat(Reflect.isConfigurationClass(D.class), is(true));
assertThat(Reflect.isConfigurationClass(R.class), is(false));
}
@Test
void isConfigurationType() {
class A {}
class B extends A {}
@Configuration
class C {}
class D extends C {}
record R() {}
assertThat(Reflect.isConfigurationType(A.class), is(false));
assertThat(Reflect.isConfigurationType(B.class), is(false));
assertThat(Reflect.isConfigurationType(C.class), is(true));
assertThat(Reflect.isConfigurationType(D.class), is(true));
assertThat(Reflect.isConfigurationType(R.class), is(true));
}
@Test

@ -56,11 +56,6 @@ class SerializerSelectorTest {
return findByCondition(field -> field.getName().equals(name));
}
private static ConfigurationElement<?> forField(Class<?> type, String fieldName) {
Field field = getField(type, fieldName);
return new ConfigurationElements.FieldElement(field);
}
@ParameterizedTest
@ValueSource(classes = {boolean.class, Boolean.class})
void selectSerializerBoolean(Class<?> cls) {
@ -248,9 +243,9 @@ class SerializerSelectorTest {
Object object;
}
assertThrowsConfigurationException(
() -> SELECTOR.select(forField(A.class, "object")),
() -> SELECTOR.select(fieldAsElement(A.class, "object")),
"Missing serializer for type class java.lang.Object.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties."
"@Configuration, make it a Java record, or provide a custom serializer for it."
);
}
@ -407,7 +402,7 @@ class SerializerSelectorTest {
class A {
Map<E, Set<List<E>>> mesle;
}
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(forField(A.class, "mesle"));
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(fieldAsElement(A.class, "mesle"));
var keySerializer = (EnumSerializer) serializer.getKeySerializer();
assertThat(keySerializer.getEnumCls(), equalTo(E.class));
@ -423,7 +418,7 @@ class SerializerSelectorTest {
class A {
Map<List<String>, String> mlss;
}
ConfigurationElement<?> element = forField(A.class, "mlss");
ConfigurationElement<?> element = fieldAsElement(A.class, "mlss");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
@ -437,7 +432,7 @@ class SerializerSelectorTest {
class A {
Map<Point, String> mps;
}
ConfigurationElement<?> element = forField(A.class, "mps");
ConfigurationElement<?> element = fieldAsElement(A.class, "mps");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
@ -452,7 +447,7 @@ class SerializerSelectorTest {
class A {
Box<String> box;
}
ConfigurationElement<?> element = forField(A.class, "box");
ConfigurationElement<?> element = fieldAsElement(A.class, "box");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
@ -466,7 +461,7 @@ class SerializerSelectorTest {
class A {
List<?>[] ga;
}
ConfigurationElement<?> element = forField(A.class, "ga");
ConfigurationElement<?> element = fieldAsElement(A.class, "ga");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
"Cannot select serializer for type 'java.util.List<?>[]'.\n" +
@ -479,7 +474,7 @@ class SerializerSelectorTest {
class A {
List<? extends String> les;
}
ConfigurationElement<?> element = forField(A.class, "les");
ConfigurationElement<?> element = fieldAsElement(A.class, "les");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
"Cannot select serializer for type '? extends java.lang.String'.\n" +
@ -492,7 +487,7 @@ class SerializerSelectorTest {
class A {
List<?> lw;
}
ConfigurationElement<?> element = forField(A.class, "lw");
ConfigurationElement<?> element = fieldAsElement(A.class, "lw");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
"Cannot select serializer for type '?'.\n" +
@ -505,7 +500,7 @@ class SerializerSelectorTest {
class A<T> {
T t;
}
ConfigurationElement<?> element = forField(A.class, "t");
ConfigurationElement<?> element = fieldAsElement(A.class, "t");
assertThrowsConfigurationException(
() -> SELECTOR.select(element),
"Cannot select serializer for type 'T'.\n" +
@ -544,65 +539,65 @@ class SerializerSelectorTest {
}
@Test
void selectCustomSerializerForField() {
var serializer = SELECTOR.select(forField(Z.class, "string"));
void selectCustomSerializerfieldAsElement() {
var serializer = SELECTOR.select(fieldAsElement(Z.class, "string"));
assertThat(serializer, instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForListsWithNesting0() {
var serializer = SELECTOR.select(forField(Z.class, "list1"));
var serializer = SELECTOR.select(fieldAsElement(Z.class, "list1"));
assertThat(serializer, instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForListsWithNesting1() {
var serializer = (ListSerializer<?, ?>) SELECTOR.select(forField(Z.class, "list2"));
var serializer = (ListSerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "list2"));
assertThat(serializer.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForListsWithNesting2() {
var serializer1 = (ListSerializer<?, ?>) SELECTOR.select(forField(Z.class, "list3"));
var serializer1 = (ListSerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "list3"));
var serializer2 = (SetAsListSerializer<?, ?>) serializer1.getElementSerializer();
assertThat(serializer2.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForSetsWithNesting0() {
var serializer = SELECTOR.select(forField(Z.class, "set1"));
var serializer = SELECTOR.select(fieldAsElement(Z.class, "set1"));
assertThat(serializer, instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForSetsWithNesting1() {
var serializer = (SetAsListSerializer<?, ?>) SELECTOR.select(forField(Z.class, "set2"));
var serializer = (SetAsListSerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "set2"));
assertThat(serializer.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForSetsWithNesting2() {
var serializer1 = (SetAsListSerializer<?, ?>) SELECTOR.select(forField(Z.class, "set3"));
var serializer1 = (SetAsListSerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "set3"));
var serializer2 = (ListSerializer<?, ?>) serializer1.getElementSerializer();
assertThat(serializer2.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForMapsWithNesting0() {
var serializer = SELECTOR.select(forField(Z.class, "map1"));
var serializer = SELECTOR.select(fieldAsElement(Z.class, "map1"));
assertThat(serializer, instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForMapsWithNesting1() {
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(forField(Z.class, "map2"));
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(fieldAsElement(Z.class, "map2"));
assertThat(serializer.getKeySerializer(), instanceOf(NumberSerializer.class));
assertThat(serializer.getValueSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForMapsWithNesting2() {
var serializer1 = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(forField(Z.class, "map3"));
var serializer1 = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(fieldAsElement(Z.class, "map3"));
var serializer2 = (MapSerializer<?, ?, ?, ?>) serializer1.getValueSerializer();
assertThat(serializer2.getKeySerializer(), instanceOf(StringSerializer.class));
assertThat(serializer2.getValueSerializer(), instanceOf(IdentitySerializer.class));
@ -610,19 +605,19 @@ class SerializerSelectorTest {
@Test
void selectCustomSerializerForArraysWithNesting0() {
var serializer = SELECTOR.select(forField(Z.class, "array1"));
var serializer = SELECTOR.select(fieldAsElement(Z.class, "array1"));
assertThat(serializer, instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForArraysWithNesting1() {
var serializer = (ArraySerializer<?, ?>) SELECTOR.select(forField(Z.class, "array2"));
var serializer = (ArraySerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "array2"));
assertThat(serializer.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@Test
void selectCustomSerializerForArraysWithNesting2() {
var serializer1 = (ArraySerializer<?, ?>) SELECTOR.select(forField(Z.class, "array3"));
var serializer1 = (ArraySerializer<?, ?>) SELECTOR.select(fieldAsElement(Z.class, "array3"));
var serializer2 = (ArraySerializer<?, ?>) serializer1.getElementSerializer();
assertThat(serializer2.getElementSerializer(), instanceOf(IdentitySerializer.class));
}
@ -639,10 +634,10 @@ class SerializerSelectorTest {
@SerializeWith(serializer = IdentitySerializer.class, nesting = 2)
List<String> list;
}
assertThat(SELECTOR.select(forField(A.class, "s1")), instanceOf(StringSerializer.class));
assertThat(SELECTOR.select(forField(A.class, "s2")), instanceOf(IdentitySerializer.class));
assertThat(SELECTOR.select(forField(A.class, "s3")), instanceOf(StringSerializer.class));
var serializer = (ListSerializer<?, ?>) SELECTOR.select(forField(A.class, "list"));
assertThat(SELECTOR.select(fieldAsElement(A.class, "s1")), instanceOf(StringSerializer.class));
assertThat(SELECTOR.select(fieldAsElement(A.class, "s2")), instanceOf(IdentitySerializer.class));
assertThat(SELECTOR.select(fieldAsElement(A.class, "s3")), instanceOf(StringSerializer.class));
var serializer = (ListSerializer<?, ?>) SELECTOR.select(fieldAsElement(A.class, "list"));
assertThat(serializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@ -653,7 +648,7 @@ class SerializerSelectorTest {
String s;
}
var element = forField(A.class, "s");
var element = fieldAsElement(A.class, "s");
var field = getField(A.class, "s");
var serializer = (SerializerWithContext) SELECTOR.select(element);
var context = serializer.ctx;
@ -670,7 +665,7 @@ class SerializerSelectorTest {
List<String> l;
}
var element = forField(A.class, "l");
var element = fieldAsElement(A.class, "l");
var field = getField(A.class, "l");
var outerSerializer = (ListSerializer<?, ?>) SELECTOR.select(element);
var innerSerializer = (SerializerWithContext) outerSerializer.getElementSerializer();
@ -727,7 +722,7 @@ class SerializerSelectorTest {
@ParameterizedTest
@ValueSource(strings = {"myType1", "myType2", "myType3", "myType4", "myType5"})
void selectCustomSerializerForTypes(String fieldName) {
var element = forField(Config.class, fieldName);
var element = fieldAsElement(Config.class, fieldName);
var serializer = (IdentitySerializer) SELECTOR.select(element);
assertThat(serializer.context().element(), is(element));
}
@ -735,9 +730,9 @@ class SerializerSelectorTest {
@Test
void serializeWithNotInherited() {
assertThrowsConfigurationException(
() -> SELECTOR.select(forField(Config.class, "myType6")),
() -> SELECTOR.select(fieldAsElement(Config.class, "myType6")),
("Missing serializer for type %s.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties.")
"@Configuration, make it a Java record, or provide a custom serializer for it.")
.formatted(MyType6.class)
);
@ -750,7 +745,7 @@ class SerializerSelectorTest {
.addSerializer(MyType1.class, serializer)
.build();
var selector = new SerializerSelector(properties);
var actual = (IdentifiableSerializer<?, ?>) selector.select(forField(Config.class, "myType1"));
var actual = (IdentifiableSerializer<?, ?>) selector.select(fieldAsElement(Config.class, "myType1"));
assertThat(actual, sameInstance(serializer));
}
}
@ -796,7 +791,7 @@ class SerializerSelectorTest {
@ParameterizedTest
@ValueSource(strings = {"myType1", "myType2", "myType3", "myType4", "myType5"})
void selectCustomSerializerForTypes(String fieldName) {
var element = forField(Config.class, fieldName);
var element = fieldAsElement(Config.class, fieldName);
var serializer = (IdentitySerializer) SELECTOR.select(element);
assertThat(serializer.context().element(), is(element));
}
@ -804,9 +799,9 @@ class SerializerSelectorTest {
@Test
void metaSerializeWithNotInherited() {
assertThrowsConfigurationException(
() -> SELECTOR.select(forField(Config.class, "myType6")),
() -> SELECTOR.select(fieldAsElement(Config.class, "myType6")),
("Missing serializer for type %s.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties.")
"@Configuration, make it a Java record, or provide a custom serializer for it.")
.formatted(MyType6.class)
);
@ -814,7 +809,7 @@ class SerializerSelectorTest {
@Test
void metaSerializeWithHasLowerPrecedenceThanSerializeWith() {
var serializer = SELECTOR.select(forField(Config.class, "myType7"));
var serializer = SELECTOR.select(fieldAsElement(Config.class, "myType7"));
assertThat(serializer, instanceOf(PointSerializer.class));
}
}

@ -262,4 +262,9 @@ public final class TestUtils {
throw new RuntimeException(e);
}
}
public static ConfigurationElement<?> fieldAsElement(Class<?> type, String fieldName) {
Field field = getField(type, fieldName);
return new ConfigurationElements.FieldElement(field);
}
}

@ -49,6 +49,8 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
@Override
public void save(T configuration, Path configurationFile) {
requireNonNull(configuration, "configuration");
requireNonNull(configurationFile, "configuration file");
tryCreateParentDirectories(configurationFile);
var extractedCommentNodes = extractor.extractCommentNodes(configuration);
var yamlFileWriter = new YamlFileWriter(configurationFile, properties);
@ -80,6 +82,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
@Override
public T load(Path configurationFile) {
requireNonNull(configurationFile, "configuration file");
try (var reader = Files.newBufferedReader(configurationFile)) {
var yaml = YAML_LOADER.loadFromReader(reader);
var conf = requireYamlMap(yaml, configurationFile);
@ -110,6 +113,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
@Override
public T update(Path configurationFile) {
requireNonNull(configurationFile, "configuration file");
if (Files.exists(configurationFile)) {
T configuration = load(configurationFile);
save(configuration, configurationFile);

@ -35,6 +35,41 @@ class YamlConfigurationStoreTest {
Integer i = null;
}
@Test
void saveRequiresNonNullArguments() {
YamlConfigurationStore<A> store = newDefaultStore(A.class);
assertThrowsNullPointerException(
() -> store.save(null, yamlFile),
"configuration"
);
assertThrowsNullPointerException(
() -> store.save(new A(), null),
"configuration file"
);
}
@Test
void loadRequiresNonNullArguments() {
YamlConfigurationStore<A> store = newDefaultStore(A.class);
assertThrowsNullPointerException(
() -> store.load(null),
"configuration file"
);
}
@Test
void updateRequiresNonNullArguments() {
YamlConfigurationStore<A> store = newDefaultStore(A.class);
assertThrowsNullPointerException(
() -> store.update(null),
"configuration file"
);
}
@Test
void save() {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()

Loading…
Cancel
Save