forked from public-mirrors/ConfigLib
Add support for Record serialization
parent
31748db01b
commit
7e79597cdd
@ -1,24 +0,0 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Implementations of this interface format the names of fields.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldFormatter extends Function<Field, String> {
|
||||
/**
|
||||
* Formats the name of the given field.
|
||||
*
|
||||
* @param field the field
|
||||
* @return formatted field name
|
||||
* @throws NullPointerException if {@code field} is null
|
||||
*/
|
||||
String format(Field field);
|
||||
|
||||
@Override
|
||||
default String apply(Field field) {
|
||||
return format(field);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Implementations of this interface format the names of class fields or record components.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface NameFormatter extends Function<String, String> {
|
||||
/**
|
||||
* Formats the name of a class field or record component.
|
||||
*
|
||||
* @param name the name that is formatted
|
||||
* @return formatted name
|
||||
* @throws NullPointerException if {@code name} is null
|
||||
*/
|
||||
String format(String name);
|
||||
|
||||
/**
|
||||
* Formats the name of a class field or record component.
|
||||
*
|
||||
* @param name the name that is formatted
|
||||
* @return formatted name
|
||||
* @throws NullPointerException if {@code name} is null
|
||||
*/
|
||||
@Override
|
||||
default String apply(String name) {
|
||||
return format(name);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class RecordSerializer<R extends Record> extends TypeSerializer<R, RecordComponent> {
|
||||
RecordSerializer(Class<R> recordType, ConfigurationProperties properties) {
|
||||
super(recordType, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<?, ?> serialize(R element) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
for (final RecordComponent component : type.getRecordComponents()) {
|
||||
final Object componentValue = Reflect.getValue(component, element);
|
||||
|
||||
if (componentValue == null && !properties.outputNulls())
|
||||
continue;
|
||||
|
||||
final Object resultValue = serialize(component.getName(), componentValue);
|
||||
|
||||
final String compName = properties.getNameFormatter().format(component.getName());
|
||||
result.put(compName, resultValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R deserialize(Map<?, ?> element) {
|
||||
final var components = type.getRecordComponents();
|
||||
final var constructorArguments = new Object[components.length];
|
||||
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
final var component = components[i];
|
||||
final var componentFormatted = properties.getNameFormatter()
|
||||
.format(component.getName());
|
||||
|
||||
if (!element.containsKey(componentFormatted)) {
|
||||
constructorArguments[i] = Reflect.getDefaultValue(component.getType());
|
||||
continue;
|
||||
}
|
||||
|
||||
final Object serializedArgument = element.get(componentFormatted);
|
||||
|
||||
if (serializedArgument == null && properties.inputNulls()) {
|
||||
requireNonPrimitiveComponentType(component);
|
||||
constructorArguments[i] = null;
|
||||
} else if (serializedArgument == null) {
|
||||
constructorArguments[i] = Reflect.getDefaultValue(component.getType());
|
||||
} else {
|
||||
constructorArguments[i] = deserialize(
|
||||
component,
|
||||
component.getName(),
|
||||
serializedArgument
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.newRecord(type, constructorArguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void requireSerializableParts() {
|
||||
if (serializers.isEmpty()) {
|
||||
String msg = "Record type '%s' does not define any components."
|
||||
.formatted(type.getSimpleName());
|
||||
throw new ConfigurationException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String baseDeserializeExceptionMessage(RecordComponent component, Object value) {
|
||||
return "Deserialization of value '%s' with type '%s' for component '%s' of record '%s' failed."
|
||||
.formatted(value, value.getClass(), component, component.getDeclaringRecord());
|
||||
}
|
||||
|
||||
private static void requireNonPrimitiveComponentType(RecordComponent component) {
|
||||
if (component.getType().isPrimitive()) {
|
||||
String msg = ("Cannot set component '%s' of record type '%s' to null. Primitive types " +
|
||||
"cannot be assigned null values.")
|
||||
.formatted(component, component.getDeclaringRecord());
|
||||
throw new ConfigurationException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Class<R> getRecordType() {
|
||||
return type;
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static de.exlll.configlib.Validator.requireNonNull;
|
||||
|
||||
/**
|
||||
* A mapper that maps field or component names to serializers that are selected based on
|
||||
* the field or component type, respectively.
|
||||
*/
|
||||
final class SerializerMapper {
|
||||
private final Class<?> type;
|
||||
private final ConfigurationProperties properties;
|
||||
private final SerializerSelector selector;
|
||||
|
||||
SerializerMapper(Class<?> type, ConfigurationProperties properties) {
|
||||
this.type = requireNonNull(type, "type");
|
||||
this.properties = requireNonNull(properties, "configuration properties");
|
||||
this.selector = new SerializerSelector(properties);
|
||||
requireConfigurationOrRecord();
|
||||
}
|
||||
|
||||
private void requireConfigurationOrRecord() {
|
||||
if (!type.isRecord() && !Reflect.isConfiguration(type)) {
|
||||
String msg = "Type '%s' must be a configuration or record type."
|
||||
.formatted(type.getSimpleName());
|
||||
throw new ConfigurationException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Serializer<?, ?>> buildSerializerMap() {
|
||||
return type.isRecord()
|
||||
? buildSerializerMapForRecord()
|
||||
: buildSerializerMapForConfiguration();
|
||||
|
||||
}
|
||||
|
||||
private Map<String, Serializer<?, ?>> buildSerializerMapForRecord() {
|
||||
return tryBuildSerializerMap(
|
||||
Arrays.stream(type.getRecordComponents()),
|
||||
RecordComponent::getName,
|
||||
RecordComponent::getGenericType
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Serializer<?, ?>> buildSerializerMapForConfiguration() {
|
||||
return tryBuildSerializerMap(filterFields(), Field::getName, Field::getGenericType);
|
||||
}
|
||||
|
||||
private <T> Map<String, Serializer<?, ?>> tryBuildSerializerMap(
|
||||
Stream<T> stream,
|
||||
Function<T, String> nameExtractor,
|
||||
Function<T, Type> typeExtractor
|
||||
) {
|
||||
try {
|
||||
return stream.collect(Collectors.toMap(
|
||||
nameExtractor,
|
||||
element -> selector.select(typeExtractor.apply(element))
|
||||
));
|
||||
} catch (StackOverflowError error) {
|
||||
String msg = "Recursive type definitions are not supported.";
|
||||
throw new ConfigurationException(msg, error);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<Field> filterFields() {
|
||||
return FieldExtractors.CONFIGURATION.extract(type)
|
||||
.filter(properties.getFieldFilter());
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static de.exlll.configlib.Validator.requireNonNull;
|
||||
|
||||
abstract class TypeSerializer<T, P> implements Serializer<T, Map<?, ?>> {
|
||||
protected final Class<T> type;
|
||||
protected final ConfigurationProperties properties;
|
||||
protected final Map<String, Serializer<?, ?>> serializers;
|
||||
|
||||
protected TypeSerializer(Class<T> type, ConfigurationProperties properties) {
|
||||
this.type = requireNonNull(type, "type");
|
||||
this.properties = requireNonNull(properties, "configuration properties");
|
||||
this.serializers = new SerializerMapper(type, properties).buildSerializerMap();
|
||||
requireSerializableParts();
|
||||
}
|
||||
|
||||
protected final Object serialize(String partName, Object value) {
|
||||
// The following cast won't cause a ClassCastException because the serializers
|
||||
// are selected based on the part type.
|
||||
@SuppressWarnings("unchecked")
|
||||
final var serializer = (Serializer<Object, Object>) serializers.get(partName);
|
||||
return (value != null) ? serializer.serialize(value) : null;
|
||||
}
|
||||
|
||||
protected final Object deserialize(P part, String partName, Object value) {
|
||||
// This unchecked cast leads to an exception if the type of the object which
|
||||
// is deserialized is not a subtype of the type the deserializer expects.
|
||||
@SuppressWarnings("unchecked")
|
||||
final var serializer = (Serializer<Object, Object>) serializers.get(partName);
|
||||
|
||||
final Object deserialized;
|
||||
try {
|
||||
deserialized = serializer.deserialize(value);
|
||||
} catch (ClassCastException e) {
|
||||
String msg = baseDeserializeExceptionMessage(part, value) + "\n" +
|
||||
"The type of the object to be deserialized does not " +
|
||||
"match the type the deserializer expects.";
|
||||
throw new ConfigurationException(msg, e);
|
||||
} catch (RuntimeException e) {
|
||||
String msg = baseDeserializeExceptionMessage(part, value);
|
||||
throw new ConfigurationException(msg, e);
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
protected abstract void requireSerializableParts();
|
||||
|
||||
protected abstract String baseDeserializeExceptionMessage(P part, Object value);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class FieldFormatterTest {
|
||||
@Test
|
||||
void applyCallsFormat() {
|
||||
class A {
|
||||
int i;
|
||||
}
|
||||
FieldFormatter formatter = mock(FieldFormatter.class, CALLS_REAL_METHODS);
|
||||
|
||||
Field field = TestUtils.getField(A.class, "i");
|
||||
formatter.apply(field);
|
||||
|
||||
verify(formatter).format(field);
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static de.exlll.configlib.TestUtils.getField;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
class FieldFormattersTest {
|
||||
private static class A {
|
||||
String lowercase;
|
||||
String camelCase;
|
||||
String withNumber123;
|
||||
String with123Number;
|
||||
String with_$;
|
||||
}
|
||||
|
||||
private static final Field f1 = getField(A.class, "lowercase");
|
||||
private static final Field f2 = getField(A.class, "camelCase");
|
||||
private static final Field f3 = getField(A.class, "withNumber123");
|
||||
private static final Field f4 = getField(A.class, "with123Number");
|
||||
private static final Field f5 = getField(A.class, "with_$");
|
||||
|
||||
@Test
|
||||
void formatIdentity() {
|
||||
FieldFormatters formatter = FieldFormatters.IDENTITY;
|
||||
|
||||
assertThat(formatter.format(f1), is("lowercase"));
|
||||
assertThat(formatter.format(f2), is("camelCase"));
|
||||
assertThat(formatter.format(f3), is("withNumber123"));
|
||||
assertThat(formatter.format(f4), is("with123Number"));
|
||||
assertThat(formatter.format(f5), is("with_$"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatLowerUnderscore() {
|
||||
FieldFormatters formatter = FieldFormatters.LOWER_UNDERSCORE;
|
||||
|
||||
assertThat(formatter.format(f1), is("lowercase"));
|
||||
assertThat(formatter.format(f2), is("camel_case"));
|
||||
assertThat(formatter.format(f3), is("with_number123"));
|
||||
assertThat(formatter.format(f4), is("with123_number"));
|
||||
assertThat(formatter.format(f5), is("with_$"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatUpperUnderscore() {
|
||||
FieldFormatters formatter = FieldFormatters.UPPER_UNDERSCORE;
|
||||
|
||||
assertThat(formatter.format(f1), is("LOWERCASE"));
|
||||
assertThat(formatter.format(f2), is("CAMEL_CASE"));
|
||||
assertThat(formatter.format(f3), is("WITH_NUMBER123"));
|
||||
assertThat(formatter.format(f4), is("WITH123_NUMBER"));
|
||||
assertThat(formatter.format(f5), is("WITH_$"));
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class NameFormatterTest {
|
||||
@Test
|
||||
void applyCallsFormat() {
|
||||
NameFormatter formatter = mock(NameFormatter.class, CALLS_REAL_METHODS);
|
||||
String name = "i";
|
||||
formatter.apply(name);
|
||||
verify(formatter).format(name);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
class NameFormattersTest {
|
||||
private static final String NAME_1 = "lowercase";
|
||||
private static final String NAME_2 = "camelCase";
|
||||
private static final String NAME_3 = "withNumber123";
|
||||
private static final String NAME_4 = "with123Number";
|
||||
private static final String NAME_5 = "with_$";
|
||||
|
||||
@Test
|
||||
void formatIdentity() {
|
||||
NameFormatters formatter = NameFormatters.IDENTITY;
|
||||
|
||||
assertThat(formatter.format(NAME_1), is("lowercase"));
|
||||
assertThat(formatter.format(NAME_2), is("camelCase"));
|
||||
assertThat(formatter.format(NAME_3), is("withNumber123"));
|
||||
assertThat(formatter.format(NAME_4), is("with123Number"));
|
||||
assertThat(formatter.format(NAME_5), is("with_$"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatLowerUnderscore() {
|
||||
NameFormatters formatter = NameFormatters.LOWER_UNDERSCORE;
|
||||
|
||||
assertThat(formatter.format(NAME_1), is("lowercase"));
|
||||
assertThat(formatter.format(NAME_2), is("camel_case"));
|
||||
assertThat(formatter.format(NAME_3), is("with_number123"));
|
||||
assertThat(formatter.format(NAME_4), is("with123_number"));
|
||||
assertThat(formatter.format(NAME_5), is("with_$"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatUpperUnderscore() {
|
||||
NameFormatters formatter = NameFormatters.UPPER_UNDERSCORE;
|
||||
|
||||
assertThat(formatter.format(NAME_1), is("LOWERCASE"));
|
||||
assertThat(formatter.format(NAME_2), is("CAMEL_CASE"));
|
||||
assertThat(formatter.format(NAME_3), is("WITH_NUMBER123"));
|
||||
assertThat(formatter.format(NAME_4), is("WITH123_NUMBER"));
|
||||
assertThat(formatter.format(NAME_5), is("WITH_$"));
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static de.exlll.configlib.TestUtils.asMap;
|
||||
import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RecordSerializerTest {
|
||||
private static <R extends Record> RecordSerializer<R> newSerializer(Class<R> cls) {
|
||||
return newSerializer(cls, builder -> {});
|
||||
}
|
||||
|
||||
private static <R extends Record> RecordSerializer<R> newSerializer(
|
||||
Class<R> cls,
|
||||
Consumer<ConfigurationProperties.Builder<?>> propertiesConfigurer
|
||||
) {
|
||||
var builder = ConfigurationProperties.newBuilder();
|
||||
builder.addSerializer(Point.class, TestUtils.POINT_SERIALIZER);
|
||||
propertiesConfigurer.accept(builder);
|
||||
return new RecordSerializer<>(cls, builder.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ctorRequiresRecordWithComponents() {
|
||||
record Empty() {}
|
||||
|
||||
assertThrowsConfigurationException(
|
||||
() -> newSerializer(Empty.class),
|
||||
"Record type 'Empty' does not define any components."
|
||||
);
|
||||
}
|
||||
|
||||
record R1(int value1, int someValue2) {}
|
||||
|
||||
@Test
|
||||
void serializeAppliesFormatter() {
|
||||
RecordSerializer<R1> serializer = newSerializer(
|
||||
R1.class,
|
||||
builder -> builder.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
|
||||
);
|
||||
Map<?, ?> map = serializer.serialize(new R1(1, 2));
|
||||
assertThat(map.remove("VALUE1"), is(1L));
|
||||
assertThat(map.remove("SOME_VALUE2"), is(2L));
|
||||
assertTrue(map.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserializeAppliesFormatter() {
|
||||
RecordSerializer<R1> serializer = newSerializer(
|
||||
R1.class,
|
||||
builder -> builder.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
|
||||
);
|
||||
Map<String, ?> map = Map.of(
|
||||
"value1", 3,
|
||||
"someValue2", 4,
|
||||
"VALUE1", 5,
|
||||
"SOME_VALUE2", 6
|
||||
);
|
||||
R1 r1 = serializer.deserialize(map);
|
||||
assertThat(r1.value1, is(5));
|
||||
assertThat(r1.someValue2, is(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializeOutputNullsTrue() {
|
||||
record R(Integer integer) {}
|
||||
RecordSerializer<R> serializer = newSerializer(R.class, b -> b.outputNulls(true));
|
||||
Map<?, ?> serialize = serializer.serialize(new R(null));
|
||||
assertThat(serialize, is(asMap("integer", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializeOutputNullsFalse() {
|
||||
record R(Integer integer) {}
|
||||
RecordSerializer<R> serializer = newSerializer(R.class, b -> b.outputNulls(false));
|
||||
Map<?, ?> serialize = serializer.serialize(new R(null));
|
||||
assertThat(serialize.entrySet(), empty());
|
||||
}
|
||||
|
||||
record R2(
|
||||
boolean f1,
|
||||
char f2,
|
||||
byte f3,
|
||||
short f4,
|
||||
int f5,
|
||||
long f6,
|
||||
float f7,
|
||||
double f8,
|
||||
Integer f9,
|
||||
R1 f10
|
||||
) {}
|
||||
|
||||
@Test
|
||||
void deserializeMissingValuesAsDefaultValues() {
|
||||
RecordSerializer<R2> serializer = newSerializer(R2.class);
|
||||
R2 r2 = serializer.deserialize(Map.of());
|
||||
|
||||
assertThat(r2.f1, is(false));
|
||||
assertThat(r2.f2, is('\0'));
|
||||
assertThat(r2.f3, is((byte) 0));
|
||||
assertThat(r2.f4, is((short) 0));
|
||||
assertThat(r2.f5, is(0));
|
||||
assertThat(r2.f6, is(0L));
|
||||
assertThat(r2.f7, is(0f));
|
||||
assertThat(r2.f8, is(0d));
|
||||
assertThat(r2.f9, nullValue());
|
||||
assertThat(r2.f10, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserializeNullValuesAsDefaultValuesIfInputNullsIsFalse() {
|
||||
RecordSerializer<R2> serializer = newSerializer(R2.class, b -> b.inputNulls(false));
|
||||
|
||||
Map<String, Object> serialized = new HashMap<>();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
serialized.put("f" + i, null);
|
||||
}
|
||||
|
||||
R2 r2 = serializer.deserialize(serialized);
|
||||
|
||||
assertThat(r2.f1, is(false));
|
||||
assertThat(r2.f2, is('\0'));
|
||||
assertThat(r2.f3, is((byte) 0));
|
||||
assertThat(r2.f4, is((short) 0));
|
||||
assertThat(r2.f5, is(0));
|
||||
assertThat(r2.f6, is(0L));
|
||||
assertThat(r2.f7, is(0f));
|
||||
assertThat(r2.f8, is(0d));
|
||||
assertThat(r2.f9, nullValue());
|
||||
assertThat(r2.f10, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserializeNullValuesAsNullIfInputNullsIsTrue() {
|
||||
record R(Integer i, String s, R1 r1) {}
|
||||
RecordSerializer<R> serializer = newSerializer(R.class, b -> b.inputNulls(true));
|
||||
|
||||
Map<String, Object> serialized = new HashMap<>();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
serialized.put("f" + i, null);
|
||||
}
|
||||
|
||||
R r = serializer.deserialize(serialized);
|
||||
|
||||
assertThat(r.i, nullValue());
|
||||
assertThat(r.s, nullValue());
|
||||
assertThat(r.r1, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserializeNullValuesAsNullIfInputNullsIsTrueFailsForPrimitiveFields() {
|
||||
RecordSerializer<R2> serializer = newSerializer(R2.class, builder -> builder.inputNulls(true));
|
||||
RecordComponent[] components = R2.class.getRecordComponents();
|
||||
|
||||
Map<String, Object> serialized = new LinkedHashMap<>();
|
||||
|
||||
// initialize map to be deserialized with default values
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
RecordComponent component = components[i - 1];
|
||||
Class<?> componentType = component.getType();
|
||||
Object value = Reflect.getDefaultValue(componentType);
|
||||
// CharacterSerializer expects String
|
||||
serialized.put(component.getName(), componentType == char.class ? "\0" : value);
|
||||
}
|
||||
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
RecordComponent component = components[i - 1];
|
||||
Class<?> componentType = component.getType();
|
||||
Object tmp = serialized.remove(component.getName());
|
||||
serialized.put(component.getName(), null);
|
||||
assertThrowsConfigurationException(
|
||||
() -> serializer.deserialize(serialized),
|
||||
("Cannot set component '%s %s' of record type " +
|
||||
"'class de.exlll.configlib.RecordSerializerTest$R2' to null. " +
|
||||
"Primitive types cannot be assigned null values.")
|
||||
.formatted(componentType.getSimpleName(), component.getName())
|
||||
|
||||
);
|
||||
serialized.put(component.getName(), tmp);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserializeInvalidType() {
|
||||
record B3(String s, List<List<String>> l) {}
|
||||
RecordSerializer<B3> serializer = newSerializer(B3.class);
|
||||
assertThrowsConfigurationException(
|
||||
() -> serializer.deserialize(Map.of("s", (byte) 3)),
|
||||
"Deserialization of value '3' with type 'class java.lang.Byte' for component " +
|
||||
"'java.lang.String s' of record 'class de.exlll.configlib.RecordSerializerTest$1B3' " +
|
||||
"failed.\nThe type of the object to be deserialized does not match the type " +
|
||||
"the deserializer expects."
|
||||
);
|
||||
assertThrowsConfigurationException(
|
||||
() -> serializer.deserialize(Map.of("l", List.of(List.of(3)))),
|
||||
"Deserialization of value '[[3]]' with type " +
|
||||
"'class java.util.ImmutableCollections$List12' for component " +
|
||||
"'java.util.List l' of record 'class de.exlll.configlib.RecordSerializerTest$1B3' " +
|
||||
"failed.\nThe type of the object to be deserialized does not match the type " +
|
||||
"the deserializer expects."
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package de.exlll.configlib;
|
||||
|
||||
import de.exlll.configlib.configurations.ExampleConfigurationA2;
|
||||
import de.exlll.configlib.configurations.ExampleConfigurationB1;
|
||||
import de.exlll.configlib.configurations.ExampleConfigurationB2;
|
||||
import de.exlll.configlib.configurations.ExampleEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static de.exlll.configlib.Serializers.*;
|
||||
import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
class SerializerMapperTest {
|
||||
private static SerializerMapper newMapper(Class<?> cls) {
|
||||
return newMapper(cls, builder -> {});
|
||||
}
|
||||
|
||||
private static SerializerMapper newMapper(
|
||||
Class<?> cls,
|
||||
Consumer<ConfigurationProperties.Builder<?>> propertiesConfigurer
|
||||
) {
|
||||
var builder = ConfigurationProperties.newBuilder();
|
||||
builder.addSerializer(Point.class, TestUtils.POINT_SERIALIZER);
|
||||
propertiesConfigurer.accept(builder);
|
||||
return new SerializerMapper(cls, builder.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requireConfigurationOrRecord() {
|
||||
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
|
||||
TestUtils.assertThrowsConfigurationException(
|
||||
() -> new SerializerMapper(Object.class, properties),
|
||||
"Type 'Object' must be a configuration or record type."
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForConfigurationFiltersFields() {
|
||||
Map<String, Serializer<?, ?>> serializers = newMapper(ExampleConfigurationA2.class)
|
||||
.buildSerializerMap();
|
||||
|
||||
assertThat(serializers.get("a1_staticFinalInt"), nullValue());
|
||||
assertThat(serializers.get("a1_staticInt"), nullValue());
|
||||
assertThat(serializers.get("a1_finalInt"), nullValue());
|
||||
assertThat(serializers.get("a1_transientInt"), nullValue());
|
||||
assertThat(serializers.get("a1_ignoredInt"), nullValue());
|
||||
assertThat(serializers.get("a1_ignoredString"), nullValue());
|
||||
assertThat(serializers.get("a1_ignoredListString"), nullValue());
|
||||
|
||||
assertThat(serializers.get("a2_staticFinalInt"), nullValue());
|
||||
assertThat(serializers.get("a2_staticInt"), nullValue());
|
||||
assertThat(serializers.get("a2_finalInt"), nullValue());
|
||||
assertThat(serializers.get("a2_transientInt"), nullValue());
|
||||
assertThat(serializers.get("a2_ignoredInt"), nullValue());
|
||||
assertThat(serializers.get("a2_ignoredString"), nullValue());
|
||||
assertThat(serializers.get("a2_ignoredListString"), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForConfigurationIgnoresFormatter() {
|
||||
Map<String, Serializer<?, ?>> serializers = newMapper(
|
||||
ExampleConfigurationA2.class,
|
||||
props -> props.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
|
||||
).buildSerializerMap();
|
||||
|
||||
assertThat(serializers.get("A2_PRIM_BOOL"), nullValue());
|
||||
assertThat(serializers.get("a2_primBool"), instanceOf(BooleanSerializer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForConfiguration() {
|
||||
Map<String, Serializer<?, ?>> serializers = newMapper(ExampleConfigurationA2.class)
|
||||
.buildSerializerMap();
|
||||
assertThat(serializers.get("a2_primBool"), instanceOf(BooleanSerializer.class));
|
||||
assertThat(serializers.get("a2_refChar"), instanceOf(CharacterSerializer.class));
|
||||
assertThat(serializers.get("a2_string"), instanceOf(StringSerializer.class));
|
||||
assertThat(serializers.get("a2_Enm"), instanceOf(EnumSerializer.class));
|
||||
|
||||
ConfigurationSerializer<?> serializerB1 =
|
||||
(ConfigurationSerializer<?>) serializers.get("a2_b1");
|
||||
ConfigurationSerializer<?> serializerB2 =
|
||||
(ConfigurationSerializer<?>) serializers.get("a2_b2");
|
||||
|
||||
assertThat(serializerB1.getConfigurationType(), equalTo(ExampleConfigurationB1.class));
|
||||
assertThat(serializerB2.getConfigurationType(), equalTo(ExampleConfigurationB2.class));
|
||||
|
||||
Serializers.ListSerializer<?, ?> serializerList =
|
||||
(Serializers.ListSerializer<?, ?>) serializers.get("a2_listByte");
|
||||
Serializers.ArraySerializer<?, ?> serializerArray =
|
||||
(Serializers.ArraySerializer<?, ?>) serializers.get("a2_arrayString");
|
||||
Serializers.SetAsListSerializer<?, ?> serializerSet =
|
||||
(Serializers.SetAsListSerializer<?, ?>) serializers.get("a2_setBigInteger");
|
||||
Serializers.MapSerializer<?, ?, ?, ?> serializerMap =
|
||||
(Serializers.MapSerializer<?, ?, ?, ?>) serializers.get("a2_mapLocalTimeLocalTime");
|
||||
|
||||
assertThat(
|
||||
serializers.get("a2_arrayPrimDouble"),
|
||||
instanceOf(PrimitiveDoubleArraySerializer.class)
|
||||
);
|
||||
|
||||
assertThat(serializerList.getElementSerializer(), instanceOf(NumberSerializer.class));
|
||||
assertThat(serializerArray.getElementSerializer(), instanceOf(StringSerializer.class));
|
||||
assertThat(serializerSet.getElementSerializer(), instanceOf(BigIntegerSerializer.class));
|
||||
assertThat(serializerMap.getKeySerializer(), instanceOf(LocalTimeSerializer.class));
|
||||
assertThat(serializerMap.getValueSerializer(), instanceOf(LocalTimeSerializer.class));
|
||||
|
||||
assertThat(serializers.get("a2_point"), sameInstance(TestUtils.POINT_SERIALIZER));
|
||||
}
|
||||
|
||||
private record R1(int integer, boolean bool) {}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForRecordIgnoresFormatter() {
|
||||
Map<String, Serializer<?, ?>> serializers = newMapper(
|
||||
R1.class,
|
||||
props -> props.setNameFormatter(NameFormatters.UPPER_UNDERSCORE)
|
||||
).buildSerializerMap();
|
||||
|
||||
assertThat(serializers.get("INTEGER"), nullValue());
|
||||
assertThat(serializers.get("BOOL"), nullValue());
|
||||
assertThat(serializers.get("integer"), instanceOf(NumberSerializer.class));
|
||||
assertThat(serializers.get("bool"), instanceOf(BooleanSerializer.class));
|
||||
}
|
||||
|
||||
private record R2(
|
||||
boolean primBool,
|
||||
Character refChar,
|
||||
String string,
|
||||
ExampleEnum enm,
|
||||
ExampleConfigurationB1 b1,
|
||||
ExampleConfigurationB2 b2,
|
||||
List<Byte> listByte,
|
||||
String[] arrayString,
|
||||
Set<BigInteger> setBigInteger,
|
||||
Map<UUID, UUID> mapUuidUuid,
|
||||
double[] arrayDouble,
|
||||
Point point
|
||||
) {}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForRecord() {
|
||||
Map<String, Serializer<?, ?>> serializers = newMapper(R2.class)
|
||||
.buildSerializerMap();
|
||||
assertThat(serializers.get("primBool"), instanceOf(BooleanSerializer.class));
|
||||
assertThat(serializers.get("refChar"), instanceOf(CharacterSerializer.class));
|
||||
assertThat(serializers.get("string"), instanceOf(StringSerializer.class));
|
||||
assertThat(serializers.get("enm"), instanceOf(EnumSerializer.class));
|
||||
|
||||
ConfigurationSerializer<?> serializerB1 =
|
||||
(ConfigurationSerializer<?>) serializers.get("b1");
|
||||
ConfigurationSerializer<?> serializerB2 =
|
||||
(ConfigurationSerializer<?>) serializers.get("b2");
|
||||
|
||||
assertThat(serializerB1.getConfigurationType(), equalTo(ExampleConfigurationB1.class));
|
||||
assertThat(serializerB2.getConfigurationType(), equalTo(ExampleConfigurationB2.class));
|
||||
|
||||
Serializers.ListSerializer<?, ?> serializerList =
|
||||
(Serializers.ListSerializer<?, ?>) serializers.get("listByte");
|
||||
Serializers.ArraySerializer<?, ?> serializerArray =
|
||||
(Serializers.ArraySerializer<?, ?>) serializers.get("arrayString");
|
||||
Serializers.SetAsListSerializer<?, ?> serializerSet =
|
||||
(Serializers.SetAsListSerializer<?, ?>) serializers.get("setBigInteger");
|
||||
Serializers.MapSerializer<?, ?, ?, ?> serializerMap =
|
||||
(Serializers.MapSerializer<?, ?, ?, ?>) serializers.get("mapUuidUuid");
|
||||
|
||||
assertThat(
|
||||
serializers.get("arrayDouble"),
|
||||
instanceOf(PrimitiveDoubleArraySerializer.class)
|
||||
);
|
||||
|
||||
assertThat(serializerList.getElementSerializer(), instanceOf(NumberSerializer.class));
|
||||
assertThat(serializerArray.getElementSerializer(), instanceOf(StringSerializer.class));
|
||||
assertThat(serializerSet.getElementSerializer(), instanceOf(BigIntegerSerializer.class));
|
||||
assertThat(serializerMap.getKeySerializer(), instanceOf(UuidSerializer.class));
|
||||
assertThat(serializerMap.getValueSerializer(), instanceOf(UuidSerializer.class));
|
||||
|
||||
assertThat(serializers.get("point"), sameInstance(TestUtils.POINT_SERIALIZER));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static final class Recursive1 {
|
||||
Recursive2 recursive2;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static final class Recursive2 {
|
||||
Recursive1 recursive1;
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForConfigurationPreventsRecursiveDefinitions() {
|
||||
assertThrowsConfigurationException(
|
||||
() -> newMapper(Recursive1.class).buildSerializerMap(),
|
||||
"Recursive type definitions are not supported."
|
||||
);
|
||||
}
|
||||
|
||||
record RecursiveRecord1(RecursiveRecord2 recursiveRecord2) {}
|
||||
|
||||
record RecursiveRecord2(RecursiveRecord1 recursiveRecord1) {}
|
||||
|
||||
record RecursiveRecord3(RecursiveRecord3 recursiveRecord3) {}
|
||||
|
||||
@Test
|
||||
void buildSerializerMapForRecordPreventsRecursiveDefinitions() {
|
||||
assertThrowsConfigurationException(
|
||||
() -> newMapper(RecursiveRecord1.class).buildSerializerMap(),
|
||||
"Recursive type definitions are not supported."
|
||||
);
|
||||
|
||||
assertThrowsConfigurationException(
|
||||
() -> newMapper(RecursiveRecord3.class).buildSerializerMap(),
|
||||
"Recursive type definitions are not supported."
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package de.exlll.configlib.configurations;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ExampleRecord1(
|
||||
int i,
|
||||
Double d,
|
||||
ExampleEnum enm,
|
||||
List<UUID> listUuid,
|
||||
float[][] arrayArrayFloat,
|
||||
ExampleConfigurationB1 b1
|
||||
) {
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ExampleRecord1 that = (ExampleRecord1) o;
|
||||
return i == that.i &&
|
||||
Objects.equals(d, that.d) &&
|
||||
enm == that.enm &&
|
||||
Objects.equals(listUuid, that.listUuid) &&
|
||||
Arrays.deepEquals(arrayArrayFloat, that.arrayArrayFloat) &&
|
||||
Objects.equals(b1, that.b1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(i, d, enm, listUuid, b1);
|
||||
result = 31 * result + Arrays.deepHashCode(arrayArrayFloat);
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package de.exlll.configlib.configurations;
|
||||
|
||||
public record ExampleRecord2(boolean b, ExampleRecord1 r1) {}
|
Loading…
Reference in New Issue