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

@@ -2,11 +2,9 @@ package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
public interface ConfigEntry<T> { public interface ConfigEntry<T> {
Class<T> valueClass(); Class<T> valueClass();
void validate(T value) throws ConfigEntryValueValidationException;
void seal(ConfigContainer<?> container); void seal(ConfigContainer<?> container);
boolean sealed(); boolean sealed();
@@ -14,4 +12,5 @@ public interface ConfigEntry<T> {
EntryExtensionsData extensionsData(); EntryExtensionsData extensionsData();
void visitInOrder(ConfigEntryVisitor visitor); void visitInOrder(ConfigEntryVisitor visitor);
void visitInOrder(ConfigEntryValueVisitor visitor, T value);
} }

View File

@@ -0,0 +1,28 @@
package de.siphalor.tweed5.core.api.entry;
public interface ConfigEntryValueVisitor {
<T> void visitEntry(ConfigEntry<T> entry, T value);
default <T> boolean enterCollectionEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
return true;
}
default <T> void leaveCollectionEntry(ConfigEntry<T> entry, T value) {
}
default <T> boolean enterCompoundEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
return true;
}
default boolean enterCompoundSubEntry(String key) {
return true;
}
default void leaveCompoundSubEntry(String key) {
}
default <T> void leaveCompoundEntry(ConfigEntry<T> entry, T value) {
}
}

View File

@@ -5,4 +5,5 @@ import de.siphalor.tweed5.core.api.container.ConfigContainer;
public interface TweedExtensionSetupContext { public interface TweedExtensionSetupContext {
ConfigContainer<?> configContainer(); ConfigContainer<?> configContainer();
<E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass); <E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass);
void registerExtension(TweedExtension extension);
} }

View File

@@ -1,6 +1,7 @@
package de.siphalor.tweed5.core.api.middleware; package de.siphalor.tweed5.core.api.middleware;
import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter; import de.siphalor.tweed5.core.api.sort.AcyclicGraphSorter;
import lombok.Getter;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
@@ -10,6 +11,7 @@ import java.util.stream.Stream;
public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> { public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> {
private static final String CONTAINER_ID = ""; private static final String CONTAINER_ID = "";
@Getter
private List<Middleware<M>> middlewares = new ArrayList<>(); private List<Middleware<M>> middlewares = new ArrayList<>();
private final Set<String> middlewareIds = new HashSet<>(); private final Set<String> middlewareIds = new HashSet<>();
private boolean sealed = false; private boolean sealed = false;
@@ -20,19 +22,37 @@ public class DefaultMiddlewareContainer<M> implements MiddlewareContainer<M> {
} }
@Override @Override
public void register(Middleware<M> middleware) { public void registerAll(Collection<Middleware<M>> middlewares) {
if (sealed) { requireUnsealed();
throw new IllegalStateException("Middleware container has already been sealed");
for (Middleware<M> middleware : middlewares) {
if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty");
}
if (!this.middlewareIds.add(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
}
} }
this.middlewares.addAll(middlewares);
}
@Override
public void register(Middleware<M> middleware) {
requireUnsealed();
if (middleware.id().isEmpty()) { if (middleware.id().isEmpty()) {
throw new IllegalArgumentException("Middleware id cannot be empty"); throw new IllegalArgumentException("Middleware id cannot be empty");
} }
if (middlewareIds.contains(middleware.id())) { if (!middlewareIds.add(middleware.id())) {
throw new IllegalArgumentException("Middleware id already registered: " + middleware.id()); throw new IllegalArgumentException("Middleware id already registered: " + middleware.id());
} }
middlewares.add(middleware); middlewares.add(middleware);
middlewareIds.add(middleware.id()); }
private void requireUnsealed() {
if (sealed) {
throw new IllegalStateException("Middleware container has already been sealed");
}
} }
@Override @Override

View File

@@ -1,6 +1,12 @@
package de.siphalor.tweed5.core.api.middleware; package de.siphalor.tweed5.core.api.middleware;
import java.util.Collection;
public interface MiddlewareContainer<M> extends Middleware<M> { public interface MiddlewareContainer<M> extends Middleware<M> {
default void registerAll(Collection<Middleware<M>> middlewares) {
middlewares.forEach(this::register);
}
void register(Middleware<M> middleware); void register(Middleware<M> middleware);
void seal(); void seal();
Collection<Middleware<M>> middlewares();
} }

View File

@@ -1,8 +0,0 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.middleware.Middleware;
public interface ConfigEntryValidationExtension {
Middleware<ConfigEntryValidationMiddleware> validationMiddleware(ConfigEntry<?> configEntry);
}

View File

@@ -1,8 +0,0 @@
package de.siphalor.tweed5.core.api.validation;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
@FunctionalInterface
public interface ConfigEntryValidationMiddleware {
<T> void validate(ConfigEntry<T> configEntry, T value) throws ConfigEntryValueValidationException;
}

View File

@@ -1,18 +0,0 @@
package de.siphalor.tweed5.core.api.validation;
public class ConfigEntryValueValidationException extends Exception {
public ConfigEntryValueValidationException() {
}
public ConfigEntryValueValidationException(String message) {
super(message);
}
public ConfigEntryValueValidationException(String message, Throwable cause) {
super(message, cause);
}
public ConfigEntryValueValidationException(Throwable cause) {
super(cause);
}
}

View File

@@ -13,6 +13,7 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -20,7 +21,7 @@ import java.util.Map;
public class DefaultConfigContainer<T> implements ConfigContainer<T> { public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Getter @Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP; private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final HashMap<Class<? extends TweedExtension>, TweedExtension> extensions = new HashMap<>(); private final Map<Class<? extends TweedExtension>, TweedExtension> extensions = new HashMap<>();
private ConfigEntry<T> rootEntry; private ConfigEntry<T> rootEntry;
private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass; private PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass;
private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions; private Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions;
@@ -33,7 +34,11 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
@Override @Override
public void registerExtension(TweedExtension extension) { public void registerExtension(TweedExtension extension) {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP); requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
extensions.put(extension.getClass(), extension);
TweedExtension previous = extensions.put(extension.getClass(), extension);
if (previous != null) {
throw new IllegalArgumentException("Extension " + extension.getClass().getName() + " is already registered");
}
} }
@Override @Override
@@ -41,6 +46,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP); requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
registeredEntryDataExtensions = new HashMap<>(); registeredEntryDataExtensions = new HashMap<>();
Collection<TweedExtension> additionalExtensions = new ArrayList<>();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() { TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override @Override
public ConfigContainer<T> configContainer() { public ConfigContainer<T> configContainer() {
@@ -56,10 +62,26 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
registeredEntryDataExtensions.put(dataClass, registered); registeredEntryDataExtensions.put(dataClass, registered);
return registered; return registered;
} }
@Override
public void registerExtension(TweedExtension extension) {
if (!extensions.containsKey(extension.getClass())) {
additionalExtensions.add(extension);
}
}
}; };
for (TweedExtension extension : extensions.values()) { Collection<TweedExtension> extensionsToSetup = extensions.values();
extension.setup(extensionSetupContext); while (!extensionsToSetup.isEmpty()) {
for (TweedExtension extension : extensionsToSetup) {
extension.setup(extensionSetupContext);
}
for (TweedExtension additionalExtension : additionalExtensions) {
extensions.put(additionalExtension.getClass(), additionalExtension);
}
extensionsToSetup = new ArrayList<>(additionalExtensions);
additionalExtensions.clear();
} }
PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder() PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder()

View File

@@ -1,15 +1,8 @@
package de.siphalor.tweed5.core.impl.entry; package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationExtension;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValidationMiddleware;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -17,17 +10,12 @@ import org.jetbrains.annotations.NotNull;
@RequiredArgsConstructor @RequiredArgsConstructor
@Getter @Getter
abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> { abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> {
private static final ConfigEntryValidationMiddleware ROOT_VALIDATION = new ConfigEntryValidationMiddleware() {
@Override
public <U> void validate(ConfigEntry<U> configEntry, U value) {}
};
@NotNull @NotNull
private final Class<T> valueClass; private final Class<T> valueClass;
private ConfigContainer<?> container; private ConfigContainer<?> container;
private EntryExtensionsData extensionsData; private EntryExtensionsData extensionsData;
private boolean sealed; private boolean sealed;
private ConfigEntryValidationMiddleware validationMiddleware;
@Override @Override
public void seal(ConfigContainer<?> container) { public void seal(ConfigContainer<?> container) {
@@ -35,18 +23,6 @@ abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> {
this.container = container; this.container = container;
this.extensionsData = container.createExtensionsData(); this.extensionsData = container.createExtensionsData();
MiddlewareContainer<ConfigEntryValidationMiddleware> validationMiddlewareContainer = new DefaultMiddlewareContainer<>();
for (TweedExtension extension : container().extensions()) {
if (extension instanceof ConfigEntryValidationExtension) {
validationMiddlewareContainer.register(((ConfigEntryValidationExtension) extension).validationMiddleware(this));
}
}
validationMiddlewareContainer.seal();
validationMiddleware = validationMiddlewareContainer.process(ROOT_VALIDATION);
sealed = true; sealed = true;
} }
@@ -55,22 +31,4 @@ abstract class BaseConfigEntryImpl<T> implements ConfigEntry<T> {
throw new IllegalStateException("Config entry is already sealed!"); throw new IllegalStateException("Config entry is already sealed!");
} }
} }
@Override
public void validate(T value) throws ConfigEntryValueValidationException {
if (value == null) {
if (valueClass.isPrimitive()) {
throw new ConfigEntryValueValidationException("Value must not be null");
}
} else if (!valueClass.isAssignableFrom(value.getClass())) {
throw new ConfigEntryValueValidationException("Value must be of type " + valueClass.getName());
}
validationMiddleware.validate(this, value);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
visitor.visitEntry(this);
}
} }

View File

@@ -2,6 +2,7 @@ package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry; import de.siphalor.tweed5.core.api.entry.CoherentCollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import java.util.Collection; import java.util.Collection;
@@ -39,4 +40,16 @@ public class CoherentCollectionConfigEntryImpl<E, T extends Collection<E>> exten
visitor.leaveCollectionEntry(this); visitor.leaveCollectionEntry(this);
} }
} }
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterCollectionEntry(this, value)) {
if (value != null) {
for (E element : value) {
visitor.visitEntry(elementEntry, element);
}
}
visitor.leaveCollectionEntry(this, value);
}
}
} }

View File

@@ -2,8 +2,8 @@ package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.validation.ConfigEntryValueValidationException;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
@@ -52,11 +52,8 @@ public class ReflectiveCompoundConfigEntryImpl<T> extends BaseConfigEntryImpl<T>
} }
try { try {
compoundEntry.configEntry().validate(value);
compoundEntry.field().set(compoundValue, value); compoundEntry.field().set(compoundValue, value);
} catch (ConfigEntryValueValidationException e) {
throw new IllegalArgumentException("Invalid value for config entry: " + key, e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
@@ -99,6 +96,23 @@ public class ReflectiveCompoundConfigEntryImpl<T> extends BaseConfigEntryImpl<T>
} }
} }
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterCompoundEntry(this, value)) {
compoundEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
try {
visitor.visitEntry(entry.configEntry(), entry.field().get(value));
} catch (IllegalAccessException ignored) {
// ignored
}
visitor.leaveCompoundSubEntry(key);
}
});
visitor.leaveCompoundEntry(this, value);
}
}
@Value @Value
public static class CompoundEntry { public static class CompoundEntry {
String name; String name;

View File

@@ -1,9 +1,21 @@
package de.siphalor.tweed5.core.impl.entry; package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
public class SimpleConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements SimpleConfigEntry<T> { public class SimpleConfigEntryImpl<T> extends BaseConfigEntryImpl<T> implements SimpleConfigEntry<T> {
public SimpleConfigEntryImpl(Class<T> valueClass) { public SimpleConfigEntryImpl(Class<T> valueClass) {
super(valueClass); super(valueClass);
} }
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
visitor.visitEntry(this);
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
visitor.visitEntry(this, value);
}
} }

View File

@@ -2,6 +2,7 @@ package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry; import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor; import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -64,4 +65,20 @@ public class StaticMapCompoundConfigEntryImpl<T extends Map<String, Object>> ext
visitor.leaveCompoundEntry(this); visitor.leaveCompoundEntry(this);
} }
} }
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterCompoundEntry(this, value)) {
if (value != null) {
compoundEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) {
//noinspection unchecked
((ConfigEntry<Object>) entry).visitInOrder(visitor, value.get(key));
visitor.leaveCompoundSubEntry(key);
}
});
}
visitor.leaveCompoundEntry(this, value);
}
}
} }

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

View File

@@ -1,5 +0,0 @@
package de.siphalor.tweed5.dataapi.api;
public interface TweedDataWriter extends TweedDataVisitor {
}

View File

@@ -4,12 +4,12 @@ import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData;
import de.siphalor.tweed5.dataapi.api.TweedDataReader; import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
public interface ReadWriteExtension extends TweedExtension { public interface ReadWriteExtension extends TweedExtension {
ReadWriteContextExtensionsData createReadWriteContextExtensionsData(); ReadWriteContextExtensionsData createReadWriteContextExtensionsData();
<T> T read(TweedDataReader reader, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException; <T> T read(TweedDataReader reader, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryReadException;
<T> void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException; <T> void write(TweedDataVisitor writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException;
} }

View File

@@ -2,9 +2,9 @@ package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException; import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
@FunctionalInterface @FunctionalInterface
public interface TweedEntryWriter<T, C extends ConfigEntry<T>> { public interface TweedEntryWriter<T, C extends ConfigEntry<T>> {
void write(TweedDataWriter writer, T value, C entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException; void write(TweedDataVisitor writer, T value, C entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException;
} }

View File

@@ -13,7 +13,7 @@ import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension
import de.siphalor.tweed5.dataapi.api.TweedDataReadException; import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader; import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException; import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.patchwork.api.Patchwork; import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator; import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
@@ -132,7 +132,7 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
} }
@Override @Override
public <T> void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException { public <T> void write(TweedDataVisitor writer, T value, ConfigEntry<T> entry, ReadWriteContextExtensionsData contextExtensionsData) throws TweedEntryWriteException {
try { try {
getWriterChain(entry).write(writer, value, entry, new TweedReadWriteContextImpl(contextExtensionsData)); getWriterChain(entry).write(writer, value, entry, new TweedReadWriteContextImpl(contextExtensionsData));
} catch (TweedDataWriteException e) { } catch (TweedDataWriteException e) {

View File

@@ -34,7 +34,7 @@ public class TweedEntryReaderWriterImpls {
@RequiredArgsConstructor @RequiredArgsConstructor
private static class PrimitiveReaderWriter<T> implements TweedEntryReaderWriter<T, ConfigEntry<T>> { private static class PrimitiveReaderWriter<T> implements TweedEntryReaderWriter<T, ConfigEntry<T>> {
private final Function<TweedDataToken, T> readerCall; private final Function<TweedDataToken, T> readerCall;
private final BiConsumer<TweedDataWriter, T> writerCall; private final BiConsumer<TweedDataVisitor, T> writerCall;
@Override @Override
public T read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) throws TweedDataReadException { public T read(TweedDataReader reader, ConfigEntry<T> entry, TweedReadContext context) throws TweedDataReadException {
@@ -42,7 +42,7 @@ public class TweedEntryReaderWriterImpls {
} }
@Override @Override
public void write(TweedDataWriter writer, T value, ConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException { public void write(TweedDataVisitor writer, T value, ConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value); requireNonNullWriteValue(value);
writerCall.accept(writer, value); writerCall.accept(writer, value);
} }
@@ -79,7 +79,7 @@ public class TweedEntryReaderWriterImpls {
} }
@Override @Override
public void write(TweedDataWriter writer, C value, CoherentCollectionConfigEntry<T, C> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException { public void write(TweedDataVisitor writer, C value, CoherentCollectionConfigEntry<T, C> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value); requireNonNullWriteValue(value);
if (value.isEmpty()) { if (value.isEmpty()) {
@@ -126,7 +126,7 @@ public class TweedEntryReaderWriterImpls {
} }
@Override @Override
public void write(TweedDataWriter writer, T value, CompoundConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException { public void write(TweedDataVisitor writer, T value, CompoundConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value); requireNonNullWriteValue(value);
writer.visitMapStart(); writer.visitMapStart();
@@ -178,7 +178,7 @@ public class TweedEntryReaderWriterImpls {
} }
@Override @Override
public void write(TweedDataWriter writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) throws TweedDataWriteException { public void write(TweedDataVisitor writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) throws TweedDataWriteException {
writer.visitNull(); writer.visitNull();
} }

View File

@@ -16,7 +16,7 @@ import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.hjson.HjsonLexer; import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader; import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter; import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -96,7 +96,7 @@ class ReadWriteExtensionImplTest {
assertEquals(Arrays.asList(true, false, true), result.get("list")); assertEquals(Arrays.asList(true, false, true), result.get("list"));
} }
private TweedDataWriter setupWriter(Function<Writer, TweedDataWriter> writerFactory) { private TweedDataVisitor setupWriter(Function<Writer, TweedDataVisitor> writerFactory) {
stringWriter = new StringWriter(); stringWriter = new StringWriter();
return writerFactory.apply(stringWriter); return writerFactory.apply(stringWriter);
} }

View File

@@ -1,7 +1,7 @@
package de.siphalor.tweed5.data.hjson; package de.siphalor.tweed5.data.hjson;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException; import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import lombok.Data; import lombok.Data;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -11,7 +11,7 @@ import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class HjsonWriter implements TweedDataWriter { public class HjsonWriter implements TweedDataVisitor {
private static final int PREFILL_INDENT = 10; private static final int PREFILL_INDENT = 10;
private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n"); private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n");