fix(default-exts): Fix order of read fallback and validation extensions

This commit is contained in:
2025-12-19 21:27:23 +01:00
parent bcfd8879bb
commit ec3b216f0a
7 changed files with 239 additions and 48 deletions

View File

@@ -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/), 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). 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 ## [0.6.0] - 2025-12-14
### Added ### Added

View File

@@ -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.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware; 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.TweedEntryReadException;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader; 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.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; 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.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 lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
@@ -19,22 +20,17 @@ import java.util.Set;
@CommonsLog @CommonsLog
public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWriteRelatedExtension { public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWriteRelatedExtension {
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
private @Nullable PresetsExtension presetsExtension;
public ReadFallbackExtensionImpl(ConfigContainer<?> configContainer) { public ReadFallbackExtensionImpl(ConfigContainer<?> configContainer) {
this.configContainer = configContainer; this.configContainer = configContainer;
} }
@Override @Override
public void extensionsFinalized() { public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
presetsExtension = configContainer.extension(PresetsExtension.class) PresetsExtension presetsExtension = configContainer.extension(PresetsExtension.class)
.orElseThrow(() -> new IllegalStateException(getClass().getSimpleName() .orElseThrow(() -> new IllegalStateException(getClass().getSimpleName()
+ " requires " + ReadFallbackExtension.class.getSimpleName())); + " requires " + ReadFallbackExtension.class.getSimpleName()));
} PatherExtension patherExtension = configContainer.extension(PatherExtension.class).orElse(null);
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
assert presetsExtension != null;
context.registerReaderMiddleware(new Middleware<TweedEntryReader<?, ?>>() { context.registerReaderMiddleware(new Middleware<TweedEntryReader<?, ?>>() {
@Override @Override
@@ -44,12 +40,12 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
@Override @Override
public Set<String> mustComeBefore() { public Set<String> mustComeBefore() {
return Collections.singleton(DEFAULT_START); return Collections.singleton(PatherExtension.EXTENSION_ID);
} }
@Override @Override
public Set<String> mustComeAfter() { public Set<String> mustComeAfter() {
return Collections.emptySet(); return Collections.singleton(ValidationExtension.EXTENSION_ID);
} }
@Override @Override
@@ -57,11 +53,19 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
//noinspection unchecked //noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> castedInner = TweedEntryReader<Object, ConfigEntry<Object>> castedInner =
(TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner; (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context1) -> { return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context) -> {
try { try {
return castedInner.read(reader, entry, context1); return castedInner.read(reader, entry, context);
} catch (TweedEntryReadException e) { } 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); return presetsExtension.presetValue(entry, PresetsExtension.DEFAULT_PRESET_NAME);
} }
}; };

View File

@@ -219,7 +219,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
} }
@Override @Override
public Set<String> mustComeAfter() { public Set<String> mustComeBefore() {
return Collections.singleton(PatherExtension.EXTENSION_ID); return Collections.singleton(PatherExtension.EXTENSION_ID);
} }

View File

@@ -1,33 +1,51 @@
package de.siphalor.tweed5.defaultextensions.readfallback.impl; 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.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer; import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; 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.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReadException; import de.siphalor.tweed5.data.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.data.extension.api.TweedReadContext; import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext; 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.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.hjson.HjsonLexer; import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader; 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.presets.api.PresetsExtension;
import de.siphalor.tweed5.defaultextensions.readfallback.api.ReadFallbackExtension; 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.NullMarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.io.StringReader; 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.entryReaderWriter;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.read; 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 de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension.presetValue;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
@ExtendWith(LogCaptureMockitoExtension.class)
@NullMarked @NullMarked
class ReadFallbackExtensionImplTest { class ReadFallbackExtensionImplTest {
@Test @Test
void test() { void test(LogsCaptor<ReadFallbackExtensionImpl> logsCaptor) {
DefaultConfigContainer<Integer> configContainer = new DefaultConfigContainer<>(); DefaultConfigContainer<Integer> configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ReadWriteExtension.class); configContainer.registerExtension(ReadWriteExtension.class);
configContainer.registerExtension(PresetsExtension.class); configContainer.registerExtension(PresetsExtension.class);
@@ -36,41 +54,103 @@ class ReadFallbackExtensionImplTest {
ConfigEntry<Integer> entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class) ConfigEntry<Integer> entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(presetValue(PresetsExtension.DEFAULT_PRESET_NAME, -1)) .apply(presetValue(PresetsExtension.DEFAULT_PRESET_NAME, -1))
.apply(entryReaderWriter(new TweedEntryReaderWriter<>() { .apply(entryReaderWriter(new EvenIntReader()));
@Override
public Integer read(
TweedDataReader reader,
ConfigEntry<Integer> 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<Integer> entry,
TweedWriteContext context
) throws TweedDataWriteException {
throw new IllegalStateException("Should not be called");
}
}));
configContainer.attachTree(entry); configContainer.attachTree(entry);
configContainer.initialize(); 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(12);
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).isEmpty();
logsCaptor.clear();
assertThat(entry.call(read(new HjsonReader(new HjsonLexer(new StringReader("13")))))).isEqualTo(-1); 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<ReadFallbackExtensionImpl> logsCaptor) {
DefaultConfigContainer<Map<String, Object>> configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ReadWriteExtension.class);
configContainer.registerExtension(PatherExtension.class);
configContainer.registerExtension(PresetsExtension.class);
configContainer.registerExtension(ReadFallbackExtension.DEFAULT);
configContainer.finishExtensionSetup();
CompoundConfigEntry<Map<String, Object>> root = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (Class) Map.class,
HashMap::new,
Collections.singletonMap(
"first",
new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (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<String, Object>) 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<String, Object>) map.get("first"))
.extracting(map -> (Integer) map.get("second"))
.isEqualTo(-1);
assertThat(logsCaptor.getLogsForLevel(Level.ERROR)).singleElement()
.extracting(ILoggingEvent::getMessage)
.asInstanceOf(STRING)
.contains("first.second");
}
private static class EvenIntReader implements TweedEntryReaderWriter<Integer, ConfigEntry<Integer>> {
@Override
public Integer read(
TweedDataReader reader,
ConfigEntry<Integer> 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<Integer> entry,
TweedWriteContext context
) throws TweedDataWriteException {
throw new IllegalStateException("Should not be called");
}
} }
} }

View File

@@ -1,3 +1,9 @@
plugins { plugins {
id("de.siphalor.tweed5.test-utils") id("de.siphalor.tweed5.test-utils")
} }
dependencies {
implementation(libs.acl)
implementation(libs.slf4j.api)
implementation(libs.slf4j.rt)
}

View File

@@ -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<LogsCaptor<?>> appenders = new ArrayList<>();
}
}

View File

@@ -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<T> extends AppenderBase<ILoggingEvent> {
private final Logger logger;
private final List<ILoggingEvent> logs = new ArrayList<>();
public List<ILoggingEvent> 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);
}
}