From a8e89aaee8cf88cfeabdedc3c10494a5fcd8678a Mon Sep 17 00:00:00 2001 From: Siphalor Date: Mon, 10 Jun 2024 00:28:26 +0200 Subject: [PATCH] Validation fallback values, tests and fixes --- .../DefaultMiddlewareContainer.java | 6 +- .../impl/ValidationExtensionImpl.java | 8 +- .../api/ValidationFallbackExtension.java | 6 + .../api/ValidationFallbackValue.java | 5 + .../impl/ValidationFallbackExtensionImpl.java | 104 ++++++++++++ .../ValidationFallbackExtensionImplTest.java | 157 ++++++++++++++++++ .../readwrite/TweedEntryReaderWriters.java | 4 + .../impl/ReadWriteExtensionImpl.java | 5 + .../impl/TweedEntryReaderWriterImpls.java | 22 +++ 9 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackExtension.java create mode 100644 tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackValue.java create mode 100644 tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImpl.java create mode 100644 tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java diff --git a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/middleware/DefaultMiddlewareContainer.java b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/middleware/DefaultMiddlewareContainer.java index 6b81855..b3f9e1a 100644 --- a/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/middleware/DefaultMiddlewareContainer.java +++ b/tweed5-core/src/main/java/de/siphalor/tweed5/core/api/middleware/DefaultMiddlewareContainer.java @@ -98,7 +98,11 @@ public class DefaultMiddlewareContainer implements MiddlewareContainer { .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); } } 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 e5f98a8..9128f5c 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 @@ -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 validationEntryDataExtension; private MiddlewareContainer entryValidatorMiddlewareContainer; - private EntryValidationReaderMiddleware readerMiddleware; private RegisteredExtensionData 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> 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> entryReaderMiddleware() { - return readerMiddleware; + return new EntryValidationReaderMiddleware(); } @Override diff --git a/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackExtension.java b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackExtension.java new file mode 100644 index 0000000..f7a695d --- /dev/null +++ b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackExtension.java @@ -0,0 +1,6 @@ +package de.siphalor.tweed5.defaultextensions.validationfallback.api; + +import de.siphalor.tweed5.core.api.extension.TweedExtension; + +public interface ValidationFallbackExtension extends TweedExtension { +} diff --git a/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackValue.java b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackValue.java new file mode 100644 index 0000000..840ec11 --- /dev/null +++ b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/api/ValidationFallbackValue.java @@ -0,0 +1,5 @@ +package de.siphalor.tweed5.defaultextensions.validationfallback.api; + +public interface ValidationFallbackValue { + Object validationFallbackValue(); +} diff --git a/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImpl.java b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImpl.java new file mode 100644 index 0000000..5227c5e --- /dev/null +++ b/tweed5-default-extensions/src/main/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImpl.java @@ -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 validationMiddleware() { + return new ValidationFallbackMiddleware(); + } + + private static class ValidationFallbackMiddleware implements Middleware { + @Override + public String id() { + return "validation-fallback"; + } + + @Override + public Set mustComeBefore() { + return Collections.emptySet(); + } + + @Override + public Set mustComeAfter() { + return Collections.singleton("$default.end"); + } + + @Override + public ConfigEntryValidator process(ConfigEntryValidator inner) { + return new ConfigEntryValidator() { + @Override + public ValidationResult validate(ConfigEntry configEntry, T value) { + ValidationResult 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 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 String description(ConfigEntry configEntry) { + if (!configEntry.extensionsData().isPatchworkPartSet(ValidationFallbackValue.class)) { + return inner.description(configEntry); + } + return inner.description(configEntry) + + "\n\nDefault/Fallback value: " + + ((ValidationFallbackValue) configEntry.extensionsData()).validationFallbackValue(); + } + }; + } + } +} 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 new file mode 100644 index 0000000..f0f4ce5 --- /dev/null +++ b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validationfallback/impl/ValidationFallbackExtensionImplTest.java @@ -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 configContainer; + private CommentExtension commentExtension; + private ValidationExtension validationExtension; + private ValidationFallbackExtension validationFallbackExtension; + private ReadWriteExtension readWriteExtension; + private SimpleConfigEntryImpl 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 entrySpecificValidation = (RegisteredExtensionData) configContainer.entryDataExtensions().get(EntrySpecificValidation.class); + entrySpecificValidation.set(intEntry.extensionsData(), () -> Arrays.asList( + new SimpleValidatorMiddleware("non-null", new ConfigEntryValidator() { + @Override + public ValidationResult validate(ConfigEntry 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 String description(ConfigEntry configEntry) { + return "Must not be null."; + } + }), + new SimpleValidatorMiddleware("range", new ConfigEntryValidator() { + @Override + public ValidationResult validate(ConfigEntry 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 String description(ConfigEntry configEntry) { + return "Must be between 1 and 6"; + } + }) { + @Override + public Set mustComeAfter() { + return Collections.singleton("non-null"); + } + } + )); + + RegisteredExtensionData validationFallbackValue = (RegisteredExtensionData) configContainer.entryDataExtensions().get(ValidationFallbackValue.class); + validationFallbackValue.set(intEntry.extensionsData(), () -> 3); + + RegisteredExtensionData readerWriterData = (RegisteredExtensionData) 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()); + } +} \ No newline at end of file diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java index 4a44307..3908cc4 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/readwrite/TweedEntryReaderWriters.java @@ -43,6 +43,10 @@ public class TweedEntryReaderWriters { return TweedEntryReaderWriterImpls.STRING_READER_WRITER; } + public static > TweedEntryReaderWriter nullableReaderWriter(TweedEntryReaderWriter delegate) { + return new TweedEntryReaderWriterImpls.NullableReaderWriter<>(delegate); + } + public static > TweedEntryReaderWriter> coherentCollectionReaderWriter() { //noinspection unchecked return (TweedEntryReaderWriter>)(TweedEntryReaderWriter) TweedEntryReaderWriterImpls.COHERENT_COLLECTION_READER_WRITER; diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java index 58eee3a..4ed47b9 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java @@ -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 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); } diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java index efaa2d9..0d8b49e 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/TweedEntryReaderWriterImpls.java @@ -31,6 +31,28 @@ public class TweedEntryReaderWriterImpls { public static final TweedEntryReaderWriter> NOOP_READER_WRITER = new NoopReaderWriter(); + @RequiredArgsConstructor + public static class NullableReaderWriter> implements TweedEntryReaderWriter { + private final TweedEntryReaderWriter 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 implements TweedEntryReaderWriter> { private final Function readerCall;