From ec3b216f0a9d85afa662fee7c00f5b8c407959b3 Mon Sep 17 00:00:00 2001 From: Siphalor Date: Fri, 19 Dec 2025 21:27:23 +0100 Subject: [PATCH] fix(default-exts): Fix order of read fallback and validation extensions --- CHANGELOG.md | 8 + .../impl/ReadFallbackExtensionImpl.java | 34 +++-- .../impl/ValidationExtensionImpl.java | 2 +- .../impl/ReadFallbackExtensionImplTest.java | 144 ++++++++++++++---- tweed5/test-utils/generic/build.gradle.kts | 6 + .../log/LogCaptureMockitoExtension.java | 60 ++++++++ .../testutils/generic/log/LogsCaptor.java | 33 ++++ 7 files changed, 239 insertions(+), 48 deletions(-) create mode 100644 tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogCaptureMockitoExtension.java create mode 100644 tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogsCaptor.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b0ef3..6a1c7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Changed +- **Breaking**: Inverted the order in which middlewares are applied. + +### Fixed +- `default-extensions`: Fixed `ReadFallbackExtension` being applied too late. + ## [0.6.0] - 2025-12-14 ### Added 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 bcd74b8..1cb7157 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,15 +3,16 @@ 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.defaultextensions.readfallback.api.ReadFallbackExtension; import de.siphalor.tweed5.data.extension.api.TweedEntryReadException; import de.siphalor.tweed5.data.extension.api.TweedEntryReader; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; +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 lombok.extern.apachecommons.CommonsLog; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.Set; @@ -19,22 +20,17 @@ import java.util.Set; @CommonsLog public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWriteRelatedExtension { private final ConfigContainer configContainer; - private @Nullable PresetsExtension presetsExtension; public ReadFallbackExtensionImpl(ConfigContainer configContainer) { this.configContainer = configContainer; } @Override - public void extensionsFinalized() { - presetsExtension = configContainer.extension(PresetsExtension.class) + public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) { + PresetsExtension presetsExtension = configContainer.extension(PresetsExtension.class) .orElseThrow(() -> new IllegalStateException(getClass().getSimpleName() + " requires " + ReadFallbackExtension.class.getSimpleName())); - } - - @Override - public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) { - assert presetsExtension != null; + PatherExtension patherExtension = configContainer.extension(PatherExtension.class).orElse(null); context.registerReaderMiddleware(new Middleware>() { @Override @@ -44,12 +40,12 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri @Override public Set mustComeBefore() { - return Collections.singleton(DEFAULT_START); + return Collections.singleton(PatherExtension.EXTENSION_ID); } @Override public Set mustComeAfter() { - return Collections.emptySet(); + return Collections.singleton(ValidationExtension.EXTENSION_ID); } @Override @@ -57,11 +53,19 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri //noinspection unchecked TweedEntryReader> castedInner = (TweedEntryReader>) inner; - return (TweedEntryReader>) (reader, entry, context1) -> { + return (TweedEntryReader>) (reader, entry, context) -> { try { - return castedInner.read(reader, entry, context1); + return castedInner.read(reader, entry, context); } catch (TweedEntryReadException e) { - log.error("Failed to read entry: " + e.getMessage(), 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); } }; 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 8374527..0055823 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 @@ -219,7 +219,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid } @Override - public Set mustComeAfter() { + public Set mustComeBefore() { return Collections.singleton(PatherExtension.EXTENSION_ID); } 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 637f6f9..8bb7fd3 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,33 +1,51 @@ 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.data.extension.api.ReadWriteExtension; import de.siphalor.tweed5.data.extension.api.TweedEntryReadException; import de.siphalor.tweed5.data.extension.api.TweedReadContext; import de.siphalor.tweed5.data.extension.api.TweedWriteContext; import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter; +import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters; import de.siphalor.tweed5.data.hjson.HjsonLexer; import de.siphalor.tweed5.data.hjson.HjsonReader; -import de.siphalor.tweed5.dataapi.api.*; +import de.siphalor.tweed5.dataapi.api.TweedDataReadException; +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; +import de.siphalor.tweed5.dataapi.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; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import java.io.StringReader; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.read; +import static de.siphalor.tweed5.data.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 class ReadFallbackExtensionImplTest { @Test - void test() { + void test(LogsCaptor logsCaptor) { DefaultConfigContainer configContainer = new DefaultConfigContainer<>(); configContainer.registerExtension(ReadWriteExtension.class); configContainer.registerExtension(PresetsExtension.class); @@ -36,41 +54,103 @@ class ReadFallbackExtensionImplTest { ConfigEntry entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class) .apply(presetValue(PresetsExtension.DEFAULT_PRESET_NAME, -1)) - .apply(entryReaderWriter(new TweedEntryReaderWriter<>() { - @Override - public Integer read( - TweedDataReader reader, - ConfigEntry entry, - TweedReadContext context - ) throws TweedEntryReadException { - int value; - try { - value = reader.readToken().readAsInt(); - } catch (TweedDataReadException e) { - throw new IllegalStateException("Should not be called", e); - } - if (value % 2 == 0) { - return value; - } else { - throw new TweedEntryReadException("Value is not even", context); - } - } - - @Override - public void write( - TweedDataVisitor writer, - @Nullable Integer value, - ConfigEntry entry, - TweedWriteContext context - ) throws TweedDataWriteException { - throw new IllegalStateException("Should not be called"); - } - })); + .apply(entryReaderWriter(new EvenIntReader())); configContainer.attachTree(entry); configContainer.initialize(); assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("12")))))).isEqualTo(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); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + void nestedWithPather(LogsCaptor logsCaptor) { + DefaultConfigContainer> configContainer = new DefaultConfigContainer<>(); + configContainer.registerExtension(ReadWriteExtension.class); + configContainer.registerExtension(PatherExtension.class); + configContainer.registerExtension(PresetsExtension.class); + configContainer.registerExtension(ReadFallbackExtension.DEFAULT); + configContainer.finishExtensionSetup(); + + CompoundConfigEntry> root = new StaticMapCompoundConfigEntryImpl<>( + configContainer, + (Class>) (Class) Map.class, + HashMap::new, + Collections.singletonMap( + "first", + new StaticMapCompoundConfigEntryImpl<>( + configContainer, + (Class>) (Class) Map.class, + HashMap::new, + Collections.singletonMap( + "second", + new SimpleConfigEntryImpl<>( + configContainer, + Integer.class + ).apply(presetValue(PresetsExtension.DEFAULT_PRESET_NAME, -1)) + .apply(entryReaderWriter(new EvenIntReader())) + ) + ).apply(entryReaderWriter(TweedEntryReaderWriters.compoundReaderWriter())) + ) + ).apply(entryReaderWriter(compoundReaderWriter())); + + configContainer.attachTree(root); + configContainer.initialize(); + + assertThat(root.call(read(new HjsonReader(new HjsonLexer(new StringReader( + "{first: {second: 12}}" + )))))) + .extracting(map -> (Map) map.get("first")) + .extracting(map -> (Integer) map.get("second")) + .isEqualTo(12); + assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).isEmpty(); + logsCaptor.clear(); + + assertThat(root.call(read(new HjsonReader(new HjsonLexer(new StringReader( + "{first: {second: 13}}" + )))))) + .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"); + } + + private static class EvenIntReader implements TweedEntryReaderWriter> { + @Override + public Integer read( + TweedDataReader reader, + ConfigEntry entry, + TweedReadContext context + ) throws TweedEntryReadException { + int value; + try { + value = reader.readToken().readAsInt(); + } catch (TweedDataReadException e) { + throw new IllegalStateException("Should not be called", e); + } + if (value % 2 == 0) { + return value; + } else { + throw new TweedEntryReadException("Value is not even", context); + } + } + + @Override + public void write( + TweedDataVisitor writer, + @Nullable Integer value, + ConfigEntry entry, + TweedWriteContext context + ) throws TweedDataWriteException { + throw new IllegalStateException("Should not be called"); + } } } diff --git a/tweed5/test-utils/generic/build.gradle.kts b/tweed5/test-utils/generic/build.gradle.kts index 25c9399..d70af1d 100644 --- a/tweed5/test-utils/generic/build.gradle.kts +++ b/tweed5/test-utils/generic/build.gradle.kts @@ -1,3 +1,9 @@ plugins { id("de.siphalor.tweed5.test-utils") } + +dependencies { + implementation(libs.acl) + implementation(libs.slf4j.api) + implementation(libs.slf4j.rt) +} diff --git a/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogCaptureMockitoExtension.java b/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogCaptureMockitoExtension.java new file mode 100644 index 0000000..aedac77 --- /dev/null +++ b/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogCaptureMockitoExtension.java @@ -0,0 +1,60 @@ +package de.siphalor.tweed5.testutils.generic.log; + +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.extension.*; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; + +public class LogCaptureMockitoExtension implements Extension, ParameterResolver, AfterEachCallback { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws + ParameterResolutionException { + return parameterContext.getParameter().getType() == LogsCaptor.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws + ParameterResolutionException { + Type logsCaptorType = parameterContext.getParameter().getParameterizedType(); + if (logsCaptorType instanceof ParameterizedType logsCaptorParameterizedType) { + Type targetType = logsCaptorParameterizedType.getActualTypeArguments()[0]; + Class targetClass = (Class) targetType; + Logger logger = (Logger) LoggerFactory.getLogger(targetClass); + + logger.info("Resolved logger to {}", Objects.toIdentityString(logger)); + + LogsCaptor appender = new LogsCaptor<>(logger); + appender.setName("test log appender/" + targetClass.getName()); + appender.start(); + + getStoreData(extensionContext).appenders.add(appender); + + logger.addAppender(appender); + + return appender; + } + + throw new ParameterResolutionException("Failed to resolve parameter " + parameterContext.getParameter()); + } + + @Override + public void afterEach(ExtensionContext context) { + for (LogsCaptor appender : getStoreData(context).appenders) { + appender.detach(); + } + } + + private StoreData getStoreData(ExtensionContext extensionContext) { + return extensionContext.getStore(ExtensionContext.Namespace.create(getClass(), extensionContext.getRequiredTestMethod())) + .getOrComputeIfAbsent(StoreData.class, k -> new StoreData(), StoreData.class); + } + + private static class StoreData { + private final Collection> appenders = new ArrayList<>(); + } +} diff --git a/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogsCaptor.java b/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogsCaptor.java new file mode 100644 index 0000000..d786d7d --- /dev/null +++ b/tweed5/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/log/LogsCaptor.java @@ -0,0 +1,33 @@ +package de.siphalor.tweed5.testutils.generic.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +public class LogsCaptor extends AppenderBase { + private final Logger logger; + private final List logs = new ArrayList<>(); + + public List getLogsForLevel(Level level) { + return logs.stream().filter(log -> log.getLevel().equals(level)).toList(); + } + + @Override + protected void append(ILoggingEvent event) { + logs.add(event); + } + + public void clear() { + logs.clear(); + } + + public void detach() { + logger.detachAppender(this); + } +}