Refactored validation and stuff

This commit is contained in:
2024-06-09 18:51:10 +02:00
parent b0f35b03b9
commit 31d905b065
42 changed files with 1309 additions and 119 deletions

View File

@@ -4,7 +4,7 @@ import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -22,12 +22,12 @@ class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?
//noinspection unchecked
TweedEntryWriter<Object, ConfigEntry<Object>> innerCasted = (TweedEntryWriter<Object, ConfigEntry<Object>>) inner;
return (TweedEntryWriter<Object, ConfigEntry<Object>>) (writer, value, entry, context) -> {
if (writer instanceof CompoundDataWriter) {
if (writer instanceof CompoundDataVisitor) {
// Comment is already written in front of the key by the CompoundDataWriter,
// so we don't have to write it here.
// We also want to unwrap the original writer,
// so that the special comment writing is limited to compounds.
writer = ((CompoundDataWriter) writer).delegate;
writer = ((CompoundDataVisitor) writer).delegate;
} else {
String comment = getEntryComment(entry);
if (comment != null) {
@@ -37,7 +37,7 @@ class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?
if (entry instanceof CompoundConfigEntry) {
innerCasted.write(
new CompoundDataWriter(writer, ((CompoundConfigEntry<?>) entry)),
new CompoundDataVisitor(writer, ((CompoundConfigEntry<?>) entry)),
value,
entry,
context
@@ -49,8 +49,8 @@ class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?
}
@RequiredArgsConstructor
private static class CompoundDataWriter implements TweedDataWriter {
private final TweedDataWriter delegate;
private static class CompoundDataVisitor implements TweedDataVisitor {
private final TweedDataVisitor delegate;
private final CompoundConfigEntry<?> compoundConfigEntry;
@Override

View File

@@ -0,0 +1,59 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import java.util.ArrayDeque;
import java.util.Deque;
public class PathTracking implements PatherData {
private final StringBuilder pathBuilder = new StringBuilder(256);
private final Deque<Context> contextStack = new ArrayDeque<>(50);
private final Deque<String> pathParts = new ArrayDeque<>(50);
private final Deque<Integer> listIndexes = new ArrayDeque<>(10);
public Context currentContext() {
return contextStack.peek();
}
public void popContext() {
if (contextStack.pop() == Context.LIST) {
listIndexes.pop();
popPathPart();
}
}
public void pushMapContext() {
contextStack.push(Context.MAP);
}
public void pushPathPart(String part) {
pathParts.push(part);
pathBuilder.append(".").append(part);
}
public void popPathPart() {
String poppedPart = pathParts.pop();
pathBuilder.setLength(pathBuilder.length() - poppedPart.length() - 1);
}
public void pushListContext() {
contextStack.push(Context.LIST);
listIndexes.push(0);
pushPathPart("0");
}
public int incrementListIndex() {
int index = listIndexes.pop() + 1;
listIndexes.push(index);
popPathPart();
pushPathPart(Integer.toString(index));
return index;
}
@Override
public String valuePath() {
return pathBuilder.toString();
}
public enum Context {
LIST, MAP,
}
}

View File

@@ -0,0 +1,70 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisitor {
private final ConfigEntryValueVisitor delegate;
private final PathTracking pathTracking;
@Override
public <T> void visitEntry(ConfigEntry<T> entry, T value) {
delegate.visitEntry(entry, value);
entryVisited();
}
@Override
public <T> boolean enterCollectionEntry(ConfigEntry<T> entry, T value) {
boolean enter = delegate.enterCollectionEntry(entry, value);
if (enter) {
pathTracking.pushListContext();
}
return enter;
}
@Override
public <T> void leaveCollectionEntry(ConfigEntry<T> entry, T value) {
delegate.leaveCollectionEntry(entry, value);
pathTracking.popContext();
entryVisited();
}
@Override
public <T> boolean enterCompoundEntry(ConfigEntry<T> entry, T value) {
boolean enter = delegate.enterCompoundEntry(entry, value);
if (enter) {
pathTracking.pushMapContext();
}
return enter;
}
@Override
public boolean enterCompoundSubEntry(String key) {
boolean enter = delegate.enterCompoundSubEntry(key);
if (enter) {
pathTracking.pushPathPart(key);
}
return enter;
}
@Override
public void leaveCompoundSubEntry(String key) {
delegate.leaveCompoundSubEntry(key);
pathTracking.popPathPart();
}
@Override
public <T> void leaveCompoundEntry(ConfigEntry<T> entry, T value) {
delegate.leaveCompoundEntry(entry, value);
pathTracking.popContext();
entryVisited();
}
private void entryVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
}
}
}

View File

@@ -0,0 +1,70 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class PathTrackingConfigEntryVisitor implements ConfigEntryVisitor {
private final ConfigEntryVisitor delegate;
private final PathTracking pathTracking;
@Override
public void visitEntry(ConfigEntry<?> entry) {
delegate.visitEntry(entry);
entryVisited();
}
@Override
public boolean enterCollectionEntry(ConfigEntry<?> entry) {
boolean enter = delegate.enterCollectionEntry(entry);
if (enter) {
pathTracking.pushListContext();
}
return enter;
}
@Override
public void leaveCollectionEntry(ConfigEntry<?> entry) {
delegate.leaveCollectionEntry(entry);
pathTracking.popContext();
entryVisited();
}
@Override
public boolean enterCompoundEntry(ConfigEntry<?> entry) {
boolean enter = delegate.enterCompoundEntry(entry);
if (enter) {
pathTracking.pushMapContext();
}
return enter;
}
@Override
public boolean enterCompoundSubEntry(String key) {
boolean enter = delegate.enterCompoundSubEntry(key);
if (enter) {
pathTracking.pushPathPart(key);
}
return enter;
}
@Override
public void leaveCompoundSubEntry(String key) {
delegate.leaveCompoundSubEntry(key);
pathTracking.popPathPart();
}
@Override
public void leaveCompoundEntry(ConfigEntry<?> entry) {
delegate.leaveCompoundEntry(entry);
pathTracking.popContext();
entryVisited();
}
private void entryVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
}
}
}

View File

@@ -0,0 +1,39 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class PathTrackingDataReader implements TweedDataReader {
private final TweedDataReader delegate;
private final PathTracking pathTracking;
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
return delegate.peekToken();
}
@Override
public TweedDataToken readToken() throws TweedDataReadException {
TweedDataToken token = delegate.readToken();
if (token.isListStart()) {
pathTracking.pushListContext();
} else if (token.isListValue()) {
pathTracking.incrementListIndex();
} else if (token.isListEnd()) {
pathTracking.popContext();
} else if (token.isMapStart()) {
pathTracking.pushMapContext();
pathTracking.pushPathPart("$");
} else if (token.isMapEntryKey()) {
pathTracking.popPathPart();
pathTracking.pushPathPart(token.readAsString());
} else if (token.isMapEnd()) {
pathTracking.popPathPart();
pathTracking.popContext();
}
return token;
}
}

View File

@@ -0,0 +1,110 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
@RequiredArgsConstructor
public class PathTrackingDataVisitor implements TweedDataVisitor {
private final TweedDataVisitor delegate;
private final PathTracking pathTracking;
@Override
public void visitNull() {
delegate.visitNull();
valueVisited();
}
@Override
public void visitBoolean(boolean value) {
delegate.visitBoolean(value);
valueVisited();
}
@Override
public void visitByte(byte value) {
delegate.visitByte(value);
valueVisited();
}
@Override
public void visitShort(short value) {
delegate.visitShort(value);
valueVisited();
}
@Override
public void visitInt(int value) {
delegate.visitInt(value);
valueVisited();
}
@Override
public void visitLong(long value) {
delegate.visitLong(value);
valueVisited();
}
@Override
public void visitFloat(float value) {
delegate.visitFloat(value);
valueVisited();
}
@Override
public void visitDouble(double value) {
delegate.visitDouble(value);
valueVisited();
}
@Override
public void visitString(@NotNull String value) {
delegate.visitString(value);
valueVisited();
}
private void valueVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
} else {
pathTracking.popPathPart();
}
}
@Override
public void visitListStart() {
delegate.visitListStart();
pathTracking.pushListContext();
}
@Override
public void visitListEnd() {
delegate.visitListEnd();
pathTracking.popContext();
valueVisited();
}
@Override
public void visitMapStart() {
delegate.visitMapStart();
pathTracking.pushMapContext();
}
@Override
public void visitMapEntryKey(String key) {
delegate.visitMapEntryKey(key);
pathTracking.pushPathPart(key);
}
@Override
public void visitMapEnd() {
delegate.visitMapEnd();
pathTracking.popContext();
valueVisited();
}
@Override
public void visitComment(String comment) {
delegate.visitComment(comment);
}
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
/**
* Extension data for {@link de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData}
* that provides the path to the value currently being read/written.
*/
public interface PatherData {
String valuePath();
}

View File

@@ -0,0 +1,99 @@
package de.siphalor.tweed5.defaultextensions.pather.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingDataReader;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingDataVisitor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PatherExtension implements TweedExtension, ReadWriteRelatedExtension {
private static final String PATHER_ID = "pather";
private RegisteredExtensionData<ReadWriteContextExtensionsData, PathTracking> rwContextPathTrackingData;
private Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware;
private Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware;
@Override
public String getId() {
return PATHER_ID;
}
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
rwContextPathTrackingData = context.registerReadWriteContextExtensionData(PathTracking.class);
entryReaderMiddleware = createEntryReaderMiddleware();
entryWriterMiddleware = createEntryWriterMiddleware();
}
private @NotNull Middleware<TweedEntryReader<?, ?>> createEntryReaderMiddleware() {
return new Middleware<TweedEntryReader<?, ?>>() {
@Override
public String id() {
return PATHER_ID;
}
@Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
//noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> castedInner = (TweedEntryReader<Object, ConfigEntry<Object>>) inner;
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
if (context.extensionsData().isPatchworkPartSet(PathTracking.class)) {
return castedInner.read(reader, entry, context);
}
PathTracking pathTracking = new PathTracking();
rwContextPathTrackingData.set(context.extensionsData(), pathTracking);
return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context);
};
}
};
}
private Middleware<TweedEntryWriter<?, ?>> createEntryWriterMiddleware() {
return new Middleware<TweedEntryWriter<?, ?>>() {
@Override
public String id() {
return PATHER_ID;
}
@Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
//noinspection unchecked
TweedEntryWriter<Object, ConfigEntry<Object>> castedInner = (TweedEntryWriter<Object, ConfigEntry<Object>>) inner;
return (TweedDataVisitor writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) -> {
if (context.extensionsData().isPatchworkPartSet(PathTracking.class)) {
castedInner.write(writer, value, entry, context);
return;
}
PathTracking pathTracking = new PathTracking();
rwContextPathTrackingData.set(context.extensionsData(), pathTracking);
castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context);
};
}
};
}
@Override
public @Nullable Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware() {
return entryReaderMiddleware;
}
@Override
public @Nullable Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware() {
return entryWriterMiddleware;
}
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import org.jetbrains.annotations.NotNull;
public interface ConfigEntryValidator {
<T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value);
@NotNull
<T> String description(ConfigEntry<T> configEntry);
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import java.util.Collection;
public interface EntrySpecificValidation {
Collection<Middleware<ConfigEntryValidator>> validators();
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
public interface ValidationExtension extends TweedExtension {
<T> ValidationIssues validate(ConfigEntry<T> entry, T value);
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface ValidationProvidingExtension {
Middleware<ConfigEntryValidator> validationMiddleware();
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.defaultextensions.validation.api.result;
import lombok.Value;
@Value
public class ValidationIssue {
String message;
ValidationIssueLevel level;
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.validation.api.result;
public enum ValidationIssueLevel {
INFO,
WARN,
ERROR
}

View File

@@ -0,0 +1,21 @@
package de.siphalor.tweed5.defaultextensions.validation.api.result;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import lombok.Value;
import java.util.Collection;
import java.util.Map;
/**
* Extension data for {@link de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData}
* that collects all validation issues.
*/
public interface ValidationIssues {
Map<String, EntryIssues> issuesByPath();
@Value
class EntryIssues {
ConfigEntry<?> entry;
Collection<ValidationIssue> issues;
}
}

View File

@@ -0,0 +1,62 @@
package de.siphalor.tweed5.defaultextensions.validation.api.result;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Function;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidationResult<T> {
private final T value;
@NotNull
private final Collection<ValidationIssue> issues;
private final boolean hasError;
public static <T> ValidationResult<T> ok(T value) {
return new ValidationResult<>(value, Collections.emptyList(), false);
}
public static <T> ValidationResult<T> withIssues(T value, @NotNull Collection<ValidationIssue> issues) {
return new ValidationResult<>(value, issues, issuesContainError(issues));
}
private static boolean issuesContainError(Collection<ValidationIssue> issues) {
if (issues.isEmpty()) {
return false;
}
for (ValidationIssue issue : issues) {
if (issue.level() == ValidationIssueLevel.ERROR) {
return true;
}
}
return false;
}
public ValidationResult<T> andThen(Function<T, ValidationResult<T>> function) {
if (hasError) {
return this;
}
ValidationResult<T> functionResult = function.apply(value);
if (functionResult.issues.isEmpty()) {
if (functionResult.value == value) {
return this;
} else if (issues.isEmpty()) {
return new ValidationResult<>(functionResult.value, Collections.emptyList(), false);
}
}
ArrayList<ValidationIssue> combinedIssues = new ArrayList<>(issues.size() + functionResult.issues.size());
combinedIssues.addAll(issues);
combinedIssues.addAll(functionResult.issues);
return new ValidationResult<>(functionResult.value, combinedIssues, functionResult.hasError);
}
}

View File

@@ -0,0 +1,91 @@
package de.siphalor.tweed5.defaultextensions.validation.api.validators;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
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 lombok.AllArgsConstructor;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
@Value
@AllArgsConstructor
public class NumberRangeValidator<N extends Number> implements ConfigEntryValidator {
@NotNull
Class<N> numberClass;
@Nullable
N minimum;
@Nullable
N maximum;
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
if (!(value instanceof Number)) {
return ValidationResult.withIssues(value, Collections.singleton(
new ValidationIssue("Value must be numeric", ValidationIssueLevel.ERROR)
));
}
if (value.getClass() != numberClass) {
return ValidationResult.withIssues(value, Collections.singleton(
new ValidationIssue(
"Value is of wrong type, expected " + numberClass.getSimpleName() +
", got " + value.getClass().getSimpleName(),
ValidationIssueLevel.ERROR
)
));
}
Number numberValue = (Number) value;
if (minimum != null && compare(numberValue, minimum) < 0) {
//noinspection unchecked
return ValidationResult.withIssues((T) minimum, Collections.singleton(
new ValidationIssue("Value must be at least " + minimum, ValidationIssueLevel.WARN)
));
}
if (maximum != null && compare(numberValue, maximum) > 0) {
//noinspection unchecked
return ValidationResult.withIssues((T) maximum, Collections.singleton(
new ValidationIssue("Value must be at most " + maximum, ValidationIssueLevel.WARN)
));
}
return ValidationResult.ok(value);
}
private int compare(@NotNull Number a, @NotNull Number b) {
if (numberClass == Byte.class) {
return Byte.compare(a.byteValue(), b.byteValue());
} else if (numberClass == Short.class) {
return Short.compare(a.shortValue(), b.shortValue());
} else if (numberClass == Integer.class) {
return Integer.compare(a.intValue(), b.intValue());
} else if (numberClass == Long.class) {
return Long.compare(a.longValue(), b.longValue());
} else if (numberClass == Float.class) {
return Float.compare(a.floatValue(), b.floatValue());
} else {
return Double.compare(a.doubleValue(), b.doubleValue());
}
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
if (minimum == null) {
if (maximum == null) {
return "";
} else {
return "Must be smaller or equal to " + maximum + ".";
}
} else {
if (maximum == null) {
return "Must be greater or equal to " + minimum + ".";
} else {
return "Must be inclusively between " + minimum + " and " + maximum + ".";
}
}
}
}

View File

@@ -0,0 +1,35 @@
package de.siphalor.tweed5.defaultextensions.validation.api.validators;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
@AllArgsConstructor
public class SimpleValidatorMiddleware implements Middleware<ConfigEntryValidator> {
String id;
ConfigEntryValidator validator;
@Override
public ConfigEntryValidator process(ConfigEntryValidator inner) {
return new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
return inner.validate(configEntry, value).andThen(v -> validator.validate(configEntry, v));
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
String description = validator.description(configEntry);
if (description.isEmpty()) {
return inner.description(configEntry);
}
return inner.description(configEntry) + "\n" + description;
}
};
}
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.defaultextensions.validation.impl;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
public interface InternalValidationEntryData {
ConfigEntryValidator completeEntryValidator();
}

View File

@@ -0,0 +1,269 @@
package de.siphalor.tweed5.defaultextensions.validation.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.data.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryValueVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherData;
import de.siphalor.tweed5.defaultextensions.pather.impl.PatherExtension;
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.ValidationProvidingExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
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 lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.var;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class ValidationExtensionImpl implements ReadWriteRelatedExtension, ValidationExtension, CommentModifyingExtension {
private static final ValidationResult<?> PRIMITIVE_IS_NULL_RESULT = ValidationResult.withIssues(
null,
Collections.singletonList(new ValidationIssue("Primitive value must not be null", ValidationIssueLevel.ERROR))
);
private static final ConfigEntryValidator PRIMITIVE_VALIDATOR = new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
if (value == null) {
//noinspection unchecked
return (ValidationResult<T>) PRIMITIVE_IS_NULL_RESULT;
}
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "Value must not be null.";
}
};
private static final ConfigEntryValidator NOOP_VALIDATOR = new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "";
}
};
private RegisteredExtensionData<EntryExtensionsData, InternalValidationEntryData> validationEntryDataExtension;
private MiddlewareContainer<ConfigEntryValidator> entryValidatorMiddlewareContainer;
private EntryValidationReaderMiddleware readerMiddleware;
private RegisteredExtensionData<ReadWriteContextExtensionsData, ValidationIssues> readContextValidationIssuesExtensionData;
@Override
public String getId() {
return "validation";
}
@Override
public void setup(TweedExtensionSetupContext context) {
context.registerExtension(new PatherExtension());
validationEntryDataExtension = context.registerEntryExtensionData(InternalValidationEntryData.class);
context.registerEntryExtensionData(EntrySpecificValidation.class);
entryValidatorMiddlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : context.configContainer().extensions()) {
if (extension instanceof ValidationProvidingExtension) {
entryValidatorMiddlewareContainer.register(((ValidationProvidingExtension) extension).validationMiddleware());
}
}
entryValidatorMiddlewareContainer.seal();
readerMiddleware = new EntryValidationReaderMiddleware();
}
@Override
public Middleware<CommentProducer> commentMiddleware() {
return new Middleware<CommentProducer>() {
@Override
public String id() {
return "validation";
}
@Override
public CommentProducer process(CommentProducer inner) {
return entry -> {
String baseComment = inner.createComment(entry);
if (entry.extensionsData().isPatchworkPartSet(InternalValidationEntryData.class)) {
String validationDescription = ((InternalValidationEntryData) entry.extensionsData())
.completeEntryValidator()
.description(entry)
.trim();
if (!validationDescription.isEmpty()) {
baseComment += "\n\n" + validationDescription;
}
}
return baseComment;
};
}
};
}
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
readContextValidationIssuesExtensionData = context.registerReadWriteContextExtensionData(ValidationIssues.class);
}
@Override
public void initEntry(ConfigEntry<?> configEntry) {
ConfigEntryValidator baseValidator;
if (configEntry.valueClass().isPrimitive()) {
baseValidator = PRIMITIVE_VALIDATOR;
} else {
baseValidator = NOOP_VALIDATOR;
}
ConfigEntryValidator entryValidator;
var entrySpecificValidators = getEntrySpecificValidators(configEntry);
if (entrySpecificValidators.isEmpty()) {
entryValidator = entryValidatorMiddlewareContainer.process(baseValidator);
} else {
DefaultMiddlewareContainer<ConfigEntryValidator> entrySpecificValidatorContainer = new DefaultMiddlewareContainer<>();
entrySpecificValidatorContainer.registerAll(entryValidatorMiddlewareContainer.middlewares());
entrySpecificValidatorContainer.registerAll(entrySpecificValidators);
entrySpecificValidatorContainer.seal();
entryValidator = entrySpecificValidatorContainer.process(baseValidator);
}
validationEntryDataExtension.set(configEntry.extensionsData(), new InternalValidationEntryDataImpl(entryValidator));
}
private Collection<Middleware<ConfigEntryValidator>> getEntrySpecificValidators(ConfigEntry<?> configEntry) {
if (!configEntry.extensionsData().isPatchworkPartSet(EntrySpecificValidation.class)) {
return Collections.emptyList();
}
return ((EntrySpecificValidation) configEntry.extensionsData()).validators();
}
@Override
public @Nullable Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware() {
return readerMiddleware;
}
@Override
public <T> ValidationIssues validate(ConfigEntry<T> entry, T value) {
PathTracking pathTracking = new PathTracking();
ValidatingConfigEntryVisitor validatingVisitor = new ValidatingConfigEntryVisitor(pathTracking);
entry.visitInOrder(new PathTrackingConfigEntryValueVisitor(validatingVisitor, pathTracking), value);
return validatingVisitor.validationIssues();
}
@Value
private static class InternalValidationEntryDataImpl implements InternalValidationEntryData {
ConfigEntryValidator completeEntryValidator;
}
private class EntryValidationReaderMiddleware implements Middleware<TweedEntryReader<?, ?>> {
@Override
public String id() {
return "validation";
}
@Override
public Set<String> mustComeAfter() {
return Collections.singleton("pather");
}
@Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
//noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> castedInner = (TweedEntryReader<Object, ConfigEntry<Object>>) inner;
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
ValidationIssues validationIssues;
if (!context.extensionsData().isPatchworkPartSet(ValidationIssues.class)) {
validationIssues = new ValidationIssuesImpl();
readContextValidationIssuesExtensionData.set(context.extensionsData(), validationIssues);
} else {
validationIssues = (ValidationIssues) context.extensionsData();
}
Object value = castedInner.read(reader, entry, context);
ValidationResult<Object> validationResult = ((InternalValidationEntryData) entry.extensionsData()).completeEntryValidator().validate(entry, value);
if (!validationResult.issues().isEmpty() && context.extensionsData().isPatchworkPartSet(PatherData.class)) {
String path = ((PatherData) context.extensionsData()).valuePath();
validationIssues.issuesByPath().put(path, new ValidationIssues.EntryIssues(
entry,
validationResult.issues()
));
}
if (validationResult.hasError()) {
throw new TweedEntryReadException("Failed to validate entry: " + validationResult.issues());
}
return validationResult.value();
};
}
}
@Getter
@RequiredArgsConstructor
private static class ValidatingConfigEntryVisitor implements ConfigEntryValueVisitor {
private final PathTracking pathTracking;
private final ValidationIssues validationIssues = new ValidationIssuesImpl();
@Override
public <T> void visitEntry(ConfigEntry<T> entry, T value) {
ValidationResult<T> result = ((InternalValidationEntryData) entry.extensionsData()).completeEntryValidator().validate(entry, value);
if (!result.issues().isEmpty()) {
validationIssues.issuesByPath().put(pathTracking.valuePath(), new ValidationIssues.EntryIssues(entry, result.issues()));
}
}
@Override
public <T> boolean enterCollectionEntry(ConfigEntry<T> entry, T value) {
return true;
}
@Override
public <T> void leaveCollectionEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
}
@Override
public <T> boolean enterCompoundEntry(ConfigEntry<T> entry, T value) {
return true;
}
@Override
public <T> void leaveCompoundEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
}
}
@Value
private static class ValidationIssuesImpl implements ValidationIssues {
Map<String, EntryIssues> issuesByPath = new HashMap<>();
}
}

View File

@@ -0,0 +1,145 @@
package de.siphalor.tweed5.defaultextensions.validation.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.api.middleware.Middleware;
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.defaultextensions.comment.api.AComment;
import de.siphalor.tweed5.defaultextensions.comment.impl.CommentExtension;
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.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.NumberRangeValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.SimpleValidatorMiddleware;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.lang.annotation.Annotation;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class ValidationExtensionImplTest {
private DefaultConfigContainer<Map<String, Object>> configContainer;
private CommentExtension commentExtension;
private ValidationExtension validationExtension;
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry;
private SimpleConfigEntryImpl<Byte> byteEntry;
private SimpleConfigEntryImpl<Integer> intEntry;
private SimpleConfigEntryImpl<Double> doubleEntry;
@BeforeEach
void setUp() {
configContainer = new DefaultConfigContainer<>();
commentExtension = new CommentExtension();
configContainer.registerExtension(commentExtension);
validationExtension = new ValidationExtensionImpl();
configContainer.registerExtension(validationExtension);
configContainer.finishExtensionSetup();
//noinspection unchecked
rootEntry = new StaticMapCompoundConfigEntryImpl<>(((Class<Map<String, Object>>) (Class<?>) Map.class), LinkedHashMap::new);
byteEntry = new SimpleConfigEntryImpl<>(Byte.class);
rootEntry.addSubEntry("byte", byteEntry);
intEntry = new SimpleConfigEntryImpl<>(Integer.class);
rootEntry.addSubEntry("int", intEntry);
doubleEntry = new SimpleConfigEntryImpl<>(Double.class);
rootEntry.addSubEntry("double", doubleEntry);
configContainer.attachAndSealTree(rootEntry);
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, AComment> commentData = (RegisteredExtensionData<EntryExtensionsData, AComment>) configContainer.entryDataExtensions().get(AComment.class);
commentData.set(intEntry.extensionsData(), new AComment() {
@Override
public Class<? extends Annotation> annotationType() {
return null;
}
@Override
public String value() {
return "This is the main comment!";
}
});
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation> entrySpecificValidation = (RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation>) configContainer.entryDataExtensions().get(EntrySpecificValidation.class);
entrySpecificValidation.set(byteEntry.extensionsData(), new EntrySpecificValidation() {
@Override
public Collection<Middleware<ConfigEntryValidator>> validators() {
return Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Byte.class, (byte) 10, (byte) 100)));
}
});
entrySpecificValidation.set(intEntry.extensionsData(), new EntrySpecificValidation() {
@Override
public Collection<Middleware<ConfigEntryValidator>> validators() {
return Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Integer.class, null, 123)));
}
});
entrySpecificValidation.set(doubleEntry.extensionsData(), new EntrySpecificValidation() {
@Override
public Collection<Middleware<ConfigEntryValidator>> validators() {
return Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Double.class, 0.5, null)));
}
});
configContainer.initialize();
}
@ParameterizedTest
@CsvSource({
"12, 34, 56.78"
})
void valid(Byte b, Integer i, Double d) {
HashMap<String, Object> value = new HashMap<>();
value.put("byte", b);
value.put("int", i);
value.put("double", d);
ValidationIssues result = validationExtension.validate(rootEntry, value);
assertNotNull(result);
assertNotNull(result.issuesByPath());
assertTrue(result.issuesByPath().isEmpty(), () -> "Should have no issues, but got " + result.issuesByPath());
}
@Test
void invalid() {
HashMap<String, Object> value = new HashMap<>();
value.put("byte", (byte) 9);
value.put("int", 124);
value.put("double", 0.2);
ValidationIssues result = validationExtension.validate(rootEntry, value);
assertNotNull(result);
assertNotNull(result.issuesByPath());
assertAll(
() -> assertValidationIssue(result, ".byte", byteEntry, new ValidationIssue("Value must be at least 10", ValidationIssueLevel.WARN)),
() -> assertValidationIssue(result, ".int", intEntry, new ValidationIssue("Value must be at most 123", ValidationIssueLevel.WARN)),
() -> assertValidationIssue(result, ".double", doubleEntry, new ValidationIssue("Value must be at least 0.5", ValidationIssueLevel.WARN))
);
}
private static void assertValidationIssue(
ValidationIssues issues,
String expectedPath,
ConfigEntry<?> expectedEntry,
ValidationIssue expectedIssue
) {
assertTrue(issues.issuesByPath().containsKey(expectedPath), "Must have issues for path " + expectedPath);
ValidationIssues.EntryIssues entryIssues = issues.issuesByPath().get(expectedPath);
assertSame(expectedEntry, entryIssues.entry(), "Entry must match");
assertEquals(1, entryIssues.issues().size(), "Entry must have exactly one issue");
assertEquals(expectedIssue, entryIssues.issues().iterator().next(), "Issue must match");
}
}