Refactored validation and stuff
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.siphalor.tweed5.defaultextensions.validation.api.result;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class ValidationIssue {
|
||||
String message;
|
||||
ValidationIssueLevel level;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.defaultextensions.validation.api.result;
|
||||
|
||||
public enum ValidationIssueLevel {
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + ".";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.siphalor.tweed5.defaultextensions.validation.impl;
|
||||
|
||||
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
|
||||
|
||||
public interface InternalValidationEntryData {
|
||||
ConfigEntryValidator completeEntryValidator();
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user