diff --git a/tweed5-minecraft/coat-bridge/src/main/java/de/siphalor/tweed5/coat/bridge/impl/TweedCoatMappersImpl.java b/tweed5-minecraft/coat-bridge/src/main/java/de/siphalor/tweed5/coat/bridge/impl/TweedCoatMappersImpl.java index 7882807..0a35471 100644 --- a/tweed5-minecraft/coat-bridge/src/main/java/de/siphalor/tweed5/coat/bridge/impl/TweedCoatMappersImpl.java +++ b/tweed5-minecraft/coat-bridge/src/main/java/de/siphalor/tweed5/coat/bridge/impl/TweedCoatMappersImpl.java @@ -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,64 +134,69 @@ 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 -> { - try { - TweedEntryReader> reader = - readWriteExtension.getDefinedEntryReader(entry); - Patchwork readExtData = readWriteExtension.createReadWriteContextExtensionsData(); - //noinspection DataFlowIssue - return reader.read( - new TweedDataReader() { - private boolean consumed = false; + return new ConfigEntryHandler() { + @Override + public String getDefault() { + return convertToString(context.defaultValue()); + } - @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 input; - } - }; - } - - @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 Collection getMessages(String text) { + try { + TweedReadResult 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())) + ); } - ); + } + + @Override + public void save(String s) { + try { + TweedReadResult 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 @@ -270,6 +276,57 @@ public class TweedCoatMappersImpl { } return wrapper[0]; } + + private TweedReadResult convertFromString(String text) { + TweedEntryReader> 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; + } + } + ); + } }; } } diff --git a/tweed5-minecraft/coat-bridge/src/testmod/java/de/siphalor/tweed5/coat/bridge/testmod/TweedCoatBridgeTestMod.java b/tweed5-minecraft/coat-bridge/src/testmod/java/de/siphalor/tweed5/coat/bridge/testmod/TweedCoatBridgeTestMod.java index 6e1a326..b41f83a 100644 --- a/tweed5-minecraft/coat-bridge/src/testmod/java/de/siphalor/tweed5/coat/bridge/testmod/TweedCoatBridgeTestMod.java +++ b/tweed5-minecraft/coat-bridge/src/testmod/java/de/siphalor/tweed5/coat/bridge/testmod/TweedCoatBridgeTestMod.java @@ -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); } diff --git a/tweed5-minecraft/fabric-helper/src/main/java/de/siphalor/tweed5/fabric/helper/api/FabricConfigContainerHelper.java b/tweed5-minecraft/fabric-helper/src/main/java/de/siphalor/tweed5/fabric/helper/api/FabricConfigContainerHelper.java index 4434c94..740f151 100644 --- a/tweed5-minecraft/fabric-helper/src/main/java/de/siphalor/tweed5/fabric/helper/api/FabricConfigContainerHelper.java +++ b/tweed5-minecraft/fabric-helper/src/main/java/de/siphalor/tweed5/fabric/helper/api/FabricConfigContainerHelper.java @@ -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 { 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 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 { 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 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( diff --git a/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesFilteredCompoundEntry.java b/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesFilteredCompoundEntry.java deleted file mode 100644 index 6d2ccbd..0000000 --- a/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesFilteredCompoundEntry.java +++ /dev/null @@ -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 implements CompoundConfigEntry { - private final CompoundConfigEntry 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> subEntries() { - return delegate.subEntries(); - } - - @Override - public ConfigContainer container() { - return delegate.container(); - } - - @Override - public Class 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); - } -} diff --git a/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java b/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java index 44d1ba9..7dd05e5 100644 --- a/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java +++ b/tweed5/attributes-extension/src/main/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImpl.java @@ -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,33 +181,17 @@ public class AttributesReadWriteFilterExtensionImpl TweedEntryReader> innerCasted = (TweedEntryReader>) inner; - return new TweedEntryReader<@Nullable Object, ConfigEntry>() { - @Override - public @Nullable Object read( - TweedDataReader reader, - ConfigEntry entry, - TweedReadContext context - ) throws TweedEntryReadException { - ReadWriteContextCustomData contextData = context.extensionsData().get(readWriteContextDataAccess); - if (contextData == null) { - contextData = new ReadWriteContextCustomData(); - context.extensionsData().set(readWriteContextDataAccess, contextData); - } - if (!doFiltersMatch(entry, contextData)) { - TweedEntryReaderWriterImpls.NOOP_READER_WRITER.read(reader, entry, context); - return contextData.noopHandlerInstalled() ? NOOP_MARKER : null; - } - if (entry instanceof CompoundConfigEntry) { - CompoundConfigEntry delegated = - new AttributesFilteredCompoundEntry<>(((CompoundConfigEntry) entry)); - boolean oldNoopHandlerInstalled = contextData.noopHandlerInstalled(); - contextData.noopHandlerInstalled(true); - Object value = innerCasted.read(reader, delegated, context); - contextData.noopHandlerInstalled(oldNoopHandlerInstalled); - return value; - } - return innerCasted.read(reader, entry, context); + return (TweedEntryReader>) (reader, entry, context) -> { + ReadWriteContextCustomData contextData = context.extensionsData().get(readWriteContextDataAccess); + if (contextData == null) { + contextData = new ReadWriteContextCustomData(); + context.extensionsData().set(readWriteContextDataAccess, contextData); } + if (!doFiltersMatch(entry, contextData)) { + TweedEntryReaderWriterImpls.NOOP_READER_WRITER.read(reader, entry, context); + return TweedReadResult.empty(); + } + return innerCasted.read(reader, entry, context); }; } } diff --git a/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java b/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java index ed87d57..da839c4 100644 --- a/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java +++ b/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java @@ -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 readValue = configContainer.rootEntry().call(read( + TweedReadResult> 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( diff --git a/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplWithPatchInfoTest.java b/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplWithPatchInfoTest.java index f0e2b68..b94fce0 100644 --- a/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplWithPatchInfoTest.java +++ b/tweed5/attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplWithPatchInfoTest.java @@ -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(); } diff --git a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImpl.java b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImpl.java index e371d88..075f2ba 100644 --- a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImpl.java +++ b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImpl.java @@ -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> innerCasted = (TweedEntryReader>) inner; - return new TweedEntryReader<@Nullable Object, ConfigEntry>() { - @Override - public @Nullable Object read( - TweedDataReader reader, - ConfigEntry entry, - TweedReadContext context - ) throws TweedEntryReadException { - Object readValue = innerCasted.read(reader, entry, context); - ReadWriteContextCustomData customData = context.extensionsData().get(readWriteContextDataAccess); - if (customData != null && customData.patchInfo() != null) { - customData.patchInfo().addEntry(entry); - } - return readValue; + return (TweedEntryReader>) (reader, entry, context) -> { + TweedReadResult readResult = innerCasted.read(reader, entry, context); + ReadWriteContextCustomData customData = context.extensionsData().get(readWriteContextDataAccess); + if (customData != null && customData.patchInfo() != null) { + customData.patchInfo().addEntry(entry); } + return readResult; }; } } diff --git a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/pather/impl/PatherExtensionImpl.java b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/pather/impl/PatherExtensionImpl.java index fcf82d5..1b1479d 100644 --- a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/pather/impl/PatherExtensionImpl.java +++ b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/pather/impl/PatherExtensionImpl.java @@ -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; - } - } + return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context); }; } }; @@ -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 " diff --git a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImpl.java b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImpl.java index 3353de3..11841a2 100644 --- a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImpl.java +++ b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImpl.java @@ -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>() { @Override public String id() { @@ -53,22 +51,15 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri //noinspection unchecked TweedEntryReader> castedInner = (TweedEntryReader>) inner; - return (TweedEntryReader>) (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 presetsExtension.presetValue(entry, PresetsExtension.DEFAULT_PRESET_NAME); - } - }; + return (TweedEntryReader>) (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 + ); } }); } diff --git a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImpl.java b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImpl.java index 1df3026..3847144 100644 --- a/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImpl.java +++ b/tweed5/default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImpl.java @@ -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,35 +234,47 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid return (TweedDataReader reader, ConfigEntry 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(); + assert entryValidator != null; - ConfigEntryValidator entryValidator = entry.extensionsData() - .get(customEntryDataAccess) - .completeValidator(); - assert entryValidator != null; + ValidationResult validationResult = entryValidator.validate(entry, value); - ValidationResult validationResult = entryValidator.validate(entry, value); + if (validationResult.issues().isEmpty()) { + return TweedReadResult.ok(validationResult.value()); + } - if (!validationResult.issues().isEmpty()) { 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 validationResult.value(); + if (validationResult.hasError()) { + return TweedReadResult.failed( + mapValidationIssuesToReadIssues(validationResult.issues(), context) + ); + } else { + return TweedReadResult.withIssues( + validationResult.value(), + mapValidationIssuesToReadIssues(validationResult.issues(), context) + ); + } + }, context); }; } } + private static TweedReadIssue[] mapValidationIssuesToReadIssues( + Collection 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); diff --git a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java index 4478309..8c52af0 100644 --- a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java +++ b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java @@ -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 patchValue = rootEntry.call(read( + TweedReadResult> patchResult = rootEntry.call(read( reader, extensionsData -> patchInfo.set(patchExtension.collectPatchInfo(extensionsData)) )); @@ -158,7 +159,7 @@ class PatchExtensionImplTest { Map resultValue = patchExtension.patch( rootEntry, baseValue, - patchValue, + patchResult.value(), Objects.requireNonNull(patchInfo.get()) ); diff --git a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImplTest.java b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImplTest.java index 40cf9da..bec8481 100644 --- a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImplTest.java +++ b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/readfallback/impl/ReadFallbackExtensionImplTest.java @@ -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) 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) 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> { @Override - public Integer read( + public TweedReadResult read( TweedDataReader reader, ConfigEntry 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)); } } diff --git a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java index 3744b66..78a6a36 100644 --- a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java +++ b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java @@ -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 value = configContainer.rootEntry().call(read( + TweedReadResult> 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( diff --git a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java index 8f507a6..11e81d1 100644 --- a/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java +++ b/tweed5/default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java @@ -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 result = intEntry.call(read(new HjsonReader(new HjsonLexer(new StringReader(input))))); + assertThat(result).extracting(TweedReadResult::value).isEqualTo(3); + assertThat(result.issues()).hasSize(1); } @Test diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/ReadWriteExtension.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/ReadWriteExtension.java index bd1f4bf..dad8132 100644 --- a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/ReadWriteExtension.java +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/ReadWriteExtension.java @@ -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 Function, T> read(TweedDataReader reader) { + static Function, TweedReadResult> read(TweedDataReader reader) { return read(reader, null); } - static Function, T> read( + static Function, TweedReadResult> read( TweedDataReader reader, @Nullable Consumer contextExtensionsDataCustomizer ) { @@ -115,7 +116,7 @@ public interface ReadWriteExtension extends TweedExtension { Patchwork createReadWriteContextExtensionsData(); - T read(TweedDataReader reader, ConfigEntry entry, Patchwork contextExtensionsData) + TweedReadResult read(TweedDataReader reader, ConfigEntry entry, Patchwork contextExtensionsData) throws TweedEntryReadException; void write( diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/TweedEntryReader.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/TweedEntryReader.java index ca72509..a3214e9 100644 --- a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/TweedEntryReader.java +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/TweedEntryReader.java @@ -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 read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException; + TweedReadResult read(TweedDataReader reader, C entry, TweedReadContext context); } diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingFunction.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingFunction.java new file mode 100644 index 0000000..14ee8a9 --- /dev/null +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingFunction.java @@ -0,0 +1,7 @@ +package de.siphalor.tweed5.serde.extension.api.read.result; + +import org.jspecify.annotations.Nullable; + +public interface ThrowingFunction { + R apply(T value) throws Exception; +} diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingReadFunction.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingReadFunction.java new file mode 100644 index 0000000..5298d23 --- /dev/null +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/ThrowingReadFunction.java @@ -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 { + static ThrowingReadFunction any( + TweedReadContext readContext, + ThrowingFunction... functions + ) { + //noinspection unchecked + ThrowingReadFunction[] wrappedFunctions = new ThrowingReadFunction[functions.length]; + for (int i = 0; i < functions.length; i++) { + wrappedFunctions[i] = wrap(readContext, functions[i]); + } + return any(wrappedFunctions); + } + + static ThrowingReadFunction any( + ThrowingReadFunction... functions + ) { + if (functions.length == 0) { + throw new IllegalArgumentException("At least one function is required"); + } + return value -> { + List issues = null; + boolean foundEmpty = false; + for (ThrowingReadFunction function : functions) { + TweedReadResult 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 ThrowingReadFunction wrap( + TweedReadContext readContext, + ThrowingFunction function + ) { + return value -> { + try { + return TweedReadResult.ok(function.apply(value)); + } catch (Exception e) { + return TweedReadResult.failed(TweedReadIssue.error(e, readContext)); + } + }; + } + + TweedReadResult apply(T value) throws Exception; +} diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadIssue.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadIssue.java new file mode 100644 index 0000000..0c154db --- /dev/null +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadIssue.java @@ -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 + ")"; + } +} diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadResult.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadResult.java new file mode 100644 index 0000000..3d60854 --- /dev/null +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/TweedReadResult.java @@ -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 { + 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 TweedReadResult ok(T value) { + return new TweedReadResult<>(value, true, null, Severity.OK); + } + + public static TweedReadResult empty() { + //noinspection unchecked + return (TweedReadResult) EMPTY; + } + + public static TweedReadResult withIssues(T value, TweedReadIssue... issues) { + return new TweedReadResult<>(value, true, issues, Severity.OK); + } + + public static TweedReadResult error(TweedReadIssue... issues) { + return new TweedReadResult<>(null, false, issues, Severity.ERROR); + } + + public static TweedReadResult 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 TweedReadResult map( + ThrowingFunction function, + TweedReadContext readContext + ) { + return andThen(value -> ok(function.apply(value)), readContext); + } + + public TweedReadResult andThen( + ThrowingReadFunction function, + TweedReadContext readContext + ) { + if (severity != Severity.OK || !hasValue()) { + //noinspection unchecked + return (TweedReadResult) this; + } + try { + TweedReadResult 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 catchError( + ThrowingFunction> 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, + } +} diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/package-info.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/package-info.java new file mode 100644 index 0000000..5910bc9 --- /dev/null +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/api/read/result/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.serde.extension.api.read.result; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImpl.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImpl.java index fe44a72..4956e66 100644 --- a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImpl.java +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImpl.java @@ -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 read( + public TweedReadResult read( TweedDataReader reader, ConfigEntry entry, Patchwork contextExtensionsData diff --git a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/TweedEntryReaderWriterImpls.java b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/TweedEntryReaderWriterImpls.java index 6d6158a..2f61ee5 100644 --- a/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/TweedEntryReaderWriterImpls.java +++ b/tweed5/serde-extension/src/main/java/de/siphalor/tweed5/serde/extension/impl/TweedEntryReaderWriterImpls.java @@ -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 implements TweedEntryReaderWriter> { @Override - public T read(TweedDataReader reader, NullableConfigEntry entry, TweedReadContext context) - throws TweedEntryReadException { + public TweedReadResult read(TweedDataReader reader, NullableConfigEntry 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> nonNullReader = context.readWriteExtension().getReaderChain(entry.nonNullEntry()); @@ -75,14 +77,14 @@ public class TweedEntryReaderWriterImpls { private final TweedEntryReader delegate; @Override - public T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException { + public TweedReadResult 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 writerFunction; @Override - public T read(TweedDataReader reader, ConfigEntry entry, TweedReadContext context) - throws TweedEntryReadException { + public TweedReadResult read(TweedDataReader reader, ConfigEntry 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> implements TweedEntryReaderWriter> { @Override - public T read(TweedDataReader reader, ConfigEntry entry, TweedReadContext context) throws - TweedEntryReadException { - try { - TweedDataToken token = reader.readToken(); - assertIsToken(token, TweedDataToken::canReadAsString, "Expected string", 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 - ); - } + public TweedReadResult read(TweedDataReader reader, ConfigEntry entry, TweedReadContext context) { + //noinspection unchecked,rawtypes + 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> implements TweedEntryReaderWriter> { @Override - public C read(TweedDataReader reader, CollectionConfigEntry 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); - } - - ConfigEntry elementEntry = entry.elementEntry(); - TweedEntryReader> elementReader = context.readWriteExtension().getReaderChain(elementEntry); - - List<@Nullable T> list = new ArrayList<>(20); - while (true) { - try { - token = reader.peekToken(); - if (token.isListEnd()) { - reader.readToken(); - break; - } else if (token.isListValue()) { - list.add(elementReader.read(reader, elementEntry, context)); - } else { - throw new TweedEntryReadException( - "Unexpected token " + token + ": expected next list value or list end", - context - ); - } - } catch (TweedDataReadException e) { - throw new TweedEntryReadException( - "Failed reading element " + list.size() + " of collection", - e, - context - ); + public TweedReadResult read(TweedDataReader reader, CollectionConfigEntry entry, TweedReadContext context) { + return requireToken(reader, TweedDataToken::isListStart, "Expected list start", context).andThen(_token -> { + if (reader.peekToken().isListEnd()) { + return TweedReadResult.ok(entry.instantiateCollection(0)); } - } - C result = entry.instantiateCollection(list.size()); - result.addAll(list); - return result; + ConfigEntry elementEntry = entry.elementEntry(); + TweedEntryReader> elementReader = context.readWriteExtension().getReaderChain(elementEntry); + + List list = new ArrayList<>(20); + List issues = new ArrayList<>(); + while (true) { + try { + TweedDataToken token = reader.peekToken(); + if (token.isListEnd()) { + reader.readToken(); + break; + } else if (token.isListValue()) { + TweedReadResult 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 { + 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) { + issues.add(TweedReadIssue.error(e, context)); + } + } + C result = entry.instantiateCollection(list.size()); + result.addAll(list); + return TweedReadResult.ok(result); + }, context); } @Override @@ -226,45 +219,54 @@ public class TweedEntryReaderWriterImpls { public static class CompoundReaderWriter implements TweedEntryReaderWriter> { @Override - public T read(TweedDataReader reader, CompoundConfigEntry 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 read(TweedDataReader reader, CompoundConfigEntry entry, TweedReadContext context) { + return requireToken(reader, TweedDataToken::isMapStart, "Expected map start", context).andThen(_token -> { + Map> compoundEntries = entry.subEntries(); + T compoundValue = entry.instantiateValue(); + List issues = new ArrayList<>(); + while (true) { + try { + TweedDataToken token = reader.readToken(); + if (token.isMapEnd()) { + break; + } else if (token.isMapEntryKey()) { + String key = token.readAsString(); - Map> compoundEntries = entry.subEntries(); - T compoundValue = entry.instantiateValue(); - while (true) { - try { - TweedDataToken token = reader.readToken(); - if (token.isMapEnd()) { - break; - } else if (token.isMapEntryKey()) { - String key = token.readAsString(); - - //noinspection unchecked - ConfigEntry subEntry = (ConfigEntry) compoundEntries.get(key); - if (subEntry == null) { - //noinspection DataFlowIssue - NOOP_READER_WRITER.read(reader, null, context); - continue; + //noinspection unchecked + ConfigEntry subEntry = (ConfigEntry) compoundEntries.get(key); + if (subEntry == null) { + TweedReadResult 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); + TweedReadResult 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 { + issues.add(TweedReadIssue.error( + "Unexpected token " + token + ": Expected map key or map end", + context + )); + return TweedReadResult.failed(issues.toArray(new TweedReadIssue[0])); } - val subEntryReaderChain = context.readWriteExtension().getReaderChain(subEntry); - Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context); - entry.set(compoundValue, key, subEntryValue); - } else { - throw new TweedEntryReadException( - "Unexpected token " + token + ": Expected map key or map end", - context - ); + } catch (TweedDataReadException e) { + throw new TweedEntryReadException("Failed reading compound entry", e, context); } - } 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> { @Override - public @Nullable Object read(TweedDataReader reader, ConfigEntry entry, TweedReadContext context) - throws TweedEntryReadException { + public TweedReadResult<@Nullable Object> read(TweedDataReader reader, ConfigEntry entry, TweedReadContext context) { try { TweedDataToken token = reader.readToken(); if (!token.isListStart() && !token.isMapStart()) { - return null; + return TweedReadResult.empty(); } ArrayDeque 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 requireToken( + TweedDataReader reader, Predicate 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)); } } } diff --git a/tweed5/serde-extension/src/test/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImplTest.java b/tweed5/serde-extension/src/test/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImplTest.java index 231d356..d829452 100644 --- a/tweed5/serde-extension/src/test/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImplTest.java +++ b/tweed5/serde-extension/src/test/java/de/siphalor/tweed5/serde/extension/impl/ReadWriteExtensionImplTest.java @@ -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 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 writerFactory) { diff --git a/tweed5/weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java b/tweed5/weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java index eb81921..6f4b463 100644 --- a/tweed5/weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java +++ b/tweed5/weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java @@ -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)