Validation fallback values, tests and fixes

This commit is contained in:
2024-06-10 00:28:26 +02:00
parent d785389fee
commit a8e89aaee8
9 changed files with 310 additions and 7 deletions

View File

@@ -98,7 +98,11 @@ public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> {
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (AcyclicGraphSorter.GraphCycleException e) {
throw new IllegalStateException(e);
StringBuilder messageBuilder = new StringBuilder("Found cycle in middleware dependencies: ");
e.cycleIndeces().forEach(index -> messageBuilder.append(allMentionedMiddlewareIds[index]).append(" -> "));
messageBuilder.append(allMentionedMiddlewareIds[e.cycleIndeces().iterator().next()]);
throw new IllegalStateException(messageBuilder.toString(), e);
}
}

View File

@@ -34,7 +34,6 @@ import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResu
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.var;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -75,7 +74,6 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
private RegisteredExtensionData<EntryExtensionsData, InternalValidationEntryData> validationEntryDataExtension;
private MiddlewareContainer<ConfigEntryValidator> entryValidatorMiddlewareContainer;
private EntryValidationReaderMiddleware readerMiddleware;
private RegisteredExtensionData<ReadWriteContextExtensionsData, ValidationIssues> readContextValidationIssuesExtensionData;
@Override
@@ -97,8 +95,6 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
}
}
entryValidatorMiddlewareContainer.seal();
readerMiddleware = new EntryValidationReaderMiddleware();
}
@Override
@@ -143,7 +139,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
}
ConfigEntryValidator entryValidator;
var entrySpecificValidators = getEntrySpecificValidators(configEntry);
Collection<Middleware<ConfigEntryValidator>> entrySpecificValidators = getEntrySpecificValidators(configEntry);
if (entrySpecificValidators.isEmpty()) {
entryValidator = entryValidatorMiddlewareContainer.process(baseValidator);
} else {
@@ -166,7 +162,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
@Override
public @Nullable Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware() {
return readerMiddleware;
return new EntryValidationReaderMiddleware();
}
@Override

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.api;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
public interface ValidationFallbackExtension extends TweedExtension {
}

View File

@@ -0,0 +1,5 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.api;
public interface ValidationFallbackValue {
Object validationFallbackValue();
}

View File

@@ -0,0 +1,104 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.impl;
import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationProvidingExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackValue;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
@AutoService(ValidationFallbackExtension.class)
public class ValidationFallbackExtensionImpl implements ValidationFallbackExtension, ValidationProvidingExtension {
@Override
public String getId() {
return "validation-fallback";
}
@Override
public void setup(TweedExtensionSetupContext context) {
context.registerEntryExtensionData(ValidationFallbackValue.class);
}
@Override
public Middleware<ConfigEntryValidator> validationMiddleware() {
return new ValidationFallbackMiddleware();
}
private static class ValidationFallbackMiddleware implements Middleware<ConfigEntryValidator> {
@Override
public String id() {
return "validation-fallback";
}
@Override
public Set<String> mustComeBefore() {
return Collections.emptySet();
}
@Override
public Set<String> mustComeAfter() {
return Collections.singleton("$default.end");
}
@Override
public ConfigEntryValidator process(ConfigEntryValidator inner) {
return new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
ValidationResult<T> result = inner.validate(configEntry, value);
if (!result.hasError()) {
return result;
}
if (!configEntry.extensionsData().isPatchworkPartSet(ValidationFallbackValue.class)) {
return result;
}
Object fallbackValue = ((ValidationFallbackValue) configEntry.extensionsData()).validationFallbackValue();
if (fallbackValue != null) {
if (fallbackValue.getClass() == configEntry.valueClass()) {
//noinspection unchecked
fallbackValue = configEntry.deepCopy((T) fallbackValue);
} else {
ArrayList<ValidationIssue> issues = new ArrayList<>(result.issues());
issues.add(new ValidationIssue(
"Fallback value is not of correct class, expected " + configEntry.valueClass().getName() +
", but got " + fallbackValue.getClass().getName(),
ValidationIssueLevel.ERROR
));
return ValidationResult.withIssues(value, issues);
}
}
//noinspection unchecked
return ValidationResult.withIssues(
(T) fallbackValue,
result.issues().stream()
.map(issue -> new ValidationIssue(issue.message(), ValidationIssueLevel.WARN))
.collect(Collectors.toList())
);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
if (!configEntry.extensionsData().isPatchworkPartSet(ValidationFallbackValue.class)) {
return inner.description(configEntry);
}
return inner.description(configEntry) +
"\n\nDefault/Fallback value: " +
((ValidationFallbackValue) configEntry.extensionsData()).validationFallbackValue();
}
};
}
}
}

View File

@@ -0,0 +1,157 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.extension.impl.ReadWriteExtensionImpl;
import de.siphalor.tweed5.data.hjson.HjsonCommentType;
import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.impl.CommentExtensionImpl;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.EntrySpecificValidation;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.SimpleValidatorMiddleware;
import de.siphalor.tweed5.defaultextensions.validation.impl.ValidationExtensionImpl;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackValue;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ValidationFallbackExtensionImplTest {
private DefaultConfigContainer<Integer> configContainer;
private CommentExtension commentExtension;
private ValidationExtension validationExtension;
private ValidationFallbackExtension validationFallbackExtension;
private ReadWriteExtension readWriteExtension;
private SimpleConfigEntryImpl<Integer> intEntry;
@SuppressWarnings("unchecked")
@BeforeEach
void setUp() {
configContainer = new DefaultConfigContainer<>();
commentExtension = new CommentExtensionImpl();
configContainer.registerExtension(commentExtension);
validationExtension = new ValidationExtensionImpl();
configContainer.registerExtension(validationExtension);
validationFallbackExtension = new ValidationFallbackExtensionImpl();
configContainer.registerExtension(validationFallbackExtension);
readWriteExtension = new ReadWriteExtensionImpl();
configContainer.registerExtension(readWriteExtension);
configContainer.finishExtensionSetup();
intEntry = new SimpleConfigEntryImpl<>(Integer.class);
configContainer.attachAndSealTree(intEntry);
RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation> entrySpecificValidation = (RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation>) configContainer.entryDataExtensions().get(EntrySpecificValidation.class);
entrySpecificValidation.set(intEntry.extensionsData(), () -> Arrays.asList(
new SimpleValidatorMiddleware("non-null", new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
if (value == null) {
return ValidationResult.withIssues(null, Collections.singleton(
new ValidationIssue("Value must not be null", ValidationIssueLevel.ERROR)
));
}
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "Must not be null.";
}
}),
new SimpleValidatorMiddleware("range", new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
Integer intValue = (Integer) value;
if (intValue < 1) {
return ValidationResult.withIssues(value, Collections.singleton(new ValidationIssue("Must be greater or equal to 1", ValidationIssueLevel.ERROR)));
}
if (intValue > 6) {
return ValidationResult.withIssues(value, Collections.singleton(new ValidationIssue("Must be smaller or equal to 6", ValidationIssueLevel.ERROR)));
}
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "Must be between 1 and 6";
}
}) {
@Override
public Set<String> mustComeAfter() {
return Collections.singleton("non-null");
}
}
));
RegisteredExtensionData<EntryExtensionsData, ValidationFallbackValue> validationFallbackValue = (RegisteredExtensionData<EntryExtensionsData, ValidationFallbackValue>) configContainer.entryDataExtensions().get(ValidationFallbackValue.class);
validationFallbackValue.set(intEntry.extensionsData(), () -> 3);
RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(intEntry.extensionsData(), new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return TweedEntryReaderWriters.nullableReaderWriter(TweedEntryReaderWriters.intReaderWriter());
}
@Override
public TweedEntryWriter<?, ?> writer() {
return TweedEntryReaderWriters.nullableReaderWriter(TweedEntryReaderWriters.intReaderWriter());
}
});
configContainer.initialize();
}
@ParameterizedTest
@ValueSource(strings = {"0", "7", "123", "null"})
void fallbackTriggers(String input) {
Integer result = assertDoesNotThrow(() -> readWriteExtension.read(
new HjsonReader(new HjsonLexer(new StringReader(input))),
intEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals(3, result);
}
@Test
void description() {
StringWriter stringWriter = new StringWriter();
assertDoesNotThrow(() -> readWriteExtension.write(
new HjsonWriter(stringWriter, new HjsonWriter.Options().multilineCommentType(HjsonCommentType.SLASHES)),
5,
intEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals("// Must not be null.\n// Must be between 1 and 6\n// \n// Default/Fallback value: 3\n5\n", stringWriter.toString());
}
}

View File

@@ -43,6 +43,10 @@ public class TweedEntryReaderWriters {
return TweedEntryReaderWriterImpls.STRING_READER_WRITER;
}
public static <T, C extends ConfigEntry<T>> TweedEntryReaderWriter<T, C> nullableReaderWriter(TweedEntryReaderWriter<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableReaderWriter<>(delegate);
}
public static <T, C extends Collection<T>> TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T, C>> coherentCollectionReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<C, CoherentCollectionConfigEntry<T,C>>)(TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.COHERENT_COLLECTION_READER_WRITER;

View File

@@ -19,6 +19,7 @@ import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.Setter;
import lombok.Value;
@@ -91,6 +92,10 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
try {
readWriteContextExtensionsDataPatchwork = patchworkClassCreator.createClass(readWriteContextExtensionsDataClasses.keySet());
for (PatchworkClassPart patchworkClassPart : readWriteContextExtensionsDataPatchwork.parts()) {
RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, ?> registeredExtension = readWriteContextExtensionsDataClasses.get(patchworkClassPart.partInterface());
registeredExtension.setter = patchworkClassPart.fieldSetter();
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException("Failed to generate read write context extensions' data patchwork class", e);
}

View File

@@ -31,6 +31,28 @@ public class TweedEntryReaderWriterImpls {
public static final TweedEntryReaderWriter<Object, ConfigEntry<Object>> NOOP_READER_WRITER = new NoopReaderWriter();
@RequiredArgsConstructor
public static class NullableReaderWriter<T, C extends ConfigEntry<T>> implements TweedEntryReaderWriter<T, C> {
private final TweedEntryReaderWriter<T, C> delegate;
@Override
public T read(TweedDataReader reader, C entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
if (reader.peekToken().isNull()) {
return null;
}
return delegate.read(reader, entry, context);
}
@Override
public void write(TweedDataVisitor writer, T value, C entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
if (value == null) {
writer.visitNull();
} else {
delegate.write(writer, value, entry, context);
}
}
}
@RequiredArgsConstructor
private static class PrimitiveReaderWriter<T> implements TweedEntryReaderWriter<T, ConfigEntry<T>> {
private final Function<TweedDataToken, T> readerCall;