Add top-level saving/loading support for Records

Records can now be saved and loaded directly using one of the several
methods available.

Also, the TypeSerializer has been refactored.
dev
Exlll 3 years ago
parent a2153106b5
commit e5028e6199

@ -1,7 +1,10 @@
package de.exlll.configlib; package de.exlll.configlib;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.*; import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;

@ -1,52 +1,34 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationField;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
final class ConfigurationSerializer<T> extends TypeSerializer<T, Field> { final class ConfigurationSerializer<T> extends TypeSerializer<T, ConfigurationField> {
ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) { ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) {
super(Validator.requireConfiguration(configurationType), properties); super(Validator.requireConfiguration(configurationType), properties);
} }
@Override
public Map<?, ?> serialize(T element) {
final Map<String, Object> result = new LinkedHashMap<>();
for (final Field field : filterFields()) {
final Object fieldValue = Reflect.getValue(field, element);
if ((fieldValue == null) && !properties.outputNulls())
continue;
final Object serializedValue = serialize(field.getName(), fieldValue);
final String formattedField = properties.getNameFormatter().format(field.getName());
result.put(formattedField, serializedValue);
}
return result;
}
@Override @Override
public T deserialize(Map<?, ?> element) { public T deserialize(Map<?, ?> element) {
final T result = Reflect.newInstance(type); final T result = Reflect.newInstance(type);
for (final Field field : filterFields()) { for (final var component : components()) {
final String fieldFormatted = properties.getNameFormatter().format(field.getName()); final var formattedName = formatter.format(component.componentName());
if (!element.containsKey(fieldFormatted)) if (!element.containsKey(formattedName))
continue; continue;
final Object serializedValue = element.get(fieldFormatted); final var serializedValue = element.get(formattedName);
final var field = component.component();
if (serializedValue == null && properties.inputNulls()) { if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveFieldType(field); requireNonPrimitiveFieldType(field);
Reflect.setValue(field, result, null); Reflect.setValue(field, result, null);
} else if (serializedValue != null) { } else if (serializedValue != null) {
final Object deserialized = deserialize(field, field.getName(), serializedValue); Object deserializeValue = deserialize(component, serializedValue);
Reflect.setValue(field, result, deserialized); Reflect.setValue(field, result, deserializeValue);
} }
} }
@ -54,7 +36,7 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, Field> {
} }
@Override @Override
protected void requireSerializableParts() { protected void requireSerializableComponents() {
if (serializers.isEmpty()) { if (serializers.isEmpty()) {
String msg = "Configuration class '" + type.getSimpleName() + "' " + String msg = "Configuration class '" + type.getSimpleName() + "' " +
"does not contain any (de-)serializable fields."; "does not contain any (de-)serializable fields.";
@ -63,9 +45,22 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, Field> {
} }
@Override @Override
protected String baseDeserializeExceptionMessage(Field field, Object value) { protected String baseDeserializeExceptionMessage(ConfigurationField component, Object value) {
return "Deserialization of value '%s' with type '%s' for field '%s' failed." return "Deserialization of value '%s' with type '%s' for field '%s' failed."
.formatted(value, value.getClass(), field); .formatted(value, value.getClass(), component.component());
}
@Override
protected Iterable<ConfigurationField> components() {
return FieldExtractors.CONFIGURATION.extract(type)
.filter(properties.getFieldFilter())
.map(ConfigurationField::new)
.toList();
}
@Override
T newDefaultInstance() {
return Reflect.newInstance(type);
} }
private static void requireNonPrimitiveFieldType(Field field) { private static void requireNonPrimitiveFieldType(Field field) {
@ -76,12 +71,6 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, Field> {
} }
} }
private List<Field> filterFields() {
return FieldExtractors.CONFIGURATION.extract(type)
.filter(properties.getFieldFilter())
.toList();
}
Class<T> getConfigurationType() { Class<T> getConfigurationType() {
return type; return type;
} }

@ -1,61 +1,42 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationRecordComponent;
import java.lang.reflect.RecordComponent; import java.lang.reflect.RecordComponent;
import java.util.LinkedHashMap; import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
final class RecordSerializer<R extends Record> extends TypeSerializer<R, RecordComponent> { final class RecordSerializer<R> extends
TypeSerializer<R, ConfigurationRecordComponent> {
RecordSerializer(Class<R> recordType, ConfigurationProperties properties) { RecordSerializer(Class<R> recordType, ConfigurationProperties properties) {
super(Validator.requireRecord(recordType), properties); super(Validator.requireRecord(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 @Override
public R deserialize(Map<?, ?> element) { public R deserialize(Map<?, ?> element) {
final var components = type.getRecordComponents(); final var components = components();
final var constructorArguments = new Object[components.length]; final var constructorArguments = new Object[components.size()];
for (int i = 0; i < components.length; i++) { for (int i = 0, size = components.size(); i < size; i++) {
final var component = components[i]; final var component = components.get(i);
final var componentFormatted = properties.getNameFormatter() final var formattedName = formatter.format(component.componentName());
.format(component.getName());
if (!element.containsKey(componentFormatted)) { if (!element.containsKey(formattedName)) {
constructorArguments[i] = Reflect.getDefaultValue(component.getType()); constructorArguments[i] = Reflect.getDefaultValue(component.componentType());
continue; continue;
} }
final Object serializedArgument = element.get(componentFormatted); final var serializedValue = element.get(formattedName);
final var recordComponent = component.component();
if (serializedArgument == null && properties.inputNulls()) { if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveComponentType(component); requireNonPrimitiveComponentType(recordComponent);
constructorArguments[i] = null; constructorArguments[i] = null;
} else if (serializedArgument == null) { } else if (serializedValue == null) {
constructorArguments[i] = Reflect.getDefaultValue(component.getType()); constructorArguments[i] = Reflect.getDefaultValue(component.componentType());
} else { } else {
constructorArguments[i] = deserialize( constructorArguments[i] = deserialize(component, serializedValue);
component,
component.getName(),
serializedArgument
);
} }
} }
@ -63,7 +44,7 @@ final class RecordSerializer<R extends Record> extends TypeSerializer<R, RecordC
} }
@Override @Override
protected void requireSerializableParts() { protected void requireSerializableComponents() {
if (serializers.isEmpty()) { if (serializers.isEmpty()) {
String msg = "Record type '%s' does not define any components." String msg = "Record type '%s' does not define any components."
.formatted(type.getSimpleName()); .formatted(type.getSimpleName());
@ -72,9 +53,21 @@ final class RecordSerializer<R extends Record> extends TypeSerializer<R, RecordC
} }
@Override @Override
protected String baseDeserializeExceptionMessage(RecordComponent component, Object value) { protected String baseDeserializeExceptionMessage(ConfigurationRecordComponent component, Object value) {
return "Deserialization of value '%s' with type '%s' for component '%s' of record '%s' failed." return "Deserialization of value '%s' with type '%s' for component '%s' of record '%s' failed."
.formatted(value, value.getClass(), component, component.getDeclaringRecord()); .formatted(value, value.getClass(), component.component(), component.declaringType());
}
@Override
protected List<ConfigurationRecordComponent> components() {
return Arrays.stream(type.getRecordComponents())
.map(ConfigurationRecordComponent::new)
.toList();
}
@Override
R newDefaultInstance() {
return Reflect.newRecordDefaultValues(type);
} }
private static void requireNonPrimitiveComponentType(RecordComponent component) { private static void requireNonPrimitiveComponentType(RecordComponent component) {

@ -8,6 +8,8 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static de.exlll.configlib.Validator.requireRecord;
final class Reflect { final class Reflect {
private static final Map<Class<?>, Object> DEFAULT_VALUES = initDefaultValues(); private static final Map<Class<?>, Object> DEFAULT_VALUES = initDefaultValues();
@ -60,9 +62,11 @@ final class Reflect {
} }
} }
static <R extends Record> R newRecord(Class<R> recordType, Object... constructorArguments) { // We could use <R extends Record> as a bound here and for the other methods below
// but that would require casts elsewhere.
static <R> R newRecord(Class<R> recordType, Object... constructorArguments) {
try { try {
Constructor<R> constructor = getCanonicalConstructor(recordType); Constructor<R> constructor = getCanonicalConstructor(requireRecord(recordType));
constructor.setAccessible(true); constructor.setAccessible(true);
return constructor.newInstance(constructorArguments); return constructor.newInstance(constructorArguments);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
@ -81,12 +85,23 @@ final class Reflect {
} }
} }
static <R extends Record> Constructor<R> getCanonicalConstructor(Class<R> recordType) static <R> R newRecordDefaultValues(Class<R> recordType) {
final Object[] args = Arrays.stream(recordParameterTypes(requireRecord(recordType)))
.map(Reflect::getDefaultValue)
.toArray(Object[]::new);
return Reflect.newRecord(recordType, args);
}
static <R> Constructor<R> getCanonicalConstructor(Class<R> recordType)
throws NoSuchMethodException { throws NoSuchMethodException {
Class<?>[] parameterTypes = Arrays.stream(recordType.getRecordComponents()) Class<?>[] parameterTypes = recordParameterTypes(requireRecord(recordType));
return recordType.getDeclaredConstructor(parameterTypes);
}
private static <R> Class<?>[] recordParameterTypes(Class<R> recordType) {
return Arrays.stream(recordType.getRecordComponents())
.map(RecordComponent::getType) .map(RecordComponent::getType)
.toArray(Class<?>[]::new); .toArray(Class<?>[]::new);
return recordType.getDeclaredConstructor(parameterTypes);
} }
static <T> T[] newArray(Class<T> componentType, int length) { static <T> T[] newArray(Class<T> componentType, int length) {

@ -43,6 +43,12 @@ sealed interface TypeComponent<T> {
*/ */
Object componentValue(Object componentHolder); Object componentValue(Object componentHolder);
/**
* Returns the type that declares this component.
*
* @return the declaring type
*/
Class<?> declaringType();
record ConfigurationField(Field component) implements TypeComponent<Field> { record ConfigurationField(Field component) implements TypeComponent<Field> {
public ConfigurationField(Field component) { public ConfigurationField(Field component) {
@ -63,6 +69,11 @@ sealed interface TypeComponent<T> {
public Object componentValue(Object componentHolder) { public Object componentValue(Object componentHolder) {
return Reflect.getValue(component, componentHolder); return Reflect.getValue(component, componentHolder);
} }
@Override
public Class<?> declaringType() {
return component.getDeclaringClass();
}
} }
record ConfigurationRecordComponent(RecordComponent component) record ConfigurationRecordComponent(RecordComponent component)
@ -85,6 +96,11 @@ sealed interface TypeComponent<T> {
public Object componentValue(Object componentHolder) { public Object componentValue(Object componentHolder) {
return Reflect.getValue(component, componentHolder); return Reflect.getValue(component, componentHolder);
} }
@Override
public Class<?> declaringType() {
return component.getDeclaringRecord();
}
} }
} }

@ -1,51 +1,89 @@
package de.exlll.configlib; package de.exlll.configlib;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
abstract class TypeSerializer<T, P> implements Serializer<T, Map<?, ?>> { sealed abstract class TypeSerializer<T, TC extends TypeComponent<?>>
implements Serializer<T, Map<?, ?>>
permits ConfigurationSerializer, RecordSerializer {
protected final Class<T> type; protected final Class<T> type;
protected final ConfigurationProperties properties; protected final ConfigurationProperties properties;
protected final NameFormatter formatter;
protected final Map<String, Serializer<?, ?>> serializers; protected final Map<String, Serializer<?, ?>> serializers;
protected TypeSerializer(Class<T> type, ConfigurationProperties properties) { protected TypeSerializer(Class<T> type, ConfigurationProperties properties) {
this.type = requireNonNull(type, "type"); this.type = requireNonNull(type, "type");
this.properties = requireNonNull(properties, "configuration properties"); this.properties = requireNonNull(properties, "configuration properties");
this.formatter = properties.getNameFormatter();
this.serializers = new SerializerMapper(type, properties).buildSerializerMap(); this.serializers = new SerializerMapper(type, properties).buildSerializerMap();
requireSerializableParts(); requireSerializableComponents();
} }
protected final Object serialize(String partName, Object value) { static <T> TypeSerializer<T, ?> newSerializerFor(
Class<T> type,
ConfigurationProperties properties
) {
return type.isRecord()
? new RecordSerializer<>(type, properties)
: new ConfigurationSerializer<>(type, properties);
}
@Override
public final Map<?, ?> serialize(T element) {
final Map<String, Object> result = new LinkedHashMap<>();
for (final TC component : components()) {
final Object componentValue = component.componentValue(element);
if ((componentValue == null) && !properties.outputNulls())
continue;
final Object serializedValue = serialize(component, componentValue);
final String formattedName = formatter.format(component.componentName());
result.put(formattedName, serializedValue);
}
return result;
}
protected final Object serialize(TC component, Object value) {
// The following cast won't cause a ClassCastException because the serializers // The following cast won't cause a ClassCastException because the serializers
// are selected based on the part type. // are selected based on the component type.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>) serializers.get(partName); final var serializer = (Serializer<Object, Object>)
serializers.get(component.componentName());
return (value != null) ? serializer.serialize(value) : null; return (value != null) ? serializer.serialize(value) : null;
} }
protected final Object deserialize(P part, String partName, Object value) { protected final Object deserialize(TC component, Object value) {
// This unchecked cast leads to an exception if the type of the object which // 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. // is deserialized is not a subtype of the type the deserializer expects.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>) serializers.get(partName); final var serializer = (Serializer<Object, Object>)
serializers.get(component.componentName());
final Object deserialized; final Object deserialized;
try { try {
deserialized = serializer.deserialize(value); deserialized = serializer.deserialize(value);
} catch (ClassCastException e) { } catch (ClassCastException e) {
String msg = baseDeserializeExceptionMessage(part, value) + "\n" + String msg = baseDeserializeExceptionMessage(component, value) + "\n" +
"The type of the object to be deserialized does not " + "The type of the object to be deserialized does not " +
"match the type the deserializer expects."; "match the type the deserializer expects.";
throw new ConfigurationException(msg, e); throw new ConfigurationException(msg, e);
} catch (RuntimeException e) { } catch (RuntimeException e) {
String msg = baseDeserializeExceptionMessage(part, value); String msg = baseDeserializeExceptionMessage(component, value);
throw new ConfigurationException(msg, e); throw new ConfigurationException(msg, e);
} }
return deserialized; return deserialized;
} }
protected abstract void requireSerializableParts(); protected abstract void requireSerializableComponents();
protected abstract String baseDeserializeExceptionMessage(TC component, Object value);
protected abstract Iterable<TC> components();
protected abstract String baseDeserializeExceptionMessage(P part, Object value); abstract T newDefaultInstance();
} }

@ -31,7 +31,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
private static final Load YAML_LOADER = newYamlLoader(); private static final Load YAML_LOADER = newYamlLoader();
private final Class<T> configurationType; private final Class<T> configurationType;
private final YamlConfigurationProperties properties; private final YamlConfigurationProperties properties;
private final ConfigurationSerializer<T> serializer; private final TypeSerializer<T, ?> serializer;
private final CommentNodeExtractor extractor; private final CommentNodeExtractor extractor;
/** /**
@ -44,7 +44,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
public YamlConfigurationStore(Class<T> configurationType, YamlConfigurationProperties properties) { public YamlConfigurationStore(Class<T> configurationType, YamlConfigurationProperties properties) {
this.configurationType = requireNonNull(configurationType, "configuration type"); this.configurationType = requireNonNull(configurationType, "configuration type");
this.properties = requireNonNull(properties, "properties"); this.properties = requireNonNull(properties, "properties");
this.serializer = new ConfigurationSerializer<>(configurationType, properties); this.serializer = TypeSerializer.newSerializerFor(configurationType, properties);
this.extractor = new CommentNodeExtractor(properties); this.extractor = new CommentNodeExtractor(properties);
} }
@ -83,7 +83,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
public T load(Path configurationFile) { public T load(Path configurationFile) {
try (var reader = Files.newBufferedReader(configurationFile)) { try (var reader = Files.newBufferedReader(configurationFile)) {
var yaml = YAML_LOADER.loadFromReader(reader); var yaml = YAML_LOADER.loadFromReader(reader);
var conf = requireConfiguration(yaml, configurationFile); var conf = requireYamlMap(yaml, configurationFile);
return serializer.deserialize(conf); return serializer.deserialize(conf);
} catch (YamlEngineException e) { } catch (YamlEngineException e) {
String msg = "The configuration file at %s does not contain valid YAML."; String msg = "The configuration file at %s does not contain valid YAML.";
@ -93,7 +93,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
} }
} }
private Map<?, ?> requireConfiguration(Object yaml, Path configurationFile) { private Map<?, ?> requireYamlMap(Object yaml, Path configurationFile) {
if (yaml == null) { if (yaml == null) {
String msg = "The configuration file at %s is empty or only contains null."; String msg = "The configuration file at %s is empty or only contains null.";
throw new ConfigurationException(msg.formatted(configurationFile)); throw new ConfigurationException(msg.formatted(configurationFile));
@ -116,7 +116,7 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
save(configuration, configurationFile); save(configuration, configurationFile);
return configuration; return configuration;
} }
T configuration = Reflect.newInstance(configurationType); T configuration = serializer.newDefaultInstance();
save(configuration, configurationFile); save(configuration, configurationFile);
return configuration; return configuration;
} }

@ -10,10 +10,10 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import static de.exlll.configlib.TestUtils.assertThrowsRuntimeException; import static de.exlll.configlib.TestUtils.*;
import static de.exlll.configlib.TestUtils.getField;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
class ReflectTest { class ReflectTest {
@ -305,4 +305,36 @@ class ReflectTest {
"The canonical constructor of record type 'R3' threw an exception." "The canonical constructor of record type 'R3' threw an exception."
); );
} }
@Test
void newRecordRequiresRecordType() {
class A {}
assertThrowsConfigurationException(
() -> Reflect.newRecord(A.class),
"Class 'A' must be a record."
);
}
@Test
void newRecordWithDefaultValues() {
record E() {}
record R(boolean a, char b, byte c, short d, int e, long f, float g, double h,
Boolean i, Character j, Integer k, Float l, E m, R n, Object o) {}
R r = Reflect.newRecordDefaultValues(R.class);
assertFalse(r.a);
assertEquals('\0', r.b);
assertEquals(0, r.c);
assertEquals(0, r.d);
assertEquals(0, r.e);
assertEquals(0, r.f);
assertEquals(0, r.g);
assertEquals(0, r.h);
assertNull(r.i);
assertNull(r.j);
assertNull(r.k);
assertNull(r.l);
assertNull(r.m);
assertNull(r.n);
assertNull(r.o);
}
} }

@ -35,6 +35,11 @@ class TypeComponentTest {
void componentValue() { void componentValue() {
assertThat(COMPONENT.componentValue(new C()), is(20)); assertThat(COMPONENT.componentValue(new C()), is(20));
} }
@Test
void declaringType() {
assertThat(COMPONENT.declaringType(), equalTo(C.class));
}
} }
static final class ConfigurationRecordComponentTest { static final class ConfigurationRecordComponentTest {
@ -58,5 +63,10 @@ class TypeComponentTest {
void componentValue() { void componentValue() {
assertThat(COMPONENT.componentValue(new R(10f)), is(10f)); assertThat(COMPONENT.componentValue(new R(10f)), is(10f));
} }
@Test
void declaringType() {
assertThat(COMPONENT.declaringType(), equalTo(R.class));
}
} }
} }

@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*;
class YamlConfigurationPropertiesTest { class YamlConfigurationPropertiesTest {
@Test @Test

@ -61,6 +61,33 @@ class YamlConfigurationStoreTest {
assertEquals(expected, TestUtils.readFile(yamlFile)); assertEquals(expected, TestUtils.readFile(yamlFile));
} }
@Test
void saveRecord() {
record R(String s, @Comment("A comment") Integer i) {}
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.header("The\nHeader")
.footer("The\nFooter")
.outputNulls(true)
.setNameFormatter(String::toUpperCase)
.build();
YamlConfigurationStore<R> store = new YamlConfigurationStore<>(R.class, properties);
store.save(new R("S1", null), yamlFile);
String expected =
"""
# The
# Header
S: S1
# A comment
I: null
# The
# Footer\
""";
assertEquals(expected, TestUtils.readFile(yamlFile));
}
@Configuration @Configuration
static final class B { static final class B {
String s = "S1"; String s = "S1";
@ -97,6 +124,36 @@ class YamlConfigurationStoreTest {
assertNull(config.i); assertNull(config.i);
} }
@Test
void loadRecord() throws IOException {
record R(String s, String t, Integer i) {}
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.inputNulls(true)
.setNameFormatter(String::toUpperCase)
.build();
YamlConfigurationStore<R> store = new YamlConfigurationStore<>(R.class, properties);
Files.writeString(
yamlFile,
"""
# The
# Header
S: S2
t: T2
I: null
# The
# Footer\
"""
);
R config = store.load(yamlFile);
assertEquals("S2", config.s);
assertNull(config.t);
assertNull(config.i);
}
@Configuration @Configuration
static final class C { static final class C {
int i; int i;
@ -207,6 +264,29 @@ class YamlConfigurationStoreTest {
assertEquals(11, config.j); assertEquals(11, config.j);
} }
@Test
void updateCreatesConfigurationFileIfItDoesNotExistRecord() {
record R(int i, char c, String s) {}
YamlConfigurationStore<R> store = new YamlConfigurationStore<>(
R.class,
YamlConfigurationProperties.newBuilder().outputNulls(true).build()
);
assertFalse(Files.exists(yamlFile));
R config = store.update(yamlFile);
assertEquals(
"""
i: 0
c: "\\0"
s: null\
""",
readFile(yamlFile)
);
assertEquals(0, config.i);
assertEquals('\0', config.c);
assertNull(config.s);
}
@Test @Test
void updateLoadsConfigurationFileIfItDoesExist() throws IOException { void updateLoadsConfigurationFileIfItDoesExist() throws IOException {
YamlConfigurationStore<E> store = newDefaultStore(E.class); YamlConfigurationStore<E> store = newDefaultStore(E.class);
@ -217,6 +297,17 @@ class YamlConfigurationStoreTest {
assertEquals(11, config.j); assertEquals(11, config.j);
} }
@Test
void updateLoadsConfigurationFileIfItDoesExistRecord() throws IOException {
record R(int i, int j) {}
YamlConfigurationStore<R> store = newDefaultStore(R.class);
Files.writeString(yamlFile, "i: 20");
R config = store.update(yamlFile);
assertEquals(20, config.i);
assertEquals(0, config.j);
}
@Test @Test
void updateUpdatesFile() throws IOException { void updateUpdatesFile() throws IOException {
YamlConfigurationStore<E> store = newDefaultStore(E.class); YamlConfigurationStore<E> store = newDefaultStore(E.class);
@ -228,6 +319,18 @@ class YamlConfigurationStoreTest {
assertEquals("i: 20\nj: 11", readFile(yamlFile)); assertEquals("i: 20\nj: 11", readFile(yamlFile));
} }
@Test
void updateUpdatesFileRecord() throws IOException {
record R(int i, int j) {}
YamlConfigurationStore<R> store = newDefaultStore(R.class);
Files.writeString(yamlFile, "i: 20\nk: 30");
R config = store.update(yamlFile);
assertEquals(20, config.i);
assertEquals(0, config.j);
assertEquals("i: 20\nj: 0", readFile(yamlFile));
}
private static <T> YamlConfigurationStore<T> newDefaultStore(Class<T> configType) { private static <T> YamlConfigurationStore<T> newDefaultStore(Class<T> configType) {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build(); YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
return new YamlConfigurationStore<>(configType, properties); return new YamlConfigurationStore<>(configType, properties);

Loading…
Cancel
Save