fix(default-exts): Fix order of read fallback and validation extensions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<TweedEntryReader<?, ?>>() {
|
||||
@Override
|
||||
@@ -44,12 +40,12 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
|
||||
|
||||
@Override
|
||||
public Set<String> mustComeBefore() {
|
||||
return Collections.singleton(DEFAULT_START);
|
||||
return Collections.singleton(PatherExtension.EXTENSION_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> mustComeAfter() {
|
||||
return Collections.emptySet();
|
||||
return Collections.singleton(ValidationExtension.EXTENSION_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,11 +53,19 @@ public class ReadFallbackExtensionImpl implements ReadFallbackExtension, ReadWri
|
||||
//noinspection unchecked
|
||||
TweedEntryReader<Object, ConfigEntry<Object>> castedInner =
|
||||
(TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
|
||||
return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context1) -> {
|
||||
return (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) (reader, entry, context) -> {
|
||||
try {
|
||||
return castedInner.read(reader, entry, context1);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,7 +219,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> mustComeAfter() {
|
||||
public Set<String> mustComeBefore() {
|
||||
return Collections.singleton(PatherExtension.EXTENSION_ID);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ReadFallbackExtensionImpl> logsCaptor) {
|
||||
DefaultConfigContainer<Integer> configContainer = new DefaultConfigContainer<>();
|
||||
configContainer.registerExtension(ReadWriteExtension.class);
|
||||
configContainer.registerExtension(PresetsExtension.class);
|
||||
@@ -36,7 +54,76 @@ class ReadFallbackExtensionImplTest {
|
||||
|
||||
ConfigEntry<Integer> entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
|
||||
.apply(presetValue(PresetsExtension.DEFAULT_PRESET_NAME, -1))
|
||||
.apply(entryReaderWriter(new TweedEntryReaderWriter<>() {
|
||||
.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<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,
|
||||
@@ -65,12 +152,5 @@ class ReadFallbackExtensionImplTest {
|
||||
) throws TweedDataWriteException {
|
||||
throw new IllegalStateException("Should not be called");
|
||||
}
|
||||
}));
|
||||
|
||||
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("13")))))).isEqualTo(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id("de.siphalor.tweed5.test-utils")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.acl)
|
||||
implementation(libs.slf4j.api)
|
||||
implementation(libs.slf4j.rt)
|
||||
}
|
||||
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user