refactor!(serde-extension): Fundamentally change error handling for data reading

This commit is contained in:
2026-04-04 22:33:15 +02:00
parent e6ef13ff7f
commit 615d3810a0
26 changed files with 706 additions and 403 deletions

View File

@@ -1,6 +1,7 @@
package de.siphalor.tweed5.coat.bridge.impl;
import de.siphalor.coat.handler.ConfigEntryHandler;
import de.siphalor.coat.handler.Message;
import de.siphalor.coat.input.CheckBoxConfigInput;
import de.siphalor.coat.input.ConfigInput;
import de.siphalor.coat.input.CycleButtonConfigInput;
@@ -20,22 +21,23 @@ import de.siphalor.tweed5.coat.bridge.api.mapping.handler.BasicTweedCoatEntryHan
import de.siphalor.tweed5.coat.bridge.api.mapping.handler.ConvertingTweedCoatEntryHandler;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde_api.api.TweedDataReadException;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataToken;
import de.siphalor.tweed5.serde_api.api.TweedDataVisitor;
import de.siphalor.tweed5.serde_api.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.language.I18n;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
@@ -44,8 +46,7 @@ import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static de.siphalor.tweed5.coat.bridge.api.TweedCoatMappingUtils.translatableComponent;
import static de.siphalor.tweed5.coat.bridge.api.TweedCoatMappingUtils.translatableComponentWithFallback;
import static de.siphalor.tweed5.coat.bridge.api.TweedCoatMappingUtils.*;
@CommonsLog
@SuppressWarnings("unchecked")
@@ -133,66 +134,71 @@ public class TweedCoatMappersImpl {
if (context.parentSaveHandler() == null) {
throw new IllegalArgumentException("No parent save handler provided");
}
return new ConvertingTweedCoatEntryHandler<>(
new BasicTweedCoatEntryHandler<>(entry, context.defaultValue(), context.parentSaveHandler()),
this::convertToString,
input -> {
return new ConfigEntryHandler<String>() {
@Override
public String getDefault() {
return convertToString(context.defaultValue());
}
@Override
public Collection<Message> getMessages(String text) {
try {
TweedEntryReader<T, ConfigEntry<T>> reader =
readWriteExtension.getDefinedEntryReader(entry);
Patchwork readExtData = readWriteExtension.createReadWriteContextExtensionsData();
//noinspection DataFlowIssue
return reader.read(
new TweedDataReader() {
private boolean consumed = false;
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
if (consumed) {
throw new IllegalStateException("Already consumed");
TweedReadResult<T> readResult = convertFromString(text);
if (readResult.hasValue() && !readResult.hasIssues()) {
return Collections.emptyList();
} else if (readResult.hasIssues()) {
Message.Level messageLevel = readResult.hasValue()
? Message.Level.WARNING
: Message.Level.ERROR;
return Arrays.stream(readResult.issues()).map(issue ->
new Message(messageLevel, literalComponent(issue.exception().getMessage()))
).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
} catch (Exception e) {
return Collections.singletonList(
new Message(Message.Level.ERROR, literalComponent(e.getMessage()))
);
}
return new TweedDataToken() {
@Override
public boolean canReadAsString() {
return true;
}
@Override
public String readAsString() {
return input;
public void save(String s) {
try {
TweedReadResult<T> readResult = convertFromString(s);
if (readResult.hasValue()) {
if (readResult.hasIssues()) {
log.warn(
"There were issues understanding value \"" + s + "\":\n - "
+ Arrays.stream(readResult.issues())
.map(issue -> issue.exception().getMessage())
.collect(Collectors.joining("\n - "))
);
}
context.parentSaveHandler().accept(readResult.value());
} else {
log.error(
"Failed to understand value \"" + s + "\":\n - "
+ Arrays.stream(readResult.issues())
.map(issue -> issue.exception().getMessage())
.collect(Collectors.joining("\n - "))
);
context.parentSaveHandler().accept(context.defaultValue());
}
} catch (Exception e) {
log.error("Failed to save value \"" + s + "\"", e);
context.parentSaveHandler().accept(context.defaultValue());
}
}
@Override
public Component asText(String text) {
return literalComponent(text);
}
};
}
@Override
public TweedDataToken readToken() throws TweedDataReadException {
TweedDataToken token = peekToken();
consumed = true;
return token;
}
@Override
public void close() {
}
}, entry, new TweedReadContext() {
@Override
public ReadWriteExtension readWriteExtension() {
return readWriteExtension;
}
@Override
public Patchwork extensionsData() {
return readExtData;
}
}
);
} catch (TweedEntryReadException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
);
}
@Override
public @Nullable ConfigContentWidget createContentWidget(TweedCoatEntryCreationContext<T> context) {
return null;
@@ -270,6 +276,57 @@ public class TweedCoatMappersImpl {
}
return wrapper[0];
}
private TweedReadResult<T> convertFromString(String text) {
TweedEntryReader<T, ConfigEntry<T>> reader =
readWriteExtension.getDefinedEntryReader(entry);
Patchwork readExtData = readWriteExtension.createReadWriteContextExtensionsData();
//noinspection DataFlowIssue
return reader.read(
new TweedDataReader() {
private boolean consumed = false;
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
if (consumed) {
throw new IllegalStateException("Already consumed");
}
return new TweedDataToken() {
@Override
public boolean canReadAsString() {
return true;
}
@Override
public String readAsString() {
return text;
}
};
}
@Override
public TweedDataToken readToken() throws TweedDataReadException {
TweedDataToken token = peekToken();
consumed = true;
return token;
}
@Override
public void close() {
}
}, entry, new TweedReadContext() {
@Override
public ReadWriteExtension readWriteExtension() {
return readWriteExtension;
}
@Override
public Patchwork extensionsData() {
return readExtData;
}
}
);
}
};
}
}

View File

@@ -58,9 +58,9 @@ public class TweedCoatBridgeTestMod implements ClientModInitializer {
MOD_ID + ".config",
GLFW.GLFW_KEY_T,
//# if MC_VERSION_NUMBER >= 12109
//- KeyMapping.Category.MISC
KeyMapping.Category.MISC
//# else
"key.categories.misc"
//- "key.categories.misc"
//# end
));
@@ -69,9 +69,9 @@ public class TweedCoatBridgeTestMod implements ClientModInitializer {
private class ScreenKeyBinding extends KeyMapping implements PriorityKeyBinding {
//# if MC_VERSION_NUMBER >= 12109
//- public ScreenKeyBinding(String name, int key, Category category) {
public ScreenKeyBinding(String name, int key, Category category) {
//# else
public ScreenKeyBinding(String name, int key, String category) {
//- public ScreenKeyBinding(String name, int key, String category) {
//# end
super(name, key, category);
}

View File

@@ -2,22 +2,22 @@ package de.siphalor.tweed5.fabric.helper.api;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataWriter;
import de.siphalor.tweed5.serde_api.api.TweedSerde;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadIssue;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataWriter;
import de.siphalor.tweed5.serde_api.api.TweedSerde;
import lombok.Getter;
import lombok.extern.apachecommons.CommonsLog;
import net.fabricmc.loader.api.FabricLoader;
import org.jspecify.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -84,14 +84,24 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
return;
}
try (TweedDataReader reader = serde.createReader(new FileInputStream(configFile))) {
try (TweedDataReader reader = serde.createReader(Files.newInputStream(configFile.toPath()))) {
Patchwork contextExtensionsData = readWriteExtension.createReadWriteContextExtensionsData();
readContextCustomizer.accept(contextExtensionsData);
PatchInfo patchInfo = patchExtension.collectPatchInfo(contextExtensionsData);
T readValue = readWriteExtension.read(reader, configContainer().rootEntry(), contextExtensionsData);
TweedReadResult<T> readResult = readWriteExtension.read(reader, configContainer().rootEntry(), contextExtensionsData);
if (readResult.hasValue()) {
patchExtension.patch(configContainer.rootEntry(), value, readResult.value(), patchInfo);
if (readResult.hasIssues()) {
log.warn(formatIssuesLogMessage(configFile, "issues", readResult.issues()));
}
} else if (readResult.hasIssues()) {
log.error(formatIssuesLogMessage(configFile, "errors", readResult.issues()));
} else {
log.debug("Reading config file " + configFile.getAbsolutePath() + " yielded empty result");
}
patchExtension.patch(configContainer.rootEntry(), value, readValue, patchInfo);
} catch (Exception e) {
log.error("Failed loading config file " + configFile.getAbsolutePath(), e);
}
@@ -108,20 +118,54 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
return defaultValueSupplier.get();
}
try (TweedDataReader reader = serde.createReader(new FileInputStream(configFile))) {
try (TweedDataReader reader = serde.createReader(Files.newInputStream(configFile.toPath()))) {
Patchwork contextExtensionsData = readWriteExtension.createReadWriteContextExtensionsData();
return readWriteExtension.read(reader, configContainer.rootEntry(), contextExtensionsData);
TweedReadResult<T> readResult = readWriteExtension.read(
reader,
configContainer.rootEntry(),
contextExtensionsData
);
if (readResult.hasValue()) {
if (readResult.hasIssues()) {
log.warn(formatIssuesLogMessage(configFile, "issues", readResult.issues()));
}
return readResult.value();
} else if (readResult.hasIssues()) {
log.error(formatIssuesLogMessage(configFile, "errors", readResult.issues()));
} else {
log.info(
"Reading config file "
+ configFile.getAbsolutePath()
+ " yielded empty result, using default value"
);
}
return defaultValueSupplier.get();
} catch (Exception e) {
log.error("Failed loading config file " + configFile.getAbsolutePath(), e);
return defaultValueSupplier.get();
}
}
private String formatIssuesLogMessage(File file, String type, TweedReadIssue[] issues) {
String filePath = file.getAbsolutePath();
StringBuilder stringBuilder = new StringBuilder(20 + filePath.length() + type.length() + issues.length * 50);
stringBuilder.append("Encountered ");
stringBuilder.append(type);
stringBuilder.append(" while reading ");
stringBuilder.append(filePath);
stringBuilder.append(": ");
for (TweedReadIssue issue : issues) {
stringBuilder.append(" - ").append(issue).append("\n");
}
return stringBuilder.toString();
}
public void writeConfigInConfigDirectory(T configValue) {
File configFile = getConfigFile();
Path tempConfigDirectory = getOrCreateTempConfigDirectory();
File tempConfigFile = tempConfigDirectory.resolve(getConfigFileName()).toFile();
try (TweedDataWriter writer = serde.createWriter(new FileOutputStream(tempConfigFile))) {
try (TweedDataWriter writer = serde.createWriter(Files.newOutputStream(tempConfigFile.toPath()))) {
Patchwork contextExtensionsData = readWriteExtension.createReadWriteContextExtensionsData();
readWriteExtension.write(

View File

@@ -1,71 +0,0 @@
package de.siphalor.tweed5.attributesextension.impl.serde.filter;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Map;
@RequiredArgsConstructor
public class AttributesFilteredCompoundEntry<T extends @Nullable Object> implements CompoundConfigEntry<T> {
private final CompoundConfigEntry<T> delegate;
@Override
public void set(T compoundValue, String key, Object value) {
if (value == AttributesReadWriteFilterExtensionImpl.NOOP_MARKER) {
return;
}
delegate.set(compoundValue, key, value);
}
@Override
public Object get(T compoundValue, String key) {
return delegate.get(compoundValue, key);
}
@Override
public @NonNull T instantiateValue() {
return delegate.instantiateValue();
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return delegate.subEntries();
}
@Override
public ConfigContainer<?> container() {
return delegate.container();
}
@Override
public Class<T> valueClass() {
return delegate.valueClass();
}
@Override
public Patchwork extensionsData() {
return delegate.extensionsData();
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
delegate.visitInOrder(visitor, value);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
delegate.visitInOrder(visitor);
}
@Override
public T deepCopy(T value) {
return delegate.deepCopy(value);
}
}

View File

@@ -5,25 +5,22 @@ import de.siphalor.tweed5.attributesextension.api.AttributesRelatedExtension;
import de.siphalor.tweed5.attributesextension.api.serde.filter.AttributesReadWriteFilterExtension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.serde_api.api.DelegatingTweedDataWriter;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataUnsupportedValueException;
import de.siphalor.tweed5.serde_api.api.TweedDataVisitor;
import de.siphalor.tweed5.serde_api.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.utils.api.UniqueSymbol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -184,13 +181,7 @@ public class AttributesReadWriteFilterExtensionImpl
TweedEntryReader<Object, ConfigEntry<Object>> innerCasted
= (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return new TweedEntryReader<@Nullable Object, ConfigEntry<Object>>() {
@Override
public @Nullable Object read(
TweedDataReader reader,
ConfigEntry<Object> entry,
TweedReadContext context
) throws TweedEntryReadException {
return (TweedEntryReader<Object, ConfigEntry<Object>>) (reader, entry, context) -> {
ReadWriteContextCustomData contextData = context.extensionsData().get(readWriteContextDataAccess);
if (contextData == null) {
contextData = new ReadWriteContextCustomData();
@@ -198,19 +189,9 @@ public class AttributesReadWriteFilterExtensionImpl
}
if (!doFiltersMatch(entry, contextData)) {
TweedEntryReaderWriterImpls.NOOP_READER_WRITER.read(reader, entry, context);
return contextData.noopHandlerInstalled() ? NOOP_MARKER : null;
}
if (entry instanceof CompoundConfigEntry) {
CompoundConfigEntry<Object> delegated =
new AttributesFilteredCompoundEntry<>(((CompoundConfigEntry<Object>) entry));
boolean oldNoopHandlerInstalled = contextData.noopHandlerInstalled();
contextData.noopHandlerInstalled(true);
Object value = innerCasted.read(reader, delegated, context);
contextData.noopHandlerInstalled(oldNoopHandlerInstalled);
return value;
return TweedReadResult.empty();
}
return innerCasted.read(reader, entry, context);
}
};
}
}

View File

@@ -9,6 +9,7 @@ import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
import de.siphalor.tweed5.serde.hjson.HjsonWriter;
@@ -164,12 +165,14 @@ class AttributesReadWriteFilterExtensionImplTest {
}
}
""")));
Map<String, Object> readValue = configContainer.rootEntry().call(read(
TweedReadResult<Map<String, Object>> readResult = configContainer.rootEntry().call(read(
reader,
patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "type", "a")
));
assertThat(readValue)
assertThat(readResult)
.extracting(TweedReadResult::value)
.asInstanceOf(map(String.class, Object.class))
.containsEntry("first", "1st")
.doesNotContainKey("second")
.hasEntrySatisfying(

View File

@@ -7,6 +7,7 @@ import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
@@ -25,6 +26,7 @@ import static de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderW
import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.map;
public class AttributesReadWriteFilterExtensionImplWithPatchInfoTest {
@Test
@@ -65,7 +67,7 @@ public class AttributesReadWriteFilterExtensionImplWithPatchInfoTest {
var patchInfo = patchExtension.collectPatchInfo(readExtData);
filterExtension.addFilter(readExtData, "type", "a");
var value = readWriteExtension.read(
var readResult = readWriteExtension.read(
new HjsonReader(new HjsonLexer(new StringReader("""
{
"first": "FIRST",
@@ -76,8 +78,10 @@ public class AttributesReadWriteFilterExtensionImplWithPatchInfoTest {
readExtData
);
assertThat(value).extracting(v -> v.get("first")).isEqualTo("FIRST");
assertThat(value).extracting(v -> v.get("second")).isNull();
assertThat(readResult)
.extracting(TweedReadResult::value)
.asInstanceOf(map(String.class, Object.class))
.isEqualTo(Map.of("first", "FIRST"));
assertThat(patchInfo.containsEntry(firstEntry)).isTrue();
assertThat(patchInfo.containsEntry(secondEntry)).isFalse();
}

View File

@@ -3,12 +3,10 @@ package de.siphalor.tweed5.defaultextensions.patch.impl;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo;
import de.siphalor.tweed5.patchwork.api.Patchwork;
@@ -96,20 +94,13 @@ public class PatchExtensionImpl implements PatchExtension, ReadWriteRelatedExten
//noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> innerCasted =
(TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return new TweedEntryReader<@Nullable Object, ConfigEntry<Object>>() {
@Override
public @Nullable Object read(
TweedDataReader reader,
ConfigEntry<Object> entry,
TweedReadContext context
) throws TweedEntryReadException {
Object readValue = innerCasted.read(reader, entry, context);
return (TweedEntryReader<Object, ConfigEntry<Object>>) (reader, entry, context) -> {
TweedReadResult<Object> readResult = innerCasted.read(reader, entry, context);
ReadWriteContextCustomData customData = context.extensionsData().get(readWriteContextDataAccess);
if (customData != null && customData.patchInfo() != null) {
customData.patchInfo().addEntry(entry);
}
return readValue;
}
return readResult;
};
}
}

View File

@@ -70,21 +70,7 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
pathTracking = PathTracking.create();
context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
try {
return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context);
} catch (TweedEntryReadException e) {
val exceptionPathTracking = e.context().extensionsData().get(rwContextPathTrackingAccess);
if (exceptionPathTracking != null) {
throw new TweedEntryReadException(
"Exception while reading entry at "
+ exceptionPathTracking.currentPath()
+ ": " + e.getMessage(),
e
);
} else {
throw e;
}
}
};
}
};
@@ -116,7 +102,8 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
try {
castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context);
} catch (TweedEntryWriteException e) {
val exceptionPathTracking = e.context().extensionsData().get(rwContextPathTrackingAccess);
PathTracking exceptionPathTracking =
e.context().extensionsData().get(rwContextPathTrackingAccess);
if (exceptionPathTracking != null) {
throw new TweedEntryWriteException(
"Exception while writing entry at "

View File

@@ -3,7 +3,6 @@ package de.siphalor.tweed5.defaultextensions.readfallback.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
@@ -11,6 +10,7 @@ import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.defaultextensions.readfallback.api.ReadFallbackExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.NonNull;
@@ -30,8 +30,6 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
PresetsExtension presetsExtension = configContainer.extension(PresetsExtension.class)
.orElseThrow(() -> new IllegalStateException(getClass().getSimpleName()
+ " requires " + ReadFallbackExtension.class.getSimpleName()));
PatherExtension patherExtension = configContainer.extension(PatherExtension.class).orElse(null);
context.registerReaderMiddleware(new Middleware<TweedEntryReader<?, ?>>() {
@Override
public String id() {
@@ -53,23 +51,16 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
//noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> castedInner =
(TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context) -> {
try {
return castedInner.read(reader, entry, context);
} catch (TweedEntryReadException e) {
if (patherExtension == null) {
log.error("Failed to read entry: " + e.getMessage(), e);
} else {
log.error(
"Failed to read entry: " + e.getMessage()
+ " at " + patherExtension.getPath(context),
e
return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context) ->
castedInner.read(reader, entry, context).catchError(
issues -> {
Object fallback =
presetsExtension.presetValue(entry, PresetsExtension.DEFAULT_PRESET_NAME);
return TweedReadResult.withIssues(fallback, issues);
},
context
);
}
return presetsExtension.presetValue(entry, PresetsExtension.DEFAULT_PRESET_NAME);
}
};
}
});
}
}

View File

@@ -9,12 +9,6 @@ import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
@@ -30,6 +24,13 @@ import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssu
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadIssue;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import lombok.*;
import org.jspecify.annotations.Nullable;
@@ -233,8 +234,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
ValidationIssues validationIssues = getOrCreateValidationIssues(context.extensionsData());
Object value = castedInner.read(reader, entry, context);
return castedInner.read(reader, entry, context).andThen(value -> {
ConfigEntryValidator entryValidator = entry.extensionsData()
.get(customEntryDataAccess)
.completeValidator();
@@ -242,26 +242,39 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
ValidationResult<Object> validationResult = entryValidator.validate(entry, value);
if (!validationResult.issues().isEmpty()) {
if (validationResult.issues().isEmpty()) {
return TweedReadResult.ok(validationResult.value());
}
String path = patherExtension.getPath(context);
validationIssues.issuesByPath().put(path, new ValidationIssues.EntryIssues(
entry,
validationResult.issues()
));
}
if (validationResult.hasError()) {
throw new TweedEntryReadException(
"Failed to validate entry: " + validationResult.issues(),
context
return TweedReadResult.failed(
mapValidationIssuesToReadIssues(validationResult.issues(), context)
);
} else {
return TweedReadResult.withIssues(
validationResult.value(),
mapValidationIssuesToReadIssues(validationResult.issues(), context)
);
}
return validationResult.value();
}, context);
};
}
}
private static TweedReadIssue[] mapValidationIssuesToReadIssues(
Collection<ValidationIssue> issues,
TweedReadContext context
) {
return issues.stream().map(
validationIssue -> TweedReadIssue.error(validationIssue.message(), context)
).toArray(TweedReadIssue[]::new);
}
private ValidationIssues getOrCreateValidationIssues(Patchwork readContextExtensionsData) {
assert readContextValidationIssuesAccess != null;
ValidationIssues validationIssues = readContextExtensionsData.get(readContextValidationIssuesAccess);

View File

@@ -9,6 +9,7 @@ import de.siphalor.tweed5.core.impl.entry.NullableConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
@@ -150,7 +151,7 @@ class PatchExtensionImplTest {
PatchExtension patchExtension = configContainer.extension(PatchExtension.class).orElseThrow();
var patchInfo = new AtomicReference<@Nullable PatchInfo>();
Map<String, Object> patchValue = rootEntry.call(read(
TweedReadResult<Map<String, Object>> patchResult = rootEntry.call(read(
reader, extensionsData ->
patchInfo.set(patchExtension.collectPatchInfo(extensionsData))
));
@@ -158,7 +159,7 @@ class PatchExtensionImplTest {
Map<String, Object> resultValue = patchExtension.patch(
rootEntry,
baseValue,
patchValue,
patchResult.value(),
Objects.requireNonNull(patchInfo.get())
);

View File

@@ -1,16 +1,19 @@
package de.siphalor.tweed5.defaultextensions.readfallback.impl;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.defaultextensions.readfallback.api.ReadFallbackExtension;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import de.siphalor.tweed5.serde.extension.api.TweedWriteContext;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadIssue;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
@@ -19,9 +22,6 @@ import de.siphalor.tweed5.serde_api.api.TweedDataReadException;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataVisitor;
import de.siphalor.tweed5.serde_api.api.TweedDataWriteException;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.defaultextensions.readfallback.api.ReadFallbackExtension;
import de.siphalor.tweed5.testutils.generic.log.LogCaptureMockitoExtension;
import de.siphalor.tweed5.testutils.generic.log.LogsCaptor;
import org.jspecify.annotations.NullMarked;
@@ -34,12 +34,11 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension.presetValue;
import static de.siphalor.tweed5.serde.extension.api.ReadWriteExtension.entryReaderWriter;
import static de.siphalor.tweed5.serde.extension.api.ReadWriteExtension.read;
import static de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriters.compoundReaderWriter;
import static de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension.presetValue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
@ExtendWith(LogCaptureMockitoExtension.class)
@NullMarked
@@ -59,12 +58,18 @@ class ReadFallbackExtensionImplTest {
configContainer.attachTree(entry);
configContainer.initialize();
assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("12")))))).isEqualTo(12);
assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("12"))))))
.isEqualTo(TweedReadResult.ok(12));
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).isEmpty();
logsCaptor.clear();
assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("13")))))).isEqualTo(-1);
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).hasSize(1);
assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("13")))))).satisfies(
r -> assertThat(r).extracting(TweedReadResult::value).isEqualTo(-1),
r -> assertThat(r.issues()).singleElement()
.extracting(TweedReadIssue::exception)
.extracting(Throwable::getMessage)
.isEqualTo("Value is not even")
);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -105,6 +110,7 @@ class ReadFallbackExtensionImplTest {
assertThat(root.call(read(new HjsonReader(new HjsonLexer(new StringReader(
"{first: {second: 12}}"
))))))
.extracting(TweedReadResult::value)
.extracting(map -> (Map<String, Object>) map.get("first"))
.extracting(map -> (Integer) map.get("second"))
.isEqualTo(12);
@@ -114,22 +120,20 @@ class ReadFallbackExtensionImplTest {
assertThat(root.call(read(new HjsonReader(new HjsonLexer(new StringReader(
"{first: {second: 13}}"
))))))
.extracting(TweedReadResult::value)
.extracting(map -> (Map<String, Object>) map.get("first"))
.extracting(map -> (Integer) map.get("second"))
.isEqualTo(-1);
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).singleElement()
.extracting(ILoggingEvent::getMessage)
.asInstanceOf(STRING)
.contains("first.second");
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).isEmpty();
}
private static class EvenIntReader implements TweedEntryReaderWriter<Integer, ConfigEntry<Integer>> {
@Override
public Integer read(
public TweedReadResult<Integer> read(
TweedDataReader reader,
ConfigEntry<Integer> entry,
TweedReadContext context
) throws TweedEntryReadException {
) {
int value;
try {
value = reader.readToken().readAsInt();
@@ -137,9 +141,9 @@ class ReadFallbackExtensionImplTest {
throw new IllegalStateException("Should not be called", e);
}
if (value % 2 == 0) {
return value;
return TweedReadResult.ok(value);
} else {
throw new TweedEntryReadException("Value is not even", context);
return TweedReadResult.error(TweedReadIssue.error("Value is not even", context));
}
}

View File

@@ -7,6 +7,7 @@ import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonCommentType;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
@@ -156,12 +157,14 @@ class ValidationExtensionImplTest {
}
""")));
var validationIssues = new AtomicReference<@Nullable ValidationIssues>();
Map<String, Object> value = configContainer.rootEntry().call(read(
TweedReadResult<Map<String, Object>> value = configContainer.rootEntry().call(read(
reader,
extensionsData -> validationIssues.set(validationExtension.captureValidationIssues(extensionsData))
));
assertThat(value).isEqualTo(Map.of("byte", (byte) 11, "int", 123, "double", 0.5));
assertThat(value)
.extracting(TweedReadResult::value)
.isEqualTo(Map.of("byte", (byte) 11, "int", 123, "double", 0.5));
//noinspection DataFlowIssue
assertThat(validationIssues.get()).isNotNull().satisfies(
vi -> assertValidationIssue(

View File

@@ -5,6 +5,7 @@ import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonCommentType;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
@@ -30,6 +31,7 @@ import static de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension.
import static de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension.validators;
import static de.siphalor.tweed5.serde.extension.api.ReadWriteExtension.*;
import static de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriters.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ValidationFallbackExtensionImplTest {
@@ -114,8 +116,9 @@ class ValidationFallbackExtensionImplTest {
@ParameterizedTest
@ValueSource(strings = {"0", "7", "123", "null"})
void fallbackTriggers(String input) {
Integer result = intEntry.call(read(new HjsonReader(new HjsonLexer(new StringReader(input)))));
assertEquals(3, result);
TweedReadResult<Integer> result = intEntry.call(read(new HjsonReader(new HjsonLexer(new StringReader(input)))));
assertThat(result).extracting(TweedReadResult::value).isEqualTo(3);
assertThat(result.issues()).hasSize(1);
}
@Test

View File

@@ -2,6 +2,7 @@ package de.siphalor.tweed5.serde.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.serde.extension.impl.ReadWriteExtensionImpl;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
@@ -55,11 +56,11 @@ public interface ReadWriteExtension extends TweedExtension {
};
}
static <T extends @Nullable Object> Function<ConfigEntry<T>, T> read(TweedDataReader reader) {
static <T extends @Nullable Object> Function<ConfigEntry<T>, TweedReadResult<T>> read(TweedDataReader reader) {
return read(reader, null);
}
static <T extends @Nullable Object> Function<ConfigEntry<T>, T> read(
static <T extends @Nullable Object> Function<ConfigEntry<T>, TweedReadResult<T>> read(
TweedDataReader reader,
@Nullable Consumer<Patchwork> contextExtensionsDataCustomizer
) {
@@ -115,7 +116,7 @@ public interface ReadWriteExtension extends TweedExtension {
Patchwork createReadWriteContextExtensionsData();
<T extends @Nullable Object> T read(TweedDataReader reader, ConfigEntry<T> entry, Patchwork contextExtensionsData)
<T extends @Nullable Object> TweedReadResult<T> read(TweedDataReader reader, ConfigEntry<T> entry, Patchwork contextExtensionsData)
throws TweedEntryReadException;
<T extends @Nullable Object> void write(

View File

@@ -1,10 +1,11 @@
package de.siphalor.tweed5.serde.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import org.jspecify.annotations.Nullable;
@FunctionalInterface
public interface TweedEntryReader<T extends @Nullable Object, C extends ConfigEntry<T>> {
T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException;
TweedReadResult<T> read(TweedDataReader reader, C entry, TweedReadContext context);
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.serde.extension.api.read.result;
import org.jspecify.annotations.Nullable;
public interface ThrowingFunction<T extends @Nullable Object, R extends @Nullable Object> {
R apply(T value) throws Exception;
}

View File

@@ -0,0 +1,71 @@
package de.siphalor.tweed5.serde.extension.api.read.result;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@FunctionalInterface
public interface ThrowingReadFunction<T extends @Nullable Object, R extends @Nullable Object> {
static <T extends @Nullable Object, R extends @Nullable Object> ThrowingReadFunction<T, R> any(
TweedReadContext readContext,
ThrowingFunction<T, R>... functions
) {
//noinspection unchecked
ThrowingReadFunction<T, R>[] wrappedFunctions = new ThrowingReadFunction[functions.length];
for (int i = 0; i < functions.length; i++) {
wrappedFunctions[i] = wrap(readContext, functions[i]);
}
return any(wrappedFunctions);
}
static <T extends @Nullable Object, R extends @Nullable Object> ThrowingReadFunction<T, R> any(
ThrowingReadFunction<T, R>... functions
) {
if (functions.length == 0) {
throw new IllegalArgumentException("At least one function is required");
}
return value -> {
List<TweedReadIssue> issues = null;
boolean foundEmpty = false;
for (ThrowingReadFunction<T, R> function : functions) {
TweedReadResult<R> result = function.apply(value);
if (result.hasValue()) {
return result;
} else if (!result.isFailed()) {
foundEmpty = true;
}
if (result.hasIssues()) {
if (issues == null) {
issues = new ArrayList<>(functions.length);
issues.addAll(Arrays.asList(result.issues()));
}
}
}
if (foundEmpty) {
return TweedReadResult.empty();
}
if (issues != null) {
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
}
throw new IllegalStateException("Unreachable");
};
}
static <T extends @Nullable Object, R extends @Nullable Object> ThrowingReadFunction<T, R> wrap(
TweedReadContext readContext,
ThrowingFunction<T, R> function
) {
return value -> {
try {
return TweedReadResult.ok(function.apply(value));
} catch (Exception e) {
return TweedReadResult.failed(TweedReadIssue.error(e, readContext));
}
};
}
TweedReadResult<R> apply(T value) throws Exception;
}

View File

@@ -0,0 +1,26 @@
package de.siphalor.tweed5.serde.extension.api.read.result;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import lombok.*;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class TweedReadIssue {
@Getter
private final Exception exception;
private final TweedReadContext readContext;
public static TweedReadIssue error(String message, TweedReadContext readContext) {
return new TweedReadIssue(new Exception(message), readContext);
}
public static TweedReadIssue error(Exception exception, TweedReadContext readContext) {
return new TweedReadIssue(exception, readContext);
}
@Override
public String toString() {
return "TweedReadIssue(" + exception + ")";
}
}

View File

@@ -0,0 +1,168 @@
package de.siphalor.tweed5.serde.extension.api.read.result;
import de.siphalor.tweed5.serde.extension.api.TweedReadContext;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.Arrays;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(doNotUseGetters = true)
public class TweedReadResult<T extends @Nullable Object> {
private static final TweedReadResult<?> EMPTY = new TweedReadResult<>(null, false, null, Severity.OK);
private final T value;
private final boolean hasValue;
private final TweedReadIssue @Nullable [] issues;
private final Severity severity;
public static <T extends @Nullable Object> TweedReadResult<T> ok(T value) {
return new TweedReadResult<>(value, true, null, Severity.OK);
}
public static <T extends @Nullable Object> TweedReadResult<T> empty() {
//noinspection unchecked
return (TweedReadResult<T>) EMPTY;
}
public static <T extends @Nullable Object> TweedReadResult<T> withIssues(T value, TweedReadIssue... issues) {
return new TweedReadResult<>(value, true, issues, Severity.OK);
}
public static <T extends @Nullable Object> TweedReadResult<T> error(TweedReadIssue... issues) {
return new TweedReadResult<>(null, false, issues, Severity.ERROR);
}
public static <T extends @Nullable Object> TweedReadResult<T> failed(TweedReadIssue... issues) {
if (issues.length == 0) {
throw new IllegalArgumentException("At least one issue is required for a failed read result");
}
return new TweedReadResult<>(null, false, issues, Severity.FAILURE);
}
public T value() {
if (hasValue) {
return value;
}
throw new IllegalStateException("No value present");
}
public boolean hasValue() {
return hasValue;
}
public boolean hasIssues() {
return issues != null;
}
public TweedReadIssue [] issues() {
if (issues == null) {
return new TweedReadIssue[0];
}
return issues;
}
public boolean isError() {
return severity == Severity.ERROR;
}
public boolean isFailed() {
return severity == Severity.FAILURE;
}
public <R extends @Nullable Object> TweedReadResult<R> map(
ThrowingFunction<T, R> function,
TweedReadContext readContext
) {
return andThen(value -> ok(function.apply(value)), readContext);
}
public <R extends @Nullable Object> TweedReadResult<R> andThen(
ThrowingReadFunction<T, R> function,
TweedReadContext readContext
) {
if (severity != Severity.OK || !hasValue()) {
//noinspection unchecked
return (TweedReadResult<R>) this;
}
try {
TweedReadResult<R> innerResult = function.apply(value());
TweedReadIssue[] issues = combineIssues(issues(), innerResult.issues());
if (issues.length == 0) {
return innerResult;
}
if (innerResult.isFailed()) {
return failed(issues);
} else if (innerResult.isError()) {
return error(issues);
} else if (innerResult.hasValue()) {
return withIssues(innerResult.value(), issues);
} else {
return empty();
}
} catch (Exception exception) {
if (hasIssues()) {
TweedReadIssue[] issues = new TweedReadIssue[issues().length + 1];
System.arraycopy(issues(), 0, issues, 0, issues().length);
issues[issues().length] = TweedReadIssue.error(exception, readContext);
return failed(issues);
}
return failed(TweedReadIssue.error(exception, readContext));
}
}
public TweedReadResult<T> catchError(
ThrowingFunction<TweedReadIssue[], TweedReadResult<T>> errorHandler,
TweedReadContext readContext
) {
if (severity != Severity.ERROR) {
return this;
}
try {
return errorHandler.apply(issues());
} catch (Exception exception) {
TweedReadIssue[] issues = new TweedReadIssue[issues().length + 1];
System.arraycopy(issues(), 0, issues, 0, issues().length);
issues[issues().length] = TweedReadIssue.error(exception, readContext);
return error(issues);
}
}
private static TweedReadIssue[] combineIssues(TweedReadIssue[] a, TweedReadIssue[] b) {
if (a.length == 0) {
return b;
}
if (b.length == 0) {
return a;
}
TweedReadIssue[] result = new TweedReadIssue[a.length + b.length];
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
@Override
public String toString() {
if (severity == Severity.FAILURE) {
return "TweedReadResult#failed(issues=" + Arrays.toString(issues) + ')';
} else if (severity == Severity.ERROR) {
return "TweedReadResult#error(issues=" + Arrays.toString(issues) + ')';
} else if (hasValue) {
if (hasIssues()) {
return "TweedReadResult#withIssues(value=" + value + ", issues=" + Arrays.toString(issues) + ')';
} else {
return "TweedReadResult#ok(value=" + value + ')';
}
} else {
return "TweedReadResult#empty";
}
}
private enum Severity {
OK,
ERROR,
FAILURE,
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.serde.extension.api.read.result;
import org.jspecify.annotations.NullMarked;

View File

@@ -9,6 +9,7 @@ import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.serde.extension.api.*;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.serde.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde_api.api.TweedDataReader;
import de.siphalor.tweed5.serde_api.api.TweedDataVisitor;
import de.siphalor.tweed5.serde_api.api.TweedDataWriteException;
@@ -136,7 +137,7 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
return readWriteContextPatchworkFactory.create();
}
public <T extends @Nullable Object> T read(
public <T extends @Nullable Object> TweedReadResult<T> read(
TweedDataReader reader,
ConfigEntry<T> entry,
Patchwork contextExtensionsData

View File

@@ -5,6 +5,9 @@ import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.serde.extension.api.*;
import de.siphalor.tweed5.serde.extension.api.read.result.ThrowingReadFunction;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadIssue;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.serde_api.api.*;
import lombok.AccessLevel;
@@ -38,15 +41,14 @@ public class TweedEntryReaderWriterImpls {
public static class NullableReaderWriter<T extends @Nullable Object> implements TweedEntryReaderWriter<T, NullableConfigEntry<T>> {
@Override
public T read(TweedDataReader reader, NullableConfigEntry<T> entry, TweedReadContext context)
throws TweedEntryReadException {
public TweedReadResult<T> read(TweedDataReader reader, NullableConfigEntry<T> entry, TweedReadContext context) {
try {
if (reader.peekToken().isNull()) {
reader.readToken();
return null;
return TweedReadResult.ok(null);
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException(e, context);
return TweedReadResult.failed(TweedReadIssue.error(e, context));
}
TweedEntryReader<T, ConfigEntry<T>> nonNullReader = context.readWriteExtension().getReaderChain(entry.nonNullEntry());
@@ -75,14 +77,14 @@ public class TweedEntryReaderWriterImpls {
private final TweedEntryReader<T, C> delegate;
@Override
public T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException {
public TweedReadResult<T> read(TweedDataReader reader, C entry, TweedReadContext context) {
try {
if (reader.peekToken().isNull()) {
reader.readToken();
return null;
return TweedReadResult.ok(null);
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException(e, context);
return TweedReadResult.failed(TweedReadIssue.error(e, context));
}
return delegate.read(reader, entry, context);
}
@@ -109,12 +111,11 @@ public class TweedEntryReaderWriterImpls {
private final PrimitiveWriteFunction<T> writerFunction;
@Override
public T read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context)
throws TweedEntryReadException {
public TweedReadResult<T> read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) {
try {
return readerFunction.read(reader.readToken());
return TweedReadResult.ok(readerFunction.read(reader.readToken()));
} catch (TweedDataReadException e) {
throw new TweedEntryReadException(e, context);
return TweedReadResult.failed(TweedReadIssue.error(e, context));
}
}
@@ -128,20 +129,15 @@ public class TweedEntryReaderWriterImpls {
@RequiredArgsConstructor
public static class EnumReaderWriter<T extends Enum<?>> implements TweedEntryReaderWriter<T, ConfigEntry<T>> {
@Override
public T read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) throws
TweedEntryReadException {
try {
TweedDataToken token = reader.readToken();
assertIsToken(token, TweedDataToken::canReadAsString, "Expected string", context);
public TweedReadResult<T> read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) {
//noinspection unchecked,rawtypes
return (T) Enum.valueOf(((Class) entry.valueClass()), token.readAsString());
} catch (TweedDataReadException | IllegalArgumentException e) {
throw new TweedEntryReadException(
"Failed reading enum value for " + entry.valueClass().getName(),
e,
context
);
}
return requireToken(reader, TweedDataToken::canReadAsString, "Expected string", context)
.map(TweedDataToken::readAsString, context)
.andThen(ThrowingReadFunction.any(
context,
value -> (T) Enum.valueOf(((Class) entry.valueClass()), value),
value -> (T) Enum.valueOf(((Class) entry.valueClass()), value.toUpperCase(Locale.ROOT))
), context);
}
@Override
@@ -158,49 +154,46 @@ public class TweedEntryReaderWriterImpls {
public static class CollectionReaderWriter<T extends @Nullable Object, C extends Collection<T>> implements TweedEntryReaderWriter<C, CollectionConfigEntry<T, C>> {
@Override
public C read(TweedDataReader reader, CollectionConfigEntry<T, C> entry, TweedReadContext context) throws
TweedEntryReadException {
TweedDataToken token;
try {
assertIsToken(reader.readToken(), TweedDataToken::isListStart, "Expected list start", context);
token = reader.peekToken();
} catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed reading collection start", e, context);
}
if (token.isListEnd()) {
return entry.instantiateCollection(0);
public TweedReadResult<C> read(TweedDataReader reader, CollectionConfigEntry<T, C> entry, TweedReadContext context) {
return requireToken(reader, TweedDataToken::isListStart, "Expected list start", context).andThen(_token -> {
if (reader.peekToken().isListEnd()) {
return TweedReadResult.ok(entry.instantiateCollection(0));
}
ConfigEntry<T> elementEntry = entry.elementEntry();
TweedEntryReader<T, ConfigEntry<T>> elementReader = context.readWriteExtension().getReaderChain(elementEntry);
List<@Nullable T> list = new ArrayList<>(20);
List<T> list = new ArrayList<>(20);
List<TweedReadIssue> issues = new ArrayList<>();
while (true) {
try {
token = reader.peekToken();
TweedDataToken token = reader.peekToken();
if (token.isListEnd()) {
reader.readToken();
break;
} else if (token.isListValue()) {
list.add(elementReader.read(reader, elementEntry, context));
TweedReadResult<T> elementResult = elementReader.read(reader, elementEntry, context);
issues.addAll(Arrays.asList(elementResult.issues()));
if (elementResult.isFailed() || elementResult.isError()) {
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
} else if (elementResult.hasValue()) {
list.add(elementResult.value());
}
} else {
throw new TweedEntryReadException(
issues.add(TweedReadIssue.error(
"Unexpected token " + token + ": expected next list value or list end",
context
);
));
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException(
"Failed reading element " + list.size() + " of collection",
e,
context
);
issues.add(TweedReadIssue.error(e, context));
}
}
C result = entry.instantiateCollection(list.size());
result.addAll(list);
return result;
return TweedReadResult.ok(result);
}, context);
}
@Override
@@ -226,16 +219,11 @@ public class TweedEntryReaderWriterImpls {
public static class CompoundReaderWriter<T> implements TweedEntryReaderWriter<T, CompoundConfigEntry<T>> {
@Override
public T read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) throws
TweedEntryReadException {
try {
assertIsToken(reader.readToken(), TweedDataToken::isMapStart, "Expected map start", context);
} catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed reading compound start", e, context);
}
public TweedReadResult<T> read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) {
return requireToken(reader, TweedDataToken::isMapStart, "Expected map start", context).andThen(_token -> {
Map<String, ConfigEntry<?>> compoundEntries = entry.subEntries();
T compoundValue = entry.instantiateValue();
List<TweedReadIssue> issues = new ArrayList<>();
while (true) {
try {
TweedDataToken token = reader.readToken();
@@ -247,24 +235,38 @@ public class TweedEntryReaderWriterImpls {
//noinspection unchecked
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key);
if (subEntry == null) {
//noinspection DataFlowIssue
NOOP_READER_WRITER.read(reader, null, context);
TweedReadResult<Object> noopResult = NOOP_READER_WRITER.read(reader, null, context);
issues.addAll(Arrays.asList(noopResult.issues()));
if (noopResult.isFailed()) {
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
}
continue;
}
val subEntryReaderChain = context.readWriteExtension().getReaderChain(subEntry);
Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context);
entry.set(compoundValue, key, subEntryValue);
TweedReadResult<Object> subEntryResult = subEntryReaderChain.read(reader, subEntry, context);
issues.addAll(Arrays.asList(subEntryResult.issues()));
if (subEntryResult.isFailed() || subEntryResult.isError()) {
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
} else if (subEntryResult.hasValue()) {
entry.set(compoundValue, key, subEntryResult.value());
}
} else {
throw new TweedEntryReadException(
issues.add(TweedReadIssue.error(
"Unexpected token " + token + ": Expected map key or map end",
context
);
));
return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0]));
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed reading compound entry", e, context);
}
}
return compoundValue;
if (issues.isEmpty()) {
return TweedReadResult.ok(compoundValue);
} else {
return TweedReadResult.withIssues(compoundValue, issues.toArray(new TweedReadIssue[0]));
}
}, context);
}
@Override
@@ -291,12 +293,11 @@ public class TweedEntryReaderWriterImpls {
public static class NoopReaderWriter implements TweedEntryReaderWriter<@Nullable Object, ConfigEntry<Object>> {
@Override
public @Nullable Object read(TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context)
throws TweedEntryReadException {
public TweedReadResult<@Nullable Object> read(TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) {
try {
TweedDataToken token = reader.readToken();
if (!token.isListStart() && !token.isMapStart()) {
return null;
return TweedReadResult.empty();
}
ArrayDeque<Context> stack = new ArrayDeque<>(20);
@@ -316,11 +317,11 @@ public class TweedEntryReaderWriterImpls {
stack.pop();
}
if (stack.isEmpty()) {
return null;
return TweedReadResult.empty();
}
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed skipping through value", e, context);
return TweedReadResult.failed(TweedReadIssue.error(e, context));
}
}
@@ -352,14 +353,23 @@ public class TweedEntryReaderWriterImpls {
}
}
private static void assertIsToken(
TweedDataToken token,
private static TweedReadResult<TweedDataToken> requireToken(
TweedDataReader reader,
Predicate<TweedDataToken> isToken,
String description,
TweedReadContext context
) throws TweedEntryReadException {
if (!isToken.test(token)) {
throw new TweedEntryReadException("Unexpected token " + token + ": " + description, context);
) {
try {
TweedDataToken token = reader.readToken();
if (isToken.test(token)) {
return TweedReadResult.ok(token);
}
return TweedReadResult.failed(TweedReadIssue.error(
"Unexpected token " + token + ": " + description,
context
));
} catch (TweedDataReadException e) {
return TweedReadResult.failed(TweedReadIssue.error(e, context));
}
}
}

View File

@@ -28,7 +28,6 @@ import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ReadWriteExtensionImplTest {
private final StringWriter stringWriter = new StringWriter();
@@ -97,7 +96,7 @@ class ReadWriteExtensionImplTest {
@Test
void read() {
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class).orElseThrow();
Map<String, Object> result = assertDoesNotThrow(() -> readWriteExtension.read(
var result = assertDoesNotThrow(() -> readWriteExtension.read(
new HjsonReader(new HjsonLexer(new StringReader("""
{
\tint: 123
@@ -112,9 +111,12 @@ class ReadWriteExtensionImplTest {
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals(2, result.size());
assertEquals(123, result.get("int"));
assertEquals(Arrays.asList(true, false, true), result.get("list"));
assertThat(result.isFailed()).isFalse();
assertThat(result.hasValue()).isTrue();
assertThat(result.value()).isEqualTo(Map.of(
"int", 123,
"list", Arrays.asList(true, false, true)
));
}
private TweedDataVisitor setupWriter(Function<Writer, TweedDataVisitor> writerFactory) {

View File

@@ -7,6 +7,7 @@ import de.siphalor.tweed5.serde.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.serde.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.serde.extension.api.TweedReaderWriterProvider;
import de.siphalor.tweed5.serde.extension.api.TweedWriteContext;
import de.siphalor.tweed5.serde.extension.api.read.result.TweedReadResult;
import de.siphalor.tweed5.serde.hjson.HjsonLexer;
import de.siphalor.tweed5.serde.hjson.HjsonReader;
import de.siphalor.tweed5.serde.hjson.HjsonWriter;
@@ -55,7 +56,7 @@ class ReadWritePojoWeavingProcessorTest {
}"""
)));
assertThat(configContainer.rootEntry().call(read(reader)))
.isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
.isEqualTo(TweedReadResult.ok(new AnnotatedConfig(987, "abdef", new TestClass(29))));
}
@AutoService(TweedReaderWriterProvider.class)