[patchwork, core, extensions] Hugely simplify Patchworks

This commit is contained in:
2025-06-13 22:04:16 +02:00
parent 694f993b8c
commit 6e5c9a23c2
72 changed files with 987 additions and 1977 deletions

View File

@@ -1,6 +1,5 @@
[versions] [versions]
assertj = "3.26.3" assertj = "3.26.3"
asm = "9.7"
autoservice = "1.1.1" autoservice = "1.1.1"
java-main = "8" java-main = "8"
java-test = "21" java-test = "21"
@@ -16,8 +15,6 @@ lombok = { id = "io.freefair.lombok", version = "8.13.1" }
[libraries] [libraries]
assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" }
asm-commons = { group = "org.ow2.asm", name = "asm-commons", version.ref = "asm" }
asm-core = { group = "org.ow2.asm", name = "asm", version.ref = "asm" }
autoservice-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "autoservice" } autoservice-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "autoservice" }
autoservice-processor = { group = "com.google.auto.service", name = "auto-service", version.ref = "autoservice" } autoservice-processor = { group = "com.google.auto.service", name = "auto-service", version.ref = "autoservice" }
jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" } jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" }

View File

@@ -1,13 +1,12 @@
package de.siphalor.tweed5.core.api.container; package de.siphalor.tweed5.core.api.container;
import de.siphalor.tweed5.construct.api.TweedConstructFactory; import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.extension.TweedExtension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -16,7 +15,7 @@ import java.util.Optional;
* @param <T> The class that the config tree represents * @param <T> The class that the config tree represents
* @see ConfigContainerSetupPhase * @see ConfigContainerSetupPhase
*/ */
public interface ConfigContainer<T> { public interface ConfigContainer<T extends @Nullable Object> {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
TweedConstructFactory<ConfigContainer> FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build(); TweedConstructFactory<ConfigContainer> FACTORY = TweedConstructFactory.builder(ConfigContainer.class).build();
@@ -36,10 +35,9 @@ public interface ConfigContainer<T> {
void attachTree(ConfigEntry<T> rootEntry); void attachTree(ConfigEntry<T> rootEntry);
EntryExtensionsData createExtensionsData(); Patchwork createExtensionsData();
void initialize(); void initialize();
ConfigEntry<T> rootEntry(); ConfigEntry<T> rootEntry();
Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions();
} }

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.core.api.container;
import org.jspecify.annotations.NullMarked;

View File

@@ -1,14 +1,14 @@
package de.siphalor.tweed5.core.api.entry; package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public abstract class BaseConfigEntry<T> implements ConfigEntry<T> { public abstract class BaseConfigEntry<T> implements ConfigEntry<T> {
private final ConfigContainer<?> container; private final ConfigContainer<?> container;
private final Class<T> valueClass; private final Class<T> valueClass;
private final EntryExtensionsData extensionsData; private final Patchwork extensionsData;
public BaseConfigEntry(ConfigContainer<?> container, Class<T> valueClass) { public BaseConfigEntry(ConfigContainer<?> container, Class<T> valueClass) {
this.container = container; this.container = container;

View File

@@ -1,8 +1,15 @@
package de.siphalor.tweed5.core.api.entry; package de.siphalor.tweed5.core.api.entry;
import java.util.Collection; import java.util.Collection;
import java.util.function.Consumer;
public interface CollectionConfigEntry<E, T extends Collection<E>> extends ConfigEntry<T> { public interface CollectionConfigEntry<E, T extends Collection<E>> extends ConfigEntry<T> {
@Override
default CollectionConfigEntry<E, T> apply(Consumer<ConfigEntry<T>> function) {
ConfigEntry.super.apply(function);
return this;
}
ConfigEntry<E> elementEntry(); ConfigEntry<E> elementEntry();
T instantiateCollection(int size); T instantiateCollection(int size);

View File

@@ -1,8 +1,15 @@
package de.siphalor.tweed5.core.api.entry; package de.siphalor.tweed5.core.api.entry;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
public interface CompoundConfigEntry<T> extends ConfigEntry<T> { public interface CompoundConfigEntry<T> extends ConfigEntry<T> {
@Override
default CompoundConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
ConfigEntry.super.apply(function);
return this;
}
Map<String, ConfigEntry<?>> subEntries(); Map<String, ConfigEntry<?>> subEntries();
<V> void set(T compoundValue, String key, V value); <V> void set(T compoundValue, String key, V value);

View File

@@ -1,18 +1,32 @@
package de.siphalor.tweed5.core.api.entry; package de.siphalor.tweed5.core.api.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.patchwork.api.Patchwork;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public interface ConfigEntry<T> { import java.util.function.Consumer;
import java.util.function.Function;
public interface ConfigEntry<T extends @Nullable Object> {
ConfigContainer<?> container(); ConfigContainer<?> container();
Class<T> valueClass(); Class<T> valueClass();
EntryExtensionsData extensionsData(); Patchwork extensionsData();
default ConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
function.accept(this);
return this;
}
default <R> R call(Function<ConfigEntry<T>, R> function) {
return function.apply(this);
}
void visitInOrder(ConfigEntryVisitor visitor); void visitInOrder(ConfigEntryVisitor visitor);
void visitInOrder(ConfigEntryValueVisitor visitor, T value); void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value);
T deepCopy(T value); T deepCopy(@NonNull T value);
} }

View File

@@ -1,4 +1,11 @@
package de.siphalor.tweed5.core.api.entry; package de.siphalor.tweed5.core.api.entry;
import java.util.function.Consumer;
public interface SimpleConfigEntry<T> extends ConfigEntry<T> { public interface SimpleConfigEntry<T> extends ConfigEntry<T> {
@Override
default SimpleConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
ConfigEntry.super.apply(function);
return this;
}
} }

View File

@@ -1,6 +0,0 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface EntryExtensionsData extends Patchwork<EntryExtensionsData> {
}

View File

@@ -1,7 +0,0 @@
package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface RegisteredExtensionData<U extends Patchwork<U>, E> {
void set(U patchwork, E extension);
}

View File

@@ -1,6 +1,8 @@
package de.siphalor.tweed5.core.api.extension; package de.siphalor.tweed5.core.api.extension;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
public interface TweedExtensionSetupContext { public interface TweedExtensionSetupContext {
<E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass); <E> PatchworkPartAccess<E> registerEntryExtensionData(Class<E> dataClass);
void registerExtension(Class<? extends TweedExtension> extensionClass); void registerExtension(Class<? extends TweedExtension> extensionClass);
} }

View File

@@ -3,21 +3,15 @@ package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase; import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
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.PatchworkFactory;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import de.siphalor.tweed5.utils.api.collection.InheritanceMap; import de.siphalor.tweed5.utils.api.collection.InheritanceMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.*; import java.util.*;
@@ -28,9 +22,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
private final Set<Class<? extends TweedExtension>> requestedExtensions = new HashSet<>(); private final Set<Class<? extends TweedExtension>> requestedExtensions = new HashSet<>();
private final InheritanceMap<TweedExtension> extensions = new InheritanceMap<>(TweedExtension.class); private final InheritanceMap<TweedExtension> extensions = new InheritanceMap<>(TweedExtension.class);
private @Nullable ConfigEntry<T> rootEntry; private @Nullable ConfigEntry<T> rootEntry;
private @Nullable PatchworkClass<EntryExtensionsData> entryExtensionsDataPatchworkClass; private @Nullable PatchworkFactory entryExtensionsDataPatchworkFactory;
private final Map<Class<?>, RegisteredExtensionDataImpl<EntryExtensionsData, ?>> registeredEntryDataExtensions
= new HashMap<>();
@Override @Override
public void registerExtension(Class<? extends TweedExtension> extensionClass) { public void registerExtension(Class<? extends TweedExtension> extensionClass) {
@@ -42,15 +34,12 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
public void finishExtensionSetup() { public void finishExtensionSetup() {
requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP); requireSetupPhase(ConfigContainerSetupPhase.EXTENSIONS_SETUP);
PatchworkFactory.Builder entryExtensionDataPatchworkFactoryBuilder = PatchworkFactory.builder();
TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() { TweedExtensionSetupContext extensionSetupContext = new TweedExtensionSetupContext() {
@Override @Override
public <E> RegisteredExtensionData<EntryExtensionsData, E> registerEntryExtensionData(Class<E> dataClass) { public <E> PatchworkPartAccess<E> registerEntryExtensionData(Class<E> dataClass) {
if (registeredEntryDataExtensions.containsKey(dataClass)) { return entryExtensionDataPatchworkFactoryBuilder.registerPart(dataClass);
throw new IllegalArgumentException("Extension " + dataClass.getName() + " is already registered");
}
RegisteredExtensionDataImpl<EntryExtensionsData, E> registered = new RegisteredExtensionDataImpl<>();
registeredEntryDataExtensions.put(dataClass, registered);
return registered;
} }
@Override @Override
@@ -81,20 +70,7 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
} }
} }
PatchworkClassCreator<EntryExtensionsData> entryExtensionsDataGenerator = PatchworkClassCreator.<EntryExtensionsData>builder() entryExtensionsDataPatchworkFactory = entryExtensionDataPatchworkFactoryBuilder.build();
.patchworkInterface(EntryExtensionsData.class)
.classPackage("de.siphalor.tweed5.core.generated.entryextensiondata")
.classPrefix("EntryExtensionsData$")
.build();
try {
entryExtensionsDataPatchworkClass = entryExtensionsDataGenerator.createClass(registeredEntryDataExtensions.keySet());
for (PatchworkClassPart part : entryExtensionsDataPatchworkClass.parts()) {
RegisteredExtensionDataImpl<EntryExtensionsData, ?> registeredExtension = registeredEntryDataExtensions.get(part.partInterface());
registeredExtension.setter(part.fieldSetter());
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException("Failed to create patchwork class for entry extensions' data", e);
}
setupPhase = ConfigContainerSetupPhase.TREE_SETUP; setupPhase = ConfigContainerSetupPhase.TREE_SETUP;
@@ -230,25 +206,11 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
} }
@Override @Override
public EntryExtensionsData createExtensionsData() { public Patchwork createExtensionsData() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP); requireSetupPhase(ConfigContainerSetupPhase.TREE_SETUP);
try { assert entryExtensionsDataPatchworkFactory != null;
assert entryExtensionsDataPatchworkClass != null; return entryExtensionsDataPatchworkFactory.create();
return (EntryExtensionsData) entryExtensionsDataPatchworkClass.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to construct patchwork class for entry extensions' data", e);
}
}
@Override
public Map<Class<?>, ? extends RegisteredExtensionData<EntryExtensionsData, ?>> entryDataExtensions() {
requireSetupPhase(
ConfigContainerSetupPhase.TREE_SETUP,
ConfigContainerSetupPhase.TREE_ATTACHED,
ConfigContainerSetupPhase.INITIALIZED
);
return registeredEntryDataExtensions;
} }
@Override @Override
@@ -295,18 +257,4 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
); );
} }
} }
@Setter
private static class RegisteredExtensionDataImpl<U extends Patchwork<U>, E> implements RegisteredExtensionData<U, E> {
private MethodHandle setter;
@Override
public void set(U patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
} }

View File

@@ -5,6 +5,7 @@ import de.siphalor.tweed5.core.api.entry.BaseConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; 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.entry.SimpleConfigEntry; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import org.jspecify.annotations.NonNull;
public class SimpleConfigEntryImpl<T> extends BaseConfigEntry<T> implements SimpleConfigEntry<T> { public class SimpleConfigEntryImpl<T> extends BaseConfigEntry<T> implements SimpleConfigEntry<T> {
public SimpleConfigEntryImpl(ConfigContainer<?> container, Class<T> valueClass) { public SimpleConfigEntryImpl(ConfigContainer<?> container, Class<T> valueClass) {
@@ -22,7 +23,7 @@ public class SimpleConfigEntryImpl<T> extends BaseConfigEntry<T> implements Simp
} }
@Override @Override
public T deepCopy(T value) { public T deepCopy(@NonNull T value) {
return value; return value;
} }
} }

View File

@@ -2,8 +2,6 @@ package de.siphalor.tweed5.core.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase; import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
@@ -131,28 +129,7 @@ class DefaultConfigContainerTest {
configContainer.registerExtension(ExtensionB.class); configContainer.registerExtension(ExtensionB.class);
configContainer.finishExtensionSetup(); configContainer.finishExtensionSetup();
var extensionData = configContainer.createExtensionsData(); var extensionData = configContainer.createExtensionsData();
assertThat(extensionData).isNotNull() assertThat(extensionData).isNotNull();
.satisfies(
data -> assertThat(data.isPatchworkPartDefined(ExtensionBData.class)).isTrue(),
data -> assertThat(data.isPatchworkPartDefined(String.class)).isFalse()
);
}
@Test
void entryDataExtensions() {
var configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(ExtensionB.class);
configContainer.finishExtensionSetup();
assertThat(configContainer.entryDataExtensions()).containsOnlyKeys(ExtensionBData.class);
//noinspection unchecked
var registeredExtension = (RegisteredExtensionData<EntryExtensionsData, ExtensionBData>)
configContainer.entryDataExtensions().get(ExtensionBData.class);
var extensionsData = configContainer.createExtensionsData();
registeredExtension.set(extensionsData, new ExtensionBDataImpl("blub"));
assertThat(((ExtensionBData) extensionsData).test()).isEqualTo("blub");
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@@ -5,8 +5,20 @@ import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.defaultextensions.comment.impl.CommentExtensionImpl; import de.siphalor.tweed5.defaultextensions.comment.impl.CommentExtensionImpl;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
public interface CommentExtension extends TweedExtension { public interface CommentExtension extends TweedExtension {
Class<? extends CommentExtension> DEFAULT = CommentExtensionImpl.class; Class<? extends CommentExtension> DEFAULT = CommentExtensionImpl.class;
static <C extends ConfigEntry<?>> Consumer<C> baseComment(String baseComment) {
return entry -> {
CommentExtension extension = entry.container().extension(CommentExtension.class)
.orElseThrow(() -> new IllegalStateException("No comment extension registered"));
extension.setBaseComment(entry, baseComment);
};
}
void setBaseComment(ConfigEntry<?> configEntry, String baseComment);
@Nullable String getFullComment(ConfigEntry<?> configEntry); @Nullable String getFullComment(ConfigEntry<?> configEntry);
} }

View File

@@ -1,5 +0,0 @@
package de.siphalor.tweed5.defaultextensions.comment.api;
public interface EntryComment {
String comment();
}

View File

@@ -2,32 +2,32 @@ package de.siphalor.tweed5.defaultextensions.comment.impl;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer; import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
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.data.extension.api.extension.ReadWriteRelatedExtension; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.*; import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.Value;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@AutoService(CommentExtension.class) @AutoService(CommentExtension.class)
public class CommentExtensionImpl implements ReadWriteRelatedExtension, CommentExtension { public class CommentExtensionImpl implements ReadWriteRelatedExtension, CommentExtension {
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
@Getter @Getter
private final RegisteredExtensionData<EntryExtensionsData, InternalCommentEntryData> internalEntryDataExtension; private final PatchworkPartAccess<CustomEntryData> customEntryDataAccess;
private final DefaultMiddlewareContainer<CommentProducer> middlewareContainer; private final DefaultMiddlewareContainer<CommentProducer> middlewareContainer;
public CommentExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) { public CommentExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) {
this.configContainer = configContainer; this.configContainer = configContainer;
this.internalEntryDataExtension = context.registerEntryExtensionData(InternalCommentEntryData.class); this.customEntryDataAccess = context.registerEntryExtensionData(CustomEntryData.class);
context.registerEntryExtensionData(EntryComment.class);
this.middlewareContainer = new DefaultMiddlewareContainer<>(); this.middlewareContainer = new DefaultMiddlewareContainer<>();
} }
@@ -48,31 +48,49 @@ public class CommentExtensionImpl implements ReadWriteRelatedExtension, CommentE
@Override @Override
public @Nullable Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware() { public @Nullable Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware() {
return TweedEntryWriterCommentMiddleware.INSTANCE; return new TweedEntryWriterCommentMiddleware(this);
}
@Override
public void setBaseComment(ConfigEntry<?> configEntry, String baseComment) {
if (configEntry.container() != configContainer) {
throw new IllegalArgumentException("config entry doesn't belong to config container of this extension");
} else if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) {
throw new IllegalStateException("config container must not be initialized");
}
getOrCreateCustomEntryData(configEntry).baseComment(baseComment);
} }
@Override @Override
public void initEntry(ConfigEntry<?> configEntry) { public void initEntry(ConfigEntry<?> configEntry) {
EntryExtensionsData entryExtensionsData = configEntry.extensionsData(); CustomEntryData entryData = getOrCreateCustomEntryData(configEntry);
String baseComment; entryData.commentProducer(middlewareContainer.process(entry -> entryData.baseComment()));
if (entryExtensionsData.isPatchworkPartSet(EntryComment.class)) { }
baseComment = ((EntryComment) entryExtensionsData).comment();
} else {
baseComment = "";
}
CommentProducer middleware = middlewareContainer.process(entry -> baseComment); private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
internalEntryDataExtension.set(entryExtensionsData, new InternalCommentEntryDataImpl(middleware)); CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
customEntryData = new CustomEntryData();
entry.extensionsData().set(customEntryDataAccess, customEntryData);
}
return customEntryData;
} }
@Override @Override
public @Nullable String getFullComment(@NonNull ConfigEntry<?> configEntry) { public @Nullable String getFullComment(ConfigEntry<?> configEntry) {
String comment = ((InternalCommentEntryData) configEntry.extensionsData()).commentProducer().createComment(configEntry); CustomEntryData customEntryData = configEntry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
return null;
}
String comment = customEntryData.commentProducer().createComment(configEntry);
return comment.isEmpty() ? null : comment; return comment.isEmpty() ? null : comment;
} }
@Value
private static class InternalCommentEntryDataImpl implements InternalCommentEntryData { @Data
CommentProducer commentProducer; private static class CustomEntryData {
private String baseComment = "";
private CommentProducer commentProducer;
} }
} }

View File

@@ -1,7 +0,0 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
public interface InternalCommentEntryData {
CommentProducer commentProducer();
}

View File

@@ -5,12 +5,14 @@ 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.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@RequiredArgsConstructor
class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?, ?>> { class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?, ?>> {
public static final TweedEntryWriterCommentMiddleware INSTANCE = new TweedEntryWriterCommentMiddleware(); private final CommentExtension commentExtension;
@Override @Override
public String id() { public String id() {
@@ -49,7 +51,7 @@ class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?
} }
@RequiredArgsConstructor @RequiredArgsConstructor
private static class CompoundDataVisitor implements TweedDataVisitor { private class CompoundDataVisitor implements TweedDataVisitor {
private final TweedDataVisitor delegate; private final TweedDataVisitor delegate;
private final CompoundConfigEntry<?> compoundConfigEntry; private final CompoundConfigEntry<?> compoundConfigEntry;
@@ -134,11 +136,7 @@ class TweedEntryWriterCommentMiddleware implements Middleware<TweedEntryWriter<?
} }
} }
private static @Nullable String getEntryComment(ConfigEntry<?> entry) { private @Nullable String getEntryComment(ConfigEntry<?> entry) {
if (!entry.extensionsData().isPatchworkPartSet(InternalCommentEntryData.class)) { return commentExtension.getFullComment(entry);
return null;
}
String comment = ((InternalCommentEntryData) entry.extensionsData()).commentProducer().createComment(entry).trim();
return comment.isEmpty() ? null : comment;
} }
} }

View File

@@ -5,7 +5,7 @@ import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque; import java.util.Deque;
public class PathTracking implements PatherData { public class PathTracking {
private final StringBuilder pathBuilder = new StringBuilder(256); private final StringBuilder pathBuilder = new StringBuilder(256);
private final Deque<Context> contextStack = new ArrayDeque<>(50); private final Deque<Context> contextStack = new ArrayDeque<>(50);
private final Deque<String> pathParts = new ArrayDeque<>(50); private final Deque<String> pathParts = new ArrayDeque<>(50);
@@ -52,8 +52,7 @@ public class PathTracking implements PatherData {
return index; return index;
} }
@Override public String currentPath() {
public String valuePath() {
return pathBuilder.toString(); return pathBuilder.toString();
} }

View File

@@ -3,6 +3,7 @@ package de.siphalor.tweed5.defaultextensions.pather.api;
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.ConfigEntryValueVisitor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
@RequiredArgsConstructor @RequiredArgsConstructor
public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisitor { public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisitor {
@@ -10,7 +11,7 @@ public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisi
private final PathTracking pathTracking; private final PathTracking pathTracking;
@Override @Override
public <T> void visitEntry(ConfigEntry<T> entry, T value) { public <T extends @Nullable Object> void visitEntry(ConfigEntry<T> entry, T value) {
delegate.visitEntry(entry, value); delegate.visitEntry(entry, value);
entryVisited(); entryVisited();
} }

View File

@@ -1,9 +0,0 @@
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

@@ -1,8 +1,13 @@
package de.siphalor.tweed5.defaultextensions.pather.api; package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.core.api.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext;
import de.siphalor.tweed5.defaultextensions.pather.impl.PatherExtensionImpl; import de.siphalor.tweed5.defaultextensions.pather.impl.PatherExtensionImpl;
public interface PatherExtension extends TweedExtension { public interface PatherExtension extends TweedExtension {
Class<? extends PatherExtension> DEFAULT = PatherExtensionImpl.class; Class<? extends PatherExtension> DEFAULT = PatherExtensionImpl.class;
String getPath(TweedReadContext context);
String getPath(TweedWriteContext context);
} }

View File

@@ -2,29 +2,33 @@ package de.siphalor.tweed5.defaultextensions.pather.impl;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware; import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.*; import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData; import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader; import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.*; 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 de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.val; import lombok.val;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@AutoService(PatherExtension.class) @AutoService(PatherExtension.class)
@NullUnmarked
public class PatherExtensionImpl implements PatherExtension, TweedExtension, ReadWriteRelatedExtension { public class PatherExtensionImpl implements PatherExtension, TweedExtension, ReadWriteRelatedExtension {
private static final String PATHER_ID = "pather"; private static final String PATHER_ID = "pather";
private RegisteredExtensionData<ReadWriteContextExtensionsData, PatherData> rwContextPathTrackingData; private final Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware = createEntryReaderMiddleware();
private Middleware<TweedEntryReader<?, ?>> entryReaderMiddleware; private final Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware = createEntryWriterMiddleware();
private Middleware<TweedEntryWriter<?, ?>> entryWriterMiddleware;
private @Nullable PatchworkPartAccess<PathTracking> rwContextPathTrackingAccess;
@Override @Override
public String getId() { public String getId() {
@@ -33,13 +37,32 @@ public class PatherExtensionImpl implements PatherExtension, TweedExtension, Rea
@Override @Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) { public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
rwContextPathTrackingData = context.registerReadWriteContextExtensionData(PatherData.class); rwContextPathTrackingAccess = context.registerReadWriteContextExtensionData(PathTracking.class);
entryReaderMiddleware = createEntryReaderMiddleware();
entryWriterMiddleware = createEntryWriterMiddleware();
} }
private @NonNull Middleware<TweedEntryReader<?, ?>> createEntryReaderMiddleware() { @Override
public String getPath(TweedReadContext context) {
assert rwContextPathTrackingAccess != null;
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking == null) {
throw new IllegalStateException("Path tracking is not active!");
}
return pathTracking.currentPath();
}
@Override
public String getPath(TweedWriteContext context) {
assert rwContextPathTrackingAccess != null;
PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking == null) {
throw new IllegalStateException("Path tracking is not active!");
}
return pathTracking.currentPath();
}
private Middleware<TweedEntryReader<?, ?>> createEntryReaderMiddleware() {
return new Middleware<TweedEntryReader<?, ?>>() { return new Middleware<TweedEntryReader<?, ?>>() {
@Override @Override
public String id() { public String id() {
@@ -48,16 +71,19 @@ public class PatherExtensionImpl implements PatherExtension, TweedExtension, Rea
@Override @Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) { public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
assert rwContextPathTrackingAccess != null;
//noinspection unchecked //noinspection unchecked
val castedInner = (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner; val castedInner = (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> { return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
if (context.extensionsData().isPatchworkPartSet(PatherData.class)) { PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking != null) {
return castedInner.read(reader, entry, context); return castedInner.read(reader, entry, context);
} }
PathTracking pathTracking = new PathTracking(); pathTracking = new PathTracking();
rwContextPathTrackingData.set(context.extensionsData(), pathTracking); context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context); return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context);
}; };
} }
@@ -73,17 +99,20 @@ public class PatherExtensionImpl implements PatherExtension, TweedExtension, Rea
@Override @Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) { public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
assert rwContextPathTrackingAccess != null;
//noinspection unchecked //noinspection unchecked
val castedInner = (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) inner; val castedInner = (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedDataVisitor writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) -> { return (TweedDataVisitor writer, Object value, ConfigEntry<Object> entry, TweedWriteContext context) -> {
if (context.extensionsData().isPatchworkPartSet(PatherData.class)) { PathTracking pathTracking = context.extensionsData().get(rwContextPathTrackingAccess);
if (pathTracking != null) {
castedInner.write(writer, value, entry, context); castedInner.write(writer, value, entry, context);
return; return;
} }
PathTracking pathTracking = new PathTracking(); pathTracking = new PathTracking();
rwContextPathTrackingData.set(context.extensionsData(), pathTracking); context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context); castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context);
}; };
} }

View File

@@ -1,9 +0,0 @@
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

@@ -2,12 +2,54 @@ package de.siphalor.tweed5.defaultextensions.validation.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; 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.core.api.middleware.Middleware;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.SimpleValidatorMiddleware;
import de.siphalor.tweed5.defaultextensions.validation.impl.ValidationExtensionImpl; import de.siphalor.tweed5.defaultextensions.validation.impl.ValidationExtensionImpl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
public interface ValidationExtension extends TweedExtension { public interface ValidationExtension extends TweedExtension {
Class<? extends ValidationExtension> DEFAULT = ValidationExtensionImpl.class; Class<? extends ValidationExtension> DEFAULT = ValidationExtensionImpl.class;
static <C extends ConfigEntry<T>, T> Consumer<C> validators(ConfigEntryValidator... validators) {
return entry -> {
ValidationExtension extension = entry.container().extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException("No validation extension registered"));
extension.addValidators(entry, validators);
};
}
static <C extends ConfigEntry<T>, T> Function<C, ValidationIssues> validate(T value) {
return entry -> {
ValidationExtension extension = entry.container().extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException("No validation extension registered"));
return extension.validate(entry, value);
};
}
default <T> void addValidators(ConfigEntry<T> entry, ConfigEntryValidator... validators) {
String lastId = null;
for (ConfigEntryValidator validator : validators) {
String id = UUID.randomUUID().toString();
Set<String> mustComeAfter = lastId == null ? Collections.emptySet() : Collections.singleton(lastId);
addValidatorMiddleware(entry, new SimpleValidatorMiddleware(id, validator) {
@Override
public Set<String> mustComeAfter() {
return mustComeAfter;
}
});
lastId = id;
}
}
<T> void addValidatorMiddleware(ConfigEntry<T> entry, Middleware<ConfigEntryValidator> validator);
<T extends @Nullable Object> ValidationIssues validate(ConfigEntry<T> entry, T value); <T extends @Nullable Object> ValidationIssues validate(ConfigEntry<T> entry, T value);
} }

View File

@@ -3,6 +3,7 @@ package de.siphalor.tweed5.defaultextensions.validation.api.result;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -11,16 +12,16 @@ import java.util.function.Function;
@Getter @Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidationResult<T> { public class ValidationResult<T extends @Nullable Object> {
private final T value; private final T value;
private final Collection<ValidationIssue> issues; private final Collection<ValidationIssue> issues;
private final boolean hasError; private final boolean hasError;
public static <T> ValidationResult<T> ok(T value) { public static <T extends @Nullable Object> ValidationResult<T> ok(T value) {
return new ValidationResult<>(value, Collections.emptyList(), false); return new ValidationResult<>(value, Collections.emptyList(), false);
} }
public static <T> ValidationResult<T> withIssues(T value, Collection<ValidationIssue> issues) { public static <T extends @Nullable Object> ValidationResult<T> withIssues(T value, Collection<ValidationIssue> issues) {
return new ValidationResult<>(value, issues, issuesContainError(issues)); return new ValidationResult<>(value, issues, issuesContainError(issues));
} }

View File

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

View File

@@ -4,8 +4,6 @@ import com.google.auto.service.AutoService;
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.ConfigEntryValueVisitor; 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.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer; import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
@@ -14,7 +12,6 @@ import de.siphalor.tweed5.core.api.middleware.MiddlewareContainer;
import de.siphalor.tweed5.data.extension.api.TweedEntryReadException; import de.siphalor.tweed5.data.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader; import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedReadContext; 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.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader; import de.siphalor.tweed5.dataapi.api.TweedDataReader;
@@ -22,20 +19,16 @@ import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtensio
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer; import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking; import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryValueVisitor; import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryValueVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherData;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension; import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator; 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.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationProvidingExtension; import de.siphalor.tweed5.defaultextensions.validation.api.ValidationProvidingExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel; 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.result.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import lombok.Getter; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.RequiredArgsConstructor; import lombok.*;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -48,7 +41,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
); );
private static final ConfigEntryValidator PRIMITIVE_VALIDATOR = new ConfigEntryValidator() { private static final ConfigEntryValidator PRIMITIVE_VALIDATOR = new ConfigEntryValidator() {
@Override @Override
public <T> ValidationResult<T> validate(@NotNull ConfigEntry<T> configEntry, @Nullable T value) { public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
if (value == null) { if (value == null) {
//noinspection unchecked //noinspection unchecked
return (ValidationResult<T>) PRIMITIVE_IS_NULL_RESULT; return (ValidationResult<T>) PRIMITIVE_IS_NULL_RESULT;
@@ -57,33 +50,32 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
} }
@Override @Override
public <T> String description(@NotNull ConfigEntry<T> configEntry) { public <T> String description(ConfigEntry<T> configEntry) {
return "Value must not be null."; return "Value must not be null.";
} }
}; };
private static final ConfigEntryValidator NOOP_VALIDATOR = new ConfigEntryValidator() { private static final ConfigEntryValidator NOOP_VALIDATOR = new ConfigEntryValidator() {
@Override @Override
public <T> ValidationResult<T> validate(@NotNull ConfigEntry<T> configEntry, @Nullable T value) { public <T> ValidationResult<@Nullable T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
return ValidationResult.ok(value); return ValidationResult.ok(value);
} }
@Override @Override
public <T> String description(@NotNull ConfigEntry<T> configEntry) { public <T> String description(ConfigEntry<T> configEntry) {
return ""; return "";
} }
}; };
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
private final RegisteredExtensionData<EntryExtensionsData, InternalValidationEntryData> validationEntryDataExtension; private final PatchworkPartAccess<CustomEntryData> customEntryDataAccess;
private final MiddlewareContainer<ConfigEntryValidator> entryValidatorMiddlewareContainer private final MiddlewareContainer<ConfigEntryValidator> entryValidatorMiddlewareContainer
= new DefaultMiddlewareContainer<>(); = new DefaultMiddlewareContainer<>();
private @Nullable RegisteredExtensionData<ReadWriteContextExtensionsData, ValidationIssues> private @Nullable PatchworkPartAccess<ValidationIssues> readContextValidationIssuesAccess;
readContextValidationIssuesExtensionData; private @Nullable PatherExtension patherExtension;
public ValidationExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) { public ValidationExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) {
this.configContainer = configContainer; this.configContainer = configContainer;
this.validationEntryDataExtension = context.registerEntryExtensionData(InternalValidationEntryData.class); this.customEntryDataAccess = context.registerEntryExtensionData(CustomEntryData.class);
context.registerEntryExtensionData(EntrySpecificValidation.class);
context.registerExtension(PatherExtension.class); context.registerExtension(PatherExtension.class);
} }
@@ -102,6 +94,9 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
} }
} }
entryValidatorMiddlewareContainer.seal(); entryValidatorMiddlewareContainer.seal();
patherExtension = configContainer.extension(PatherExtension.class)
.orElseThrow(() -> new IllegalStateException("Missing requested PatherExtension"));
} }
@Override @Override
@@ -116,16 +111,21 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
public CommentProducer process(CommentProducer inner) { public CommentProducer process(CommentProducer inner) {
return entry -> { return entry -> {
String baseComment = inner.createComment(entry); String baseComment = inner.createComment(entry);
if (entry.extensionsData().isPatchworkPartSet(InternalValidationEntryData.class)) { CustomEntryData entryData = entry.extensionsData().get(customEntryDataAccess);
String validationDescription = ((InternalValidationEntryData) entry.extensionsData()) if (entryData == null || entryData.completeValidator() == null) {
.completeEntryValidator() return baseComment;
.description(entry) }
.trim(); String validationDescription = entryData.completeValidator()
if (!validationDescription.isEmpty()) { .description(entry)
baseComment += "\n\n" + validationDescription; .trim();
} if (validationDescription.isEmpty()) {
return baseComment;
}
if (baseComment.isEmpty()) {
return validationDescription;
} else {
return "\n\n" + validationDescription;
} }
return baseComment;
}; };
} }
}; };
@@ -133,7 +133,13 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
@Override @Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) { public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
readContextValidationIssuesExtensionData = context.registerReadWriteContextExtensionData(ValidationIssues.class); readContextValidationIssuesAccess = context.registerReadWriteContextExtensionData(ValidationIssues.class);
}
@Override
public <T> void addValidatorMiddleware(ConfigEntry<T> entry, Middleware<ConfigEntryValidator> validator) {
CustomEntryData entryData = getOrCreateCustomEntryData(entry);
entryData.addValidator(validator);
} }
@Override @Override
@@ -145,26 +151,26 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
baseValidator = NOOP_VALIDATOR; baseValidator = NOOP_VALIDATOR;
} }
ConfigEntryValidator entryValidator; CustomEntryData entryData = getOrCreateCustomEntryData(configEntry);
Collection<Middleware<ConfigEntryValidator>> entrySpecificValidators = getEntrySpecificValidators(configEntry);
if (entrySpecificValidators.isEmpty()) { if (entryData.validators().isEmpty()) {
entryValidator = entryValidatorMiddlewareContainer.process(baseValidator); entryData.completeValidator(entryValidatorMiddlewareContainer.process(baseValidator));
} else { } else {
DefaultMiddlewareContainer<ConfigEntryValidator> entrySpecificValidatorContainer = new DefaultMiddlewareContainer<>(); DefaultMiddlewareContainer<ConfigEntryValidator> entrySpecificValidatorContainer = new DefaultMiddlewareContainer<>();
entrySpecificValidatorContainer.registerAll(entryValidatorMiddlewareContainer.middlewares()); entrySpecificValidatorContainer.registerAll(entryValidatorMiddlewareContainer.middlewares());
entrySpecificValidatorContainer.registerAll(entrySpecificValidators); entrySpecificValidatorContainer.registerAll(entryData.validators());
entrySpecificValidatorContainer.seal(); entrySpecificValidatorContainer.seal();
entryValidator = entrySpecificValidatorContainer.process(baseValidator); entryData.completeValidator(entrySpecificValidatorContainer.process(baseValidator));
} }
validationEntryDataExtension.set(configEntry.extensionsData(), new InternalValidationEntryDataImpl(entryValidator));
} }
private Collection<Middleware<ConfigEntryValidator>> getEntrySpecificValidators(ConfigEntry<?> configEntry) { private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
if (!configEntry.extensionsData().isPatchworkPartSet(EntrySpecificValidation.class)) { CustomEntryData entryData = entry.extensionsData().get(customEntryDataAccess);
return Collections.emptyList(); if (entryData == null) {
entryData = new CustomEntryData();
entry.extensionsData().set(customEntryDataAccess, entryData);
} }
return ((EntrySpecificValidation) configEntry.extensionsData()).validators(); return entryData;
} }
@Override @Override
@@ -173,7 +179,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
} }
@Override @Override
public <T> ValidationIssues validate(@NotNull ConfigEntry<T> entry, @Nullable T value) { public <T> ValidationIssues validate(ConfigEntry<T> entry, @Nullable T value) {
PathTracking pathTracking = new PathTracking(); PathTracking pathTracking = new PathTracking();
ValidatingConfigEntryVisitor validatingVisitor = new ValidatingConfigEntryVisitor(pathTracking); ValidatingConfigEntryVisitor validatingVisitor = new ValidatingConfigEntryVisitor(pathTracking);
@@ -182,9 +188,22 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
return validatingVisitor.validationIssues(); return validatingVisitor.validationIssues();
} }
@Value @Data
private static class InternalValidationEntryDataImpl implements InternalValidationEntryData { private static class CustomEntryData {
ConfigEntryValidator completeEntryValidator; @Setter(AccessLevel.NONE)
private @Nullable List<Middleware<ConfigEntryValidator>> validators;
private @Nullable ConfigEntryValidator completeValidator;
public List<Middleware<ConfigEntryValidator>> validators() {
return validators == null ? Collections.emptyList() : validators;
}
public void addValidator(Middleware<ConfigEntryValidator> validator) {
if (validators == null) {
validators = new ArrayList<>();
}
validators.add(validator);
}
} }
private class EntryValidationReaderMiddleware implements Middleware<TweedEntryReader<?, ?>> { private class EntryValidationReaderMiddleware implements Middleware<TweedEntryReader<?, ?>> {
@@ -200,23 +219,29 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
@Override @Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) { public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
assert readContextValidationIssuesAccess != null && patherExtension != null;
//noinspection unchecked //noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> castedInner = (TweedEntryReader<Object, ConfigEntry<Object>>) inner; TweedEntryReader<Object, ConfigEntry<Object>> castedInner = (TweedEntryReader<Object, ConfigEntry<Object>>) inner;
return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> { return (TweedDataReader reader, ConfigEntry<Object> entry, TweedReadContext context) -> {
ValidationIssues validationIssues; ValidationIssues validationIssues = context.extensionsData().get(readContextValidationIssuesAccess);
if (!context.extensionsData().isPatchworkPartSet(ValidationIssues.class)) { if (validationIssues == null) {
validationIssues = new ValidationIssuesImpl(); validationIssues = new ValidationIssuesImpl();
readContextValidationIssuesExtensionData.set(context.extensionsData(), validationIssues);
} else { } else {
validationIssues = (ValidationIssues) context.extensionsData(); validationIssues = (ValidationIssues) context.extensionsData();
} }
Object value = castedInner.read(reader, entry, context); Object value = castedInner.read(reader, entry, context);
ValidationResult<Object> validationResult = ((InternalValidationEntryData) entry.extensionsData()).completeEntryValidator().validate(entry, value); ConfigEntryValidator entryValidator = entry.extensionsData()
.get(customEntryDataAccess)
.completeValidator();
assert entryValidator != null;
if (!validationResult.issues().isEmpty() && context.extensionsData().isPatchworkPartSet(PatherData.class)) { ValidationResult<Object> validationResult = entryValidator.validate(entry, value);
String path = ((PatherData) context.extensionsData()).valuePath();
if (!validationResult.issues().isEmpty()) {
String path = patherExtension.getPath(context);
validationIssues.issuesByPath().put(path, new ValidationIssues.EntryIssues( validationIssues.issuesByPath().put(path, new ValidationIssues.EntryIssues(
entry, entry,
validationResult.issues() validationResult.issues()
@@ -234,15 +259,19 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
private static class ValidatingConfigEntryVisitor implements ConfigEntryValueVisitor { private class ValidatingConfigEntryVisitor implements ConfigEntryValueVisitor {
private final PathTracking pathTracking; private final PathTracking pathTracking;
private final ValidationIssues validationIssues = new ValidationIssuesImpl(); private final ValidationIssues validationIssues = new ValidationIssuesImpl();
@Override @Override
public <T> void visitEntry(ConfigEntry<T> entry, T value) { public <T> void visitEntry(ConfigEntry<T> entry, T value) {
ValidationResult<T> result = ((InternalValidationEntryData) entry.extensionsData()).completeEntryValidator().validate(entry, value); CustomEntryData entryData = entry.extensionsData().get(customEntryDataAccess);
assert entryData != null;
ConfigEntryValidator entryValidator = entryData.completeValidator();
assert entryValidator != null;
ValidationResult<T> result = entryValidator.validate(entry, value);
if (!result.issues().isEmpty()) { if (!result.issues().isEmpty()) {
validationIssues.issuesByPath().put(pathTracking.valuePath(), new ValidationIssues.EntryIssues(entry, result.issues())); validationIssues.issuesByPath().put(pathTracking.currentPath(), new ValidationIssues.EntryIssues(entry, result.issues()));
} }
} }

View File

@@ -1,8 +1,21 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.api; package de.siphalor.tweed5.defaultextensions.validationfallback.api;
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.defaultextensions.validationfallback.impl.ValidationFallbackExtensionImpl; import de.siphalor.tweed5.defaultextensions.validationfallback.impl.ValidationFallbackExtensionImpl;
import java.util.function.Consumer;
public interface ValidationFallbackExtension extends TweedExtension { public interface ValidationFallbackExtension extends TweedExtension {
Class<? extends ValidationFallbackExtension> DEFAULT = ValidationFallbackExtensionImpl.class; Class<? extends ValidationFallbackExtension> DEFAULT = ValidationFallbackExtensionImpl.class;
static <C extends ConfigEntry<T>, T> Consumer<C> validationFallbackValue(T value) {
return entry -> {
ValidationFallbackExtension extension = entry.container().extension(ValidationFallbackExtension.class)
.orElseThrow(() -> new IllegalStateException("ValidationFallbackExtension is not registered"));
extension.setFallbackValue(entry, value);
};
}
<T> void setFallbackValue(ConfigEntry<T> entry, T value);
} }

View File

@@ -1,7 +0,0 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.api;
import org.jspecify.annotations.Nullable;
public interface ValidationFallbackValue {
@Nullable Object validationFallbackValue();
}

View File

@@ -10,7 +10,8 @@ import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssu
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension; import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackValue; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Data;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@@ -20,8 +21,11 @@ import java.util.stream.Collectors;
@AutoService(ValidationFallbackExtension.class) @AutoService(ValidationFallbackExtension.class)
public class ValidationFallbackExtensionImpl implements ValidationFallbackExtension, ValidationProvidingExtension { public class ValidationFallbackExtensionImpl implements ValidationFallbackExtension, ValidationProvidingExtension {
private final PatchworkPartAccess<CustomEntryData> customEntryDataAccess;
public ValidationFallbackExtensionImpl(TweedExtensionSetupContext context) { public ValidationFallbackExtensionImpl(TweedExtensionSetupContext context) {
context.registerEntryExtensionData(ValidationFallbackValue.class); customEntryDataAccess = context.registerEntryExtensionData(CustomEntryData.class);
} }
@Override @Override
@@ -29,12 +33,31 @@ public class ValidationFallbackExtensionImpl implements ValidationFallbackExtens
return "validation-fallback"; return "validation-fallback";
} }
@Override
public <T> void setFallbackValue(ConfigEntry<T> entry, T value) {
getOrCreateCustomEntryData(entry).fallbackValue(value);
}
private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
customEntryData = new CustomEntryData();
entry.extensionsData().set(customEntryDataAccess, customEntryData);
}
return customEntryData;
}
@Override @Override
public Middleware<ConfigEntryValidator> validationMiddleware() { public Middleware<ConfigEntryValidator> validationMiddleware() {
return new ValidationFallbackMiddleware(); return new ValidationFallbackMiddleware();
} }
private static class ValidationFallbackMiddleware implements Middleware<ConfigEntryValidator> { @Data
private static class CustomEntryData {
@Nullable Object fallbackValue;
}
private class ValidationFallbackMiddleware implements Middleware<ConfigEntryValidator> {
@Override @Override
public String id() { public String id() {
return "validation-fallback"; return "validation-fallback";
@@ -59,13 +82,14 @@ public class ValidationFallbackExtensionImpl implements ValidationFallbackExtens
if (!result.hasError()) { if (!result.hasError()) {
return result; return result;
} }
if (!configEntry.extensionsData().isPatchworkPartSet(ValidationFallbackValue.class)) { CustomEntryData entryData = configEntry.extensionsData().get(customEntryDataAccess);
if (entryData == null) {
return result; return result;
} }
Object fallbackValue = ((ValidationFallbackValue) configEntry.extensionsData()).validationFallbackValue(); Object fallbackValue = entryData.fallbackValue();
if (fallbackValue != null) { if (fallbackValue != null) {
if (fallbackValue.getClass() == configEntry.valueClass()) { if (configEntry.valueClass().isInstance(fallbackValue)) {
//noinspection unchecked //noinspection unchecked
fallbackValue = configEntry.deepCopy((T) fallbackValue); fallbackValue = configEntry.deepCopy((T) fallbackValue);
} else { } else {
@@ -90,12 +114,11 @@ public class ValidationFallbackExtensionImpl implements ValidationFallbackExtens
@Override @Override
public <T> String description(ConfigEntry<T> configEntry) { public <T> String description(ConfigEntry<T> configEntry) {
if (!configEntry.extensionsData().isPatchworkPartSet(ValidationFallbackValue.class)) { CustomEntryData entryData = configEntry.extensionsData().get(customEntryDataAccess);
if (entryData == null) {
return inner.description(configEntry); return inner.description(configEntry);
} }
return inner.description(configEntry) + return inner.description(configEntry) + "\n\nDefault/Fallback value: " + entryData.fallbackValue();
"\n\nDefault/Fallback value: " +
((ValidationFallbackValue) configEntry.extensionsData()).validationFallbackValue();
} }
}; };
} }

View File

@@ -1,27 +1,20 @@
package de.siphalor.tweed5.defaultextensions.comment.impl; package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension; import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware; import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer; import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension; import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.hjson.HjsonCommentType; import de.siphalor.tweed5.data.hjson.HjsonCommentType;
import de.siphalor.tweed5.data.hjson.HjsonWriter; import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension; import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension; import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer; import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.comment.api.EntryComment;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor; import org.jspecify.annotations.NonNull;
import lombok.Value;
import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.NullUnmarked;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -31,6 +24,9 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*;
import static de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension.baseComment;
import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap;
import static java.util.Map.entry; import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -38,44 +34,44 @@ import static org.junit.jupiter.api.Assertions.*;
@NullUnmarked @NullUnmarked
class CommentExtensionImplTest { class CommentExtensionImplTest {
private DefaultConfigContainer<Map<String, Object>> configContainer; private DefaultConfigContainer<@NonNull Map<String, Object>> configContainer;
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry; private CompoundConfigEntry<Map<String, Object>> rootEntry;
private SimpleConfigEntryImpl<Integer> intEntry; private SimpleConfigEntry<Integer> intEntry;
private SimpleConfigEntryImpl<String> stringEntry; private SimpleConfigEntry<String> stringEntry;
private SimpleConfigEntryImpl<Long> noCommentEntry; private SimpleConfigEntry<Long> noCommentEntry;
@SafeVarargs @SafeVarargs
final void setupContainer(Class<? extends TweedExtension>... extraExtensions) { final void setupContainer(Class<? extends TweedExtension>... extraExtensions) {
configContainer = new DefaultConfigContainer<>(); configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(CommentExtension.DEFAULT); configContainer.registerExtension(CommentExtension.DEFAULT);
configContainer.registerExtension(ReadWriteExtension.DEFAULT);
configContainer.registerExtensions(extraExtensions); configContainer.registerExtensions(extraExtensions);
configContainer.finishExtensionSetup(); configContainer.finishExtensionSetup();
intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class); intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
stringEntry = new SimpleConfigEntryImpl<>(configContainer, String.class); .apply(entryReaderWriter(intReaderWriter()))
noCommentEntry = new SimpleConfigEntryImpl<>(configContainer, Long.class); .apply(baseComment("It is an integer"));
stringEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()))
.apply(baseComment("It is a string"));
noCommentEntry = new SimpleConfigEntryImpl<>(configContainer, Long.class)
.apply(entryReaderWriter(longReaderWriter()));
//noinspection unchecked //noinspection unchecked
rootEntry = new StaticMapCompoundConfigEntryImpl<>( rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer, configContainer,
((Class<Map<String, Object>>)(Class<?>) Map.class), ((Class<Map<String, Object>>) (Class<?>) Map.class),
LinkedHashMap::new, LinkedHashMap::new,
sequencedMap(List.of( sequencedMap(List.of(
entry("int", intEntry), entry("int", intEntry),
entry("string", stringEntry), entry("string", stringEntry),
entry("noComment", noCommentEntry) entry("noComment", noCommentEntry)
)) )))
); .apply(entryReaderWriter(compoundReaderWriter()))
.apply(baseComment("This is the root value.\nIt is the topmost value in the tree."));
configContainer.attachTree(rootEntry); configContainer.attachTree(rootEntry);
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, EntryComment> commentData = (RegisteredExtensionData<EntryExtensionsData, EntryComment>) configContainer.entryDataExtensions().get(EntryComment.class);
commentData.set(rootEntry.extensionsData(), new CommentImpl("This is the root value.\nIt is the topmost value in the tree."));
commentData.set(intEntry.extensionsData(), new CommentImpl("It is an integer"));
commentData.set(stringEntry.extensionsData(), new CommentImpl("It is a string"));
} }
@Test @Test
@@ -102,8 +98,7 @@ class CommentExtensionImplTest {
@Test @Test
void simpleCommentsInHjson() { void simpleCommentsInHjson() {
setupContainer(ReadWriteExtension.DEFAULT); setupContainer();
setupReadWriteTypes();
configContainer.initialize(); configContainer.initialize();
Map<String, Object> value = new HashMap<>(); Map<String, Object> value = new HashMap<>();
@@ -134,21 +129,6 @@ class CommentExtensionImplTest {
""", output.toString()); """, output.toString());
} }
private void setupReadWriteTypes() {
//noinspection unchecked
var readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(rootEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.compoundReaderWriter()));
readerWriterData.set(intEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.intReaderWriter()));
readerWriterData.set(stringEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.stringReaderWriter()));
readerWriterData.set(noCommentEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.longReaderWriter()));
}
@Value
private static class CommentImpl implements EntryComment {
String comment;
}
@NoArgsConstructor @NoArgsConstructor
public static class TestCommentModifyingExtension implements TweedExtension, CommentModifyingExtension { public static class TestCommentModifyingExtension implements TweedExtension, CommentModifyingExtension {
@Override @Override
@@ -158,7 +138,7 @@ class CommentExtensionImplTest {
@Override @Override
public Middleware<CommentProducer> commentMiddleware() { public Middleware<CommentProducer> commentMiddleware() {
return new Middleware<CommentProducer>() { return new Middleware<>() {
@Override @Override
public String id() { public String id() {
return "test"; return "test";
@@ -171,19 +151,4 @@ class CommentExtensionImplTest {
}; };
} }
} }
@RequiredArgsConstructor
private static class TrivialEntryReaderWriterDefinition implements EntryReaderWriterDefinition {
private final TweedEntryReaderWriter<?, ?> readerWriter;
@Override
public TweedEntryReader<?, ?> reader() {
return readerWriter;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return readerWriter;
}
}
} }

View File

@@ -1,37 +1,39 @@
package de.siphalor.tweed5.defaultextensions.validation.impl; package de.siphalor.tweed5.defaultextensions.validation.impl;
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.extension.EntryExtensionsData; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer; import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension; import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.EntryComment;
import de.siphalor.tweed5.defaultextensions.validation.api.EntrySpecificValidation;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension; 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.ValidationIssue;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssueLevel; 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.result.ValidationIssues;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.NumberRangeValidator; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.CsvSource;
import java.util.*; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension.baseComment;
import static de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension.validators;
import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap;
import static java.util.Map.entry; import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
class ValidationExtensionImplTest { class ValidationExtensionImplTest {
private DefaultConfigContainer<Map<String, Object>> configContainer; private DefaultConfigContainer<Map<String, Object>> configContainer;
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry; private CompoundConfigEntry<Map<String, Object>> rootEntry;
private SimpleConfigEntryImpl<Byte> byteEntry; private SimpleConfigEntry<Byte> byteEntry;
private SimpleConfigEntryImpl<Integer> intEntry; private SimpleConfigEntry<Integer> intEntry;
private SimpleConfigEntryImpl<Double> doubleEntry; private SimpleConfigEntry<Double> doubleEntry;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@@ -41,9 +43,13 @@ class ValidationExtensionImplTest {
configContainer.registerExtension(ValidationExtension.DEFAULT); configContainer.registerExtension(ValidationExtension.DEFAULT);
configContainer.finishExtensionSetup(); configContainer.finishExtensionSetup();
byteEntry = new SimpleConfigEntryImpl<>(configContainer, Byte.class); byteEntry = new SimpleConfigEntryImpl<>(configContainer, Byte.class)
intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class); .apply(validators(new NumberRangeValidator<>(Byte.class, (byte) 10, (byte) 100)));
doubleEntry = new SimpleConfigEntryImpl<>(configContainer, Double.class); intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(validators(new NumberRangeValidator<>(Integer.class, null, 123)))
.apply(baseComment("This is the main comment!"));
doubleEntry = new SimpleConfigEntryImpl<>(configContainer, Double.class)
.apply(validators(new NumberRangeValidator<>(Double.class, 0.5, null)));
//noinspection unchecked //noinspection unchecked
rootEntry = new StaticMapCompoundConfigEntryImpl<>( rootEntry = new StaticMapCompoundConfigEntryImpl<>(
@@ -59,25 +65,6 @@ class ValidationExtensionImplTest {
configContainer.attachTree(rootEntry); configContainer.attachTree(rootEntry);
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, EntryComment> commentData = (RegisteredExtensionData<EntryExtensionsData, EntryComment>) configContainer.entryDataExtensions().get(EntryComment.class);
commentData.set(intEntry.extensionsData(), () -> "This is the main comment!");
//noinspection unchecked
RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation> entrySpecificValidation = (RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation>) configContainer.entryDataExtensions().get(EntrySpecificValidation.class);
entrySpecificValidation.set(
byteEntry.extensionsData(),
() -> Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Byte.class, (byte) 10, (byte) 100)))
);
entrySpecificValidation.set(
intEntry.extensionsData(),
() -> Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Integer.class, null, 123)))
);
entrySpecificValidation.set(
doubleEntry.extensionsData(),
() -> Collections.singleton(new SimpleValidatorMiddleware("range", new NumberRangeValidator<>(Double.class, 0.5, null)))
);
configContainer.initialize(); configContainer.initialize();
} }

View File

@@ -1,30 +1,21 @@
package de.siphalor.tweed5.defaultextensions.validationfallback.impl; package de.siphalor.tweed5.defaultextensions.validationfallback.impl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer; import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension; import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.hjson.HjsonCommentType; import de.siphalor.tweed5.data.hjson.HjsonCommentType;
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.defaultextensions.comment.api.CommentExtension; import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator; 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.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssue; 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.ValidationIssueLevel;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult; import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.SimpleValidatorMiddleware;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension; import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackValue;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -33,18 +24,19 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Set;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.*;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*;
import static de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension.validators;
import static de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension.validationFallbackValue;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
class ValidationFallbackExtensionImplTest { class ValidationFallbackExtensionImplTest {
private DefaultConfigContainer<Integer> configContainer; private DefaultConfigContainer<Integer> configContainer;
private SimpleConfigEntryImpl<Integer> intEntry; private SimpleConfigEntry<Integer> intEntry;
@SuppressWarnings("unchecked")
@BeforeEach @BeforeEach
void setUp() { void setUp() {
configContainer = new DefaultConfigContainer<>(); configContainer = new DefaultConfigContainer<>();
@@ -55,92 +47,82 @@ class ValidationFallbackExtensionImplTest {
configContainer.finishExtensionSetup(); configContainer.finishExtensionSetup();
intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class); intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(nullableReader(intReaderWriter()), nullableWriter(intReaderWriter())))
.apply(validators(
new ConfigEntryValidator() {
@Override
public <T extends @Nullable Object> ValidationResult<T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
if (value == null) {
return ValidationResult.withIssues(
null, Collections.singleton(
new ValidationIssue(
"Value must not be null",
ValidationIssueLevel.ERROR
)
)
);
}
return ValidationResult.ok(value);
}
@Override
public <T> String description(ConfigEntry<T> configEntry) {
return "Must not be null.";
}
},
new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
assert value != null;
int intValue = (int) value;
if (intValue < 1) {
return ValidationResult.withIssues(
value,
Collections.singleton(new ValidationIssue(
"Must be greater or equal to 1",
ValidationIssueLevel.ERROR
))
);
}
if (intValue > 6) {
return ValidationResult.withIssues(
value,
Collections.singleton(new ValidationIssue(
"Must be smaller or equal to 6",
ValidationIssueLevel.ERROR
))
);
}
return ValidationResult.ok(value);
}
@Override
public <T> String description(ConfigEntry<T> configEntry) {
return "Must be between 1 and 6";
}
}
))
.apply(validationFallbackValue(3));
configContainer.attachTree(intEntry); configContainer.attachTree(intEntry);
RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation> entrySpecificValidation = (RegisteredExtensionData<EntryExtensionsData, EntrySpecificValidation>) configContainer.entryDataExtensions().get(EntrySpecificValidation.class);
entrySpecificValidation.set(intEntry.extensionsData(), () -> Arrays.asList(
new SimpleValidatorMiddleware("non-null", new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
if (value == null) {
return ValidationResult.withIssues(null, Collections.singleton(
new ValidationIssue("Value must not be null", ValidationIssueLevel.ERROR)
));
}
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "Must not be null.";
}
}),
new SimpleValidatorMiddleware("range", new ConfigEntryValidator() {
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, @Nullable T value) {
Integer intValue = (Integer) value;
if (intValue < 1) {
return ValidationResult.withIssues(value, Collections.singleton(new ValidationIssue("Must be greater or equal to 1", ValidationIssueLevel.ERROR)));
}
if (intValue > 6) {
return ValidationResult.withIssues(value, Collections.singleton(new ValidationIssue("Must be smaller or equal to 6", ValidationIssueLevel.ERROR)));
}
return ValidationResult.ok(value);
}
@Override
public @NotNull <T> String description(ConfigEntry<T> configEntry) {
return "Must be between 1 and 6";
}
}) {
@Override
public Set<String> mustComeAfter() {
return Collections.singleton("non-null");
}
}
));
RegisteredExtensionData<EntryExtensionsData, ValidationFallbackValue> validationFallbackValue = (RegisteredExtensionData<EntryExtensionsData, ValidationFallbackValue>) configContainer.entryDataExtensions().get(ValidationFallbackValue.class);
validationFallbackValue.set(intEntry.extensionsData(), () -> 3);
RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(intEntry.extensionsData(), new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return TweedEntryReaderWriters.nullableReader(TweedEntryReaderWriters.intReaderWriter());
}
@Override
public TweedEntryWriter<?, ?> writer() {
return TweedEntryReaderWriters.nullableWriter(TweedEntryReaderWriters.intReaderWriter());
}
});
configContainer.initialize(); configContainer.initialize();
} }
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = {"0", "7", "123", "null"}) @ValueSource(strings = {"0", "7", "123", "null"})
void fallbackTriggers(String input) { void fallbackTriggers(String input) {
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class).orElseThrow(); Integer result = intEntry.call(read(new HjsonReader(new HjsonLexer(new StringReader(input)))));
Integer result = assertDoesNotThrow(() -> readWriteExtension.read(
new HjsonReader(new HjsonLexer(new StringReader(input))),
intEntry,
readWriteExtension.createReadWriteContextExtensionsData()
));
assertEquals(3, result); assertEquals(3, result);
} }
@Test @Test
void description() { void description() {
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class).orElseThrow();
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
assertDoesNotThrow(() -> readWriteExtension.write( intEntry.apply(write(
new HjsonWriter(stringWriter, new HjsonWriter.Options().multilineCommentType(HjsonCommentType.SLASHES)), new HjsonWriter(stringWriter, new HjsonWriter.Options().multilineCommentType(HjsonCommentType.SLASHES)),
5, 5
intEntry,
readWriteExtension.createReadWriteContextExtensionsData()
)); ));
assertEquals( assertEquals(

View File

@@ -1,8 +1,3 @@
plugins { plugins {
id("de.siphalor.tweed5.base-module") id("de.siphalor.tweed5.base-module")
} }
dependencies {
implementation(libs.asm.core)
implementation(libs.asm.commons)
}

View File

@@ -0,0 +1,7 @@
package de.siphalor.tweed5.patchwork.api;
public class InvalidPatchworkAccessException extends RuntimeException {
public InvalidPatchworkAccessException(String message) {
super(message);
}
}

View File

@@ -1,8 +1,16 @@
package de.siphalor.tweed5.patchwork.api; package de.siphalor.tweed5.patchwork.api;
public interface Patchwork<S extends Patchwork<S>> { import org.jetbrains.annotations.CheckReturnValue;
boolean isPatchworkPartDefined(Class<?> patchworkInterface); import org.jetbrains.annotations.Contract;
boolean isPatchworkPartSet(Class<?> patchworkInterface); import org.jspecify.annotations.Nullable;
S copy(); public interface Patchwork {
@Contract(pure = true)
<T extends @Nullable Object> @Nullable T get(PatchworkPartAccess<T> access) throws InvalidPatchworkAccessException;
@Contract(mutates = "this")
<T extends @Nullable Object> void set(PatchworkPartAccess<T> access, T value) throws InvalidPatchworkAccessException;
@CheckReturnValue
@Contract(pure = true)
Patchwork copy();
} }

View File

@@ -1,90 +0,0 @@
package de.siphalor.tweed5.patchwork.api;
import de.siphalor.tweed5.patchwork.impl.ByteArrayClassLoader;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.Value;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Value
public class PatchworkClassCreator<P extends Patchwork<P>> {
Class<P> patchworkInterface;
PatchworkClassGenerator.Config generatorConfig;
public static <P extends Patchwork<P>> Builder<P> builder() {
return new Builder<>();
}
public PatchworkClass<P> createClass(Collection<Class<?>> partInterfaces) throws
PatchworkClassGenerator.GenerationException {
List<PatchworkClassPart> parts = partInterfaces.stream()
.map(PatchworkClassPart::new)
.collect(Collectors.toList());
PatchworkClassGenerator generator = new PatchworkClassGenerator(generatorConfig, parts);
try {
generator.verify();
} catch (PatchworkClassGenerator.VerificationException e) {
throw new IllegalArgumentException(e);
}
generator.generate();
byte[] classBytes = generator.emit();
//noinspection unchecked
Class<P> patchworkClass = (Class<P>) ByteArrayClassLoader.loadClass(null, classBytes);
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
for (PatchworkClassPart part : parts) {
try {
MethodHandle setterHandle = lookup.findSetter(patchworkClass, part.fieldName(), part.partInterface());
part.fieldSetter(setterHandle);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException(
"Failed to access setter for patchwork part " + part.partInterface().getName(),
e
);
}
}
try {
MethodHandle constructorHandle = lookup.findConstructor(patchworkClass, MethodType.methodType(Void.TYPE));
return PatchworkClass.<P>builder()
.classPackage(generatorConfig.classPackage())
.className(generator.className())
.theClass(patchworkClass)
.constructor(constructorHandle)
.parts(parts)
.build();
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new IllegalStateException("Failed to access constructor of patchwork class", e);
}
}
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Builder<P extends Patchwork<P>> {
private Class<P> patchworkInterface;
private String classPackage;
private String classPrefix = "";
public PatchworkClassCreator<P> build() {
return new PatchworkClassCreator<>(
patchworkInterface,
new PatchworkClassGenerator.Config(classPackage)
.classPrefix(classPrefix)
.markerInterfaces(Collections.singletonList(patchworkInterface))
);
}
}
}

View File

@@ -0,0 +1,16 @@
package de.siphalor.tweed5.patchwork.api;
import de.siphalor.tweed5.patchwork.impl.PatchworkFactoryImpl;
public interface PatchworkFactory {
static Builder builder() {
return new PatchworkFactoryImpl.Builder();
}
Patchwork create();
interface Builder {
<T> PatchworkPartAccess<T> registerPart(Class<T> partClass);
PatchworkFactory build();
}
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.patchwork.api;
import org.jspecify.annotations.Nullable;
public interface PatchworkPartAccess<T extends @Nullable Object> {
}

View File

@@ -1,7 +0,0 @@
package de.siphalor.tweed5.patchwork.api;
public class PatchworkPartIsNullException extends RuntimeException {
public PatchworkPartIsNullException(String message) {
super(message);
}
}

View File

@@ -1,21 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import org.jspecify.annotations.Nullable;
public class ByteArrayClassLoader extends ClassLoader {
public static Class<?> loadClass(@Nullable String binaryClassName, byte[] byteCode) {
return new ByteArrayClassLoader(ByteArrayClassLoader.class.getClassLoader())
.createClass(binaryClassName, byteCode);
}
private ByteArrayClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> createClass(@Nullable String binaryClassName, byte[] byteCode) {
Class<?> clazz = defineClass(binaryClassName, byteCode, 0, byteCode.length);
resolveClass(clazz);
return clazz;
}
}

View File

@@ -1,18 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Builder;
import lombok.Value;
import java.lang.invoke.MethodHandle;
import java.util.Collection;
@Value
@Builder
public class PatchworkClass<P extends Patchwork<P>> {
String classPackage;
String className;
Class<P> theClass;
MethodHandle constructor;
Collection<PatchworkClassPart> parts;
}

View File

@@ -1,653 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
import de.siphalor.tweed5.patchwork.impl.util.StreamUtils;
import lombok.*;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public class PatchworkClassGenerator {
/**
* Class version to use (Java 8)
*/
private static final int CLASS_VERSION = Opcodes.V1_8;
private static final String INNER_EQUALS_METHOD_NAME = "patchwork$innerEquals";
private static String generateUniqueIdentifier() {
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", "");
}
private final Config config;
private final Collection<PatchworkClassPart> parts;
private final String className;
@Getter(AccessLevel.NONE)
private final ClassWriter classWriter;
public PatchworkClassGenerator(Config config, Collection<PatchworkClassPart> parts) {
this.config = config;
this.parts = parts;
className = config.classPrefix() + generateUniqueIdentifier();
classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
}
public String internalClassName() {
return config.classPackage().replace('.', '/') + "/" + className();
}
public String binaryClassName() {
return config.classPackage() + "." + className();
}
public void verify() throws VerificationException {
for (PatchworkClassPart part : parts) {
verifyClass(part.partInterface());
}
verifyPartMethods();
}
private void verifyClass(Class<?> partClass) throws InvalidPatchworkPartClassException {
if (!partClass.isInterface()) {
throw new InvalidPatchworkPartClassException(partClass, "Must be an interface");
}
if ((partClass.getModifiers() & Modifier.PUBLIC) == 0) {
throw new InvalidPatchworkPartClassException(partClass, "Interface must be public");
}
}
private void verifyPartMethods() throws DuplicateMethodsException {
Map<MethodDescriptor, Collection<Method>> methodsBySignature = new HashMap<>();
for (PatchworkClassPart patchworkPart : parts) {
for (Method method : patchworkPart.partInterface().getMethods()) {
MethodDescriptor signature = new MethodDescriptor(method.getName(), method.getParameterTypes());
methodsBySignature
.computeIfAbsent(signature, s -> new ArrayList<>())
.add(method);
}
}
List<Method> duplicateMethods = methodsBySignature.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toList());
if (!duplicateMethods.isEmpty()) {
throw new DuplicateMethodsException(duplicateMethods);
}
}
public void generate() throws GenerationException {
beginClass();
generateSimpleConstructor();
for (PatchworkClassPart extensionClass : parts) {
addPart(extensionClass);
}
appendPojoMethods();
appendDefaultPatchworkMethods();
classWriter.visitEnd();
}
public byte[] emit() {
return classWriter.toByteArray();
}
private void beginClass() {
String[] interfaces = StreamUtils.concat(
Stream.of(Type.getInternalName(Patchwork.class)),
config.markerInterfaces().stream().map(Type::getInternalName),
parts.stream().map(ext -> Type.getInternalName(ext.partInterface()))
).toArray(String[]::new);
classWriter.visit(
CLASS_VERSION,
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL,
internalClassName(),
null,
Type.getInternalName(Object.class),
interfaces
);
}
private void generateSimpleConstructor() {
MethodVisitor methodWriter = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodWriter.visitInsn(Opcodes.RETURN);
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
private void appendPojoMethods() {
appendEqualsMethod();
appendHashCodeMethod();
appendToStringMethod();
}
// <editor-fold desc="POJO Methods">
private void appendEqualsMethod() {
appendInnerEqualsMethod();
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"equals",
"(Ljava/lang/Object;)Z",
null,
null
);
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label falseLabel = methodWriter.newLabel();
Label continueLabel = methodWriter.newLabel();
methodWriter.loadArg(0);
methodWriter.loadThis();
methodWriter.visitJumpInsn(Opcodes.IF_ACMPNE, continueLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(continueLabel);
methodWriter.loadArg(0);
methodWriter.visitTypeInsn(Opcodes.INSTANCEOF, internalClassName());
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.loadArg(0);
methodWriter.visitTypeInsn(Opcodes.CHECKCAST, internalClassName());
methodWriter.loadThis();
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
internalClassName(),
INNER_EQUALS_METHOD_NAME,
"(L" + internalClassName() + ";)Z",
false
);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendInnerEqualsMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PRIVATE,
INNER_EQUALS_METHOD_NAME,
"(L" + internalClassName() + ";)Z",
null,
null
);
methodWriter.visitParameter("other", Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label falseLabel = methodWriter.newLabel();
for (PatchworkClassPart part : parts) {
methodWriter.loadArg(0);
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
Type.getInternalName(Objects.class),
"equals",
"(Ljava/lang/Object;Ljava/lang/Object;)Z",
false
);
methodWriter.visitJumpInsn(Opcodes.IFEQ, falseLabel);
}
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendHashCodeMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"hashCode",
"()I",
null,
null
);
methodWriter.visitCode();
methodWriter.push(parts.size());
methodWriter.newArray(Type.getType(Object.class));
int i = 0;
for (PatchworkClassPart part : parts) {
methodWriter.dup();
methodWriter.push(i);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.visitInsn(Opcodes.AASTORE);
i++;
}
methodWriter.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/util/Objects",
"hash",
"([Ljava/lang/Object;)I",
false
);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendToStringMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"toString",
"()Ljava/lang/String;",
null,
null
);
methodWriter.visitCode();
String stringBuilderType = Type.getInternalName(StringBuilder.class);
methodWriter.visitTypeInsn(Opcodes.NEW, stringBuilderType);
methodWriter.dup();
methodWriter.push(className().length() + 10 + parts.size() * 64);
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
stringBuilderType,
"<init>",
"(I)V",
false
);
StringBuilder constantConcat = new StringBuilder();
constantConcat.append(className()).append("{\n");
for (PatchworkClassPart part : parts) {
constantConcat.append("\t").append(part.partInterface().getSimpleName()).append(": ");
methodWriter.push(constantConcat.toString());
constantConcat.setLength(0);
visitStringBuilderAppendString(methodWriter);
Label nullLabel = methodWriter.newLabel();
Label continueLabel = methodWriter.newLabel();
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.dup();
methodWriter.visitJumpInsn(Opcodes.IFNULL, nullLabel);
visitToString(methodWriter);
methodWriter.visitJumpInsn(Opcodes.GOTO, continueLabel);
methodWriter.visitLabel(nullLabel);
methodWriter.pop();
methodWriter.push("<unset>");
methodWriter.visitLabel(continueLabel);
visitStringBuilderAppendString(methodWriter);
constantConcat.append(",\n");
}
constantConcat.append("}");
methodWriter.push(constantConcat.toString());
visitStringBuilderAppendString(methodWriter);
visitToString(methodWriter);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
// </editor-fold>
private void appendDefaultPatchworkMethods() {
appendCopyMethod();
appendIsPatchworkPartDefinedMethod();
appendIsPatchworkPartSetMethod();
}
// <editor-fold desc="Patchwork Methods">
private void appendCopyMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"copy",
"()L" + Type.getInternalName(Patchwork.class) + ";",
null,
null
);
methodWriter.visitCode();
methodWriter.visitTypeInsn(Opcodes.NEW, internalClassName());
methodWriter.dup();
methodWriter.visitMethodInsn(
Opcodes.INVOKESPECIAL,
internalClassName(),
"<init>",
"()V",
false
);
for (PatchworkClassPart part : parts) {
methodWriter.dup();
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
visitFieldInsn(methodWriter, part, Opcodes.PUTFIELD);
}
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendIsPatchworkPartDefinedMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"isPatchworkPartDefined",
"(Ljava/lang/Class;)Z",
null,
null
);
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label trueLabel = methodWriter.newLabel();
for (PatchworkClassPart part : parts) {
methodWriter.loadArg(0);
methodWriter.push(Type.getType(part.partInterface()));
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, trueLabel);
}
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitLabel(trueLabel);
methodWriter.push(true);
methodWriter.returnValue();
methodWriter.visitMaxs(0, 0);
methodWriter.visitEnd();
}
private void appendIsPatchworkPartSetMethod() {
GeneratorAdapter methodWriter = createMethod(
Opcodes.ACC_PUBLIC,
"isPatchworkPartSet",
"(Ljava/lang/Class;)Z",
null,
null
);
methodWriter.visitParameter(null, Opcodes.ACC_FINAL);
methodWriter.visitCode();
Label[] labels = new Label[parts.size()];
int i = 0;
for (PatchworkClassPart part : parts) {
labels[i] = methodWriter.newLabel();
methodWriter.loadArg(0);
methodWriter.push(Type.getType(part.partInterface()));
methodWriter.ifCmp(Type.getType(Object.class), GeneratorAdapter.EQ, labels[i]);
i++;
}
methodWriter.push(false);
methodWriter.returnValue();
Label falseLabel = methodWriter.newLabel();
i = 0;
for (PatchworkClassPart part : parts) {
methodWriter.visitLabel(labels[i]);
methodWriter.loadThis();
visitFieldInsn(methodWriter, part, Opcodes.GETFIELD);
methodWriter.push((String) null);
methodWriter.ifCmp(Type.getType(part.partInterface()), GeneratorAdapter.EQ, falseLabel);
methodWriter.push(true);
methodWriter.returnValue();
i++;
}
methodWriter.visitLabel(falseLabel);
methodWriter.push(false);
methodWriter.returnValue();
methodWriter.visitMaxs(1, 1);
methodWriter.visitEnd();
}
public void addPart(PatchworkClassPart patchworkPart) throws GenerationException {
patchworkPart.fieldName("f_" + generateUniqueIdentifier());
classWriter.visitField(
Opcodes.ACC_PUBLIC,
patchworkPart.fieldName(),
patchworkPart.partInterface().descriptorString(),
null,
null
);
appendPartMethods(patchworkPart);
}
private void appendPartMethods(PatchworkClassPart patchworkPart) throws GenerationException {
try {
ClassReader classReader = new ClassReader(patchworkPart.partInterface().getName());
classReader.accept(new PartClassVisitor(patchworkPart), ClassReader.SKIP_FRAMES);
} catch (IOException e) {
throw new GenerationException("Failed to read interface class file", e);
}
}
// </editor-fold>
private GeneratorAdapter createMethod(
int access,
String name,
String desc,
@Nullable String signature,
String @Nullable [] exceptions
) {
MethodVisitor methodVisitor = classWriter.visitMethod(access, name, desc, signature, exceptions);
return new GeneratorAdapter(methodVisitor, access, name, desc);
}
private void visitFieldInsn(MethodVisitor methodWriter, PatchworkClassPart part, int opcode) {
methodWriter.visitFieldInsn(
opcode,
internalClassName(),
part.fieldName(),
Type.getDescriptor(part.partInterface())
);
}
private static void visitToString(MethodVisitor methodWriter) {
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
Type.getInternalName(Object.class),
"toString",
"()Ljava/lang/String;",
false
);
}
private static void visitStringBuilderAppendString(MethodVisitor methodWriter) {
String stringBuilderType = Type.getInternalName(StringBuilder.class);
methodWriter.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
stringBuilderType,
"append",
"(Ljava/lang/String;)L" + stringBuilderType + ";",
false
);
}
private class PartClassVisitor extends ClassVisitor {
private final PatchworkClassPart extensionClass;
protected PartClassVisitor(PatchworkClassPart extensionClass) {
super(Opcodes.ASM9);
this.extensionClass = extensionClass;
}
@Override
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions
) {
GeneratorAdapter methodWriter = createMethod(Opcodes.ACC_PUBLIC, name, descriptor, signature, exceptions);
return new PartMethodVisitor(api, methodWriter, descriptor, extensionClass);
}
}
private class PartMethodVisitor extends MethodVisitor {
private final GeneratorAdapter methodWriter;
private final String methodDescriptor;
private final PatchworkClassPart patchworkPart;
protected PartMethodVisitor(
int api,
GeneratorAdapter methodWriter,
String methodDescriptor,
PatchworkClassPart patchworkPart
) {
super(api);
this.methodWriter = methodWriter;
this.patchworkPart = patchworkPart;
this.methodDescriptor = methodDescriptor;
}
@Override
public void visitParameter(String name, int access) {
methodWriter.visitParameter(name, access);
}
@Override
public void visitEnd() {
Label nullLabel = methodWriter.newLabel();
methodWriter.visitCode();
methodWriter.visitVarInsn(Opcodes.ALOAD, 0);
methodWriter.visitFieldInsn(
Opcodes.GETFIELD,
internalClassName(),
patchworkPart.fieldName(),
Type.getDescriptor(patchworkPart.partInterface())
);
methodWriter.dup();
methodWriter.ifNull(nullLabel);
methodWriter.loadArgs();
methodWriter.visitMethodInsn(
Opcodes.INVOKEINTERFACE,
Type.getInternalName(patchworkPart.partInterface()),
methodWriter.getName(),
methodDescriptor,
true
);
methodWriter.returnValue();
methodWriter.visitLabel(nullLabel);
methodWriter.pop();
methodWriter.throwException(
Type.getType(PatchworkPartIsNullException.class),
"The patchwork part " + patchworkPart.partInterface().getSimpleName() + " has not been set"
);
methodWriter.visitMaxs(-1, -1);
methodWriter.visitEnd();
}
}
@Value
private static class MethodDescriptor {
String name;
Class<?>[] parameterTypes;
}
@Data
public static class Config {
@lombok.NonNull
private @NonNull String classPackage;
private String classPrefix = "";
private Collection<Class<?>> markerInterfaces = Collections.emptyList();
}
public static class VerificationException extends Exception {
private VerificationException(String message) {
super(message);
}
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class InvalidPatchworkPartClassException extends VerificationException {
Class<?> partClass;
public InvalidPatchworkPartClassException(Class<?> partClass, String message) {
super("Invalid patchwork part class " + partClass.getName() + ": " + message);
this.partClass = partClass;
}
@Override
public String toString() {
return super.toString();
}
}
@Value
@EqualsAndHashCode(callSuper = true)
public static class DuplicateMethodsException extends VerificationException {
transient Collection<Method> signatures;
private DuplicateMethodsException(Collection<Method> methods) {
super("Duplicate method signatures:\n" + methods.stream()
.map(DuplicateMethodsException::getMethodMessage)
.collect(Collectors.joining("\n")));
this.signatures = methods;
}
private static String getMethodMessage(Method method) {
StringBuilder stringBuilder = new StringBuilder("\t- "
+ method.getDeclaringClass().getCanonicalName()
+ "#(");
for (Class<?> parameterType : method.getParameterTypes()) {
stringBuilder.append(parameterType.getCanonicalName());
stringBuilder.append(", ");
}
stringBuilder.append(")");
stringBuilder.append(method.getReturnType().getCanonicalName());
stringBuilder.append("\n");
return stringBuilder.toString();
}
@Override
public String toString() {
return super.toString();
}
}
public static class GenerationException extends Exception {
public GenerationException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -1,12 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import lombok.Data;
import java.lang.invoke.MethodHandle;
@Data
public class PatchworkClassPart {
private final Class<?> partInterface;
private String fieldName;
private MethodHandle fieldSetter;
}

View File

@@ -0,0 +1,100 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.InvalidPatchworkAccessException;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class PatchworkFactoryImpl implements PatchworkFactory {
private final UUID factoryUuid;
private final Class<?>[] partClasses;
@Override
public Patchwork create() {
return new PatchworkImpl(new Object[partClasses.length]);
}
public static class Builder implements PatchworkFactory.Builder {
private final UUID factoryUuid = UUID.randomUUID();
private final List<Class<?>> partClasses = new ArrayList<>();
private boolean built;
@Override
public <T> PatchworkPartAccess<T> registerPart(Class<T> partClass) {
requireFresh();
partClasses.add(partClass);
return new PartAccessImpl<>(factoryUuid, partClasses.size() - 1);
}
@Override
public PatchworkFactory build() {
requireFresh();
built = true;
return new PatchworkFactoryImpl(factoryUuid, partClasses.toArray(new Class<?>[0]));
}
private void requireFresh() {
if (built) {
throw new IllegalStateException("Builder has already been used.");
}
}
}
@RequiredArgsConstructor
private static class PartAccessImpl<T extends @Nullable Object> implements PatchworkPartAccess<T> {
private final UUID factoryUuid;
private final int partIndex;
}
@RequiredArgsConstructor
@EqualsAndHashCode
private class PatchworkImpl implements Patchwork {
private final @Nullable Object[] partValues;
@Override
public <T extends @Nullable Object> T get(PatchworkPartAccess<T> access) throws InvalidPatchworkAccessException {
PartAccessImpl<T> castedAccess = validatePartAccess(access);
//noinspection unchecked
return (T) partValues[castedAccess.partIndex];
}
@Override
public <T extends @Nullable Object> void set(PatchworkPartAccess<T> access, T value) throws InvalidPatchworkAccessException {
PartAccessImpl<T> castedAccess = validatePartAccess(access);
if (value != null && !partClasses[castedAccess.partIndex].isInstance(value)) {
throw new IllegalArgumentException(
"value " + value + " of type " + value.getClass().getName() +
" doesn't match registered value class " + partClasses[castedAccess.partIndex].getName()
);
}
partValues[castedAccess.partIndex] = value;
}
private <T extends @Nullable Object> PartAccessImpl<T> validatePartAccess(PatchworkPartAccess<T> access)
throws InvalidPatchworkAccessException {
if (!(access instanceof PartAccessImpl<?>)) {
throw new InvalidPatchworkAccessException("Part access is of incorrect class.");
} else if (((PartAccessImpl<?>) access).factoryUuid != factoryUuid) {
throw new InvalidPatchworkAccessException("Part access does not belong to the same patchwork factory.");
}
return (PartAccessImpl<T>) access;
}
@Override
public Patchwork copy() {
return new PatchworkImpl(Arrays.copyOf(partValues, partValues.length));
}
}
}

View File

@@ -1,14 +0,0 @@
package de.siphalor.tweed5.patchwork.impl.util;
import java.util.stream.Stream;
public class StreamUtils {
@SafeVarargs
public static <T> Stream<T> concat(Stream<T>... streams) {
Stream<T> result = Stream.empty();
for (Stream<T> stream : streams) {
result = Stream.concat(result, stream);
}
return result;
}
}

View File

@@ -1,269 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartIsNullException;
import lombok.Data;
import lombok.experimental.Accessors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
class PatchworkClassGeneratorGeneratedClassTest {
PatchworkClassPart partA;
PatchworkClassPart partB;
byte[] bytes;
Class<Patchwork<?>> patchworkClass;
Patchwork<?> patchwork;
@BeforeEach
void setUp() {
partA = new PatchworkClassPart(ExtensionA.class);
partB = new PatchworkClassPart(ExtensionB.class);
PatchworkClassGenerator generator = new PatchworkClassGenerator(
new PatchworkClassGenerator.Config("de.siphalor.tweed5.core.test")
.classPrefix("FullTest$")
.markerInterfaces(Collections.singletonList(MarkerInterface.class)),
Arrays.asList(partA, partB)
);
assertDoesNotThrow(generator::verify);
assertDoesNotThrow(generator::generate);
bytes = generator.emit();
//noinspection unchecked
patchworkClass = (Class<Patchwork<?>>) assertDoesNotThrow(() -> ByteArrayClassLoader.loadClass(null, bytes));
patchwork = createPatchworkInstance();
}
@Test
@Disabled("Dumping the class is only for testing purposes")
void dumpClass() {
try {
Path target = File.createTempFile("tweed5-patchwork", ".class").toPath();
try (OutputStream os = Files.newOutputStream(target)) {
os.write(bytes);
}
System.out.println("Dumped generated class to " + target);
} catch (IOException e) {
assertNull(e, "Must not throw exception");
}
}
@Test
void packageName() {
assertEquals("de.siphalor.tweed5.core.test", patchworkClass.getPackage().getName());
}
@Test
void className() {
assertTrue(patchworkClass.getSimpleName().startsWith("FullTest$"), "Generated class name must start with prefix FullTest$, got " + patchworkClass.getSimpleName());
}
@Test
void implementsInterfaces() {
assertImplements(MarkerInterface.class, patchworkClass);
assertImplements(Patchwork.class, patchworkClass);
assertImplements(ExtensionA.class, patchworkClass);
assertImplements(ExtensionB.class, patchworkClass);
}
@Test
void toStringMethod() {
int defaultHashCode = System.identityHashCode(patchwork);
String stringResult = patchwork.toString();
assertFalse(stringResult.contains(Integer.toHexString(defaultHashCode)), "Expected toString not to be the default toString, got: " + stringResult);
}
@Test
void toStringContent() {
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
((ExtensionA) patchwork).setText("Hello World!");
String stringResult = patchwork.toString();
assertTrue(stringResult.contains("Hello World!"), "Expected toString to contain the toString of its extensions, got: " + stringResult);
}
@Test
void hashCodeMethod() {
int emptyHashCode = patchwork.hashCode();
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
int hashCodeWithA = patchwork.hashCode();
((ExtensionA) patchwork).setText("Hello World!");
int hashCodeWithAContent = patchwork.hashCode();
assertNotEquals(emptyHashCode, hashCodeWithA, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithA);
assertNotEquals(emptyHashCode, hashCodeWithAContent, "Expected hashCode to be different, got: " + emptyHashCode + " and " + hashCodeWithAContent);
assertNotEquals(hashCodeWithA, hashCodeWithAContent, "Expected hashCode to be different, got: " + hashCodeWithA + " and " + hashCodeWithAContent);
}
@SuppressWarnings("SimplifiableAssertion")
@Test
void equalsMethod() {
//noinspection ConstantValue,SimplifiableAssertion
assertFalse(patchwork.equals(null));
//noinspection EqualsWithItself
assertTrue(patchwork.equals(patchwork));
Patchwork<?> other = createPatchworkInstance();
assertTrue(patchwork.equals(other));
assertTrue(other.equals(patchwork));
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
assertFalse(patchwork.equals(other));
setFieldValue(other, partA.fieldName(), new ExtensionAImpl());
assertTrue(patchwork.equals(other));
assertTrue(other.equals(patchwork));
((ExtensionA) patchwork).setText("Hello 1!");
((ExtensionA) other).setText("Hello 2!");
assertFalse(patchwork.equals(other));
}
@Test
void copy() {
ExtensionAImpl aImpl = new ExtensionAImpl();
setFieldValue(patchwork, partA.fieldName(), aImpl);
Patchwork<?> copy = patchwork.copy();
Object aValue = getFieldValue(copy, partA.fieldName());
Object bValue = getFieldValue(copy, partB.fieldName());
assertSame(aImpl, aValue);
assertNull(bValue);
}
@Test
void isPartDefined() {
assertPartDefined(ExtensionA.class, patchwork);
assertPartDefined(ExtensionB.class, patchwork);
assertPartUndefined(ExtensionC.class, patchwork);
}
static void assertPartDefined(Class<?> anInterface, Patchwork<?> patchwork) {
assertTrue(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must be defined");
}
static void assertPartUndefined(Class<?> anInterface, Patchwork<?> patchwork) {
assertFalse(patchwork.isPatchworkPartDefined(anInterface), "Patchwork part " + anInterface.getName() + " must not be defined");
}
@Test
void checkFieldsExist() {
assertFieldExists(partA.fieldName(), ExtensionA.class);
assertFieldExists(partB.fieldName(), ExtensionB.class);
}
void assertFieldExists(String fieldName, Class<?> fieldType) {
Field field = getField(fieldName);
assertEquals(fieldType, field.getType());
}
@Test
void isPartSet() {
assertPartUnset(ExtensionA.class, patchwork);
assertPartUnset(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
assertPartSet(ExtensionA.class, patchwork);
assertPartUnset(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
assertPartSet(ExtensionA.class, patchwork);
assertPartSet(ExtensionB.class, patchwork);
assertPartUnset(ExtensionC.class, patchwork);
}
@Test
void inheritedMethodCalls() {
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionA) patchwork).getText());
assertThrows(PatchworkPartIsNullException.class, () -> ((ExtensionB) patchwork).multiply(1, 2));
setFieldValue(patchwork, partA.fieldName(), new ExtensionAImpl());
setFieldValue(patchwork, partB.fieldName(), new ExtensionBImpl());
assertEquals(6, assertDoesNotThrow(() -> ((ExtensionB) patchwork).multiply(2, 3)), "Method of extension b must be working");
assertDoesNotThrow(() -> ((ExtensionA) patchwork).setText("something"));
assertEquals("something", ((ExtensionA) patchwork).getText());
}
Patchwork<?> createPatchworkInstance() {
Constructor<?> constructor = assertDoesNotThrow(() -> patchworkClass.getConstructor(), "The generated class' constructor must be accessible");
return assertDoesNotThrow(() -> (Patchwork<?>) constructor.newInstance());
}
Object getFieldValue(Patchwork<?> patchwork, String fieldName) {
return assertDoesNotThrow(() -> getField(fieldName).get(patchwork));
}
void setFieldValue(Patchwork<?> patchwork, String fieldName, Object value) {
assertDoesNotThrow(() -> getField(fieldName).set(patchwork, value), "Field " + fieldName + " must be accessible");
}
Field getField(String fieldName) {
return assertDoesNotThrow(() -> patchworkClass.getField(fieldName), "Field " + fieldName + " must exist and be public");
}
static void assertPartSet(Class<?> anInterface, Patchwork<?> patchwork) {
assertTrue(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must be set");
}
static void assertPartUnset(Class<?> anInterface, Patchwork<?> patchwork) {
assertFalse(patchwork.isPatchworkPartSet(anInterface), "Patchwork part " + anInterface.getName() + " must not be set");
}
static void assertImplements(Class<?> anInterface, Class<?> theClass) {
assertTrue(anInterface.isAssignableFrom(theClass), "Class " + theClass.getName() + " must implement " + anInterface.getName());
}
public interface MarkerInterface {}
public interface ExtensionA {
String getText();
void setText(String text);
}
@Data
@Accessors(fluent = false)
static class ExtensionAImpl implements ExtensionA {
private String text;
}
public interface ExtensionB {
int multiply(int a, int b);
}
static class ExtensionBImpl implements ExtensionB {
public int multiply(int a, int b) {
return a * b;
}
}
public interface ExtensionC {
void test();
}
}

View File

@@ -1,52 +0,0 @@
package de.siphalor.tweed5.patchwork.impl;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PatchworkClassGeneratorTest {
@Test
void notAnInterface() {
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NotAnInterface.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
@Test
void nonPublicInterface() {
PatchworkClassGenerator generator = createGenerator(Collections.singletonList(NonPublicInterface.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
@Test
void duplicateFields() {
PatchworkClassGenerator generator = createGenerator(Arrays.asList(DuplicateA.class, DuplicateB.class));
assertThrows(PatchworkClassGenerator.VerificationException.class, generator::verify);
}
PatchworkClassGenerator createGenerator(Collection<Class<?>> partClasses) {
return new PatchworkClassGenerator(
new PatchworkClassGenerator.Config("de.siphalor.tweed5.patchwork.test.generated"),
partClasses.stream().map(PatchworkClassPart::new).collect(Collectors.toList())
);
}
public static class NotAnInterface {
}
interface NonPublicInterface {
}
public interface DuplicateA {
void test();
}
public interface DuplicateB {
void test();
}
}

View File

@@ -0,0 +1,94 @@
package de.siphalor.tweed5.patchwork.impl;
import de.siphalor.tweed5.patchwork.api.InvalidPatchworkAccessException;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class PatchworkFactoryImplTest {
@Test
void test() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
PatchworkPartAccess<String> stringAccess1 = builder.registerPart(String.class);
PatchworkPartAccess<@Nullable Number> integerAccess1 = builder.registerPart(Number.class);
PatchworkPartAccess<String> stringAccess2 = builder.registerPart(String.class);
PatchworkFactory factory = builder.build();
Patchwork patchwork = factory.create();
assertThat(patchwork.get(stringAccess1)).isNull();
assertThat(patchwork.get(integerAccess1)).isNull();
assertThat(patchwork.get(stringAccess2)).isNull();
patchwork.set(stringAccess1, "Hello");
patchwork.set(stringAccess2, "World");
patchwork.set(integerAccess1, 123);
assertThat(patchwork.get(stringAccess1)).isEqualTo("Hello");
assertThat(patchwork.get(integerAccess1)).isEqualTo(123);
assertThat(patchwork.get(stringAccess2)).isEqualTo("World");
patchwork.set(integerAccess1, null);
assertThat(patchwork.get(integerAccess1)).isNull();
}
@Test
void copy() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
PatchworkPartAccess<String> stringAccess = builder.registerPart(String.class);
PatchworkFactory factory = builder.build();
Patchwork patchwork = factory.create();
patchwork.set(stringAccess, "Hello");
Patchwork copy = patchwork.copy();
assertThat(copy.get(stringAccess)).isEqualTo("Hello");
assertThat(copy).isEqualTo(patchwork);
}
@Test
void lateRegister() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
builder.build();
assertThatThrownBy(() -> builder.registerPart(String.class)).isInstanceOf(IllegalStateException.class);
}
@Test
void doubleBuild() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
builder.build();
assertThatThrownBy(builder::build).isInstanceOf(IllegalStateException.class);
}
@Test
void invalidAccess() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
builder.registerPart(String.class);
PatchworkFactory factory = builder.build();
Patchwork patchwork = factory.create();
PatchworkFactoryImpl.Builder otherBuilder = new PatchworkFactoryImpl.Builder();
PatchworkPartAccess<String> otherAccess = otherBuilder.registerPart(String.class);
otherBuilder.build();
assertThatThrownBy(() -> patchwork.get(otherAccess)).isInstanceOf(InvalidPatchworkAccessException.class);
assertThatThrownBy(() -> patchwork.set(otherAccess, "Hello")).isInstanceOf(InvalidPatchworkAccessException.class);
}
@Test
void setWrongType() {
PatchworkFactoryImpl.Builder builder = new PatchworkFactoryImpl.Builder();
//noinspection unchecked
PatchworkPartAccess<Object> access = ((PatchworkPartAccess<Object>)(Object) builder.registerPart(String.class));
PatchworkFactory factory = builder.build();
Patchwork patchwork = factory.create();
assertThatThrownBy(() -> patchwork.set(access, 123)).isInstanceOf(IllegalArgumentException.class);
}
}

View File

@@ -1,6 +0,0 @@
package de.siphalor.tweed5.data.extension.api;
public interface EntryReaderWriterDefinition {
TweedEntryReader<?, ?> reader();
TweedEntryWriter<?, ?> writer();
}

View File

@@ -1,6 +0,0 @@
package de.siphalor.tweed5.data.extension.api;
public interface ReadWriteEntryDataExtension {
TweedEntryReader<?, ?> entryReaderChain();
TweedEntryWriter<?, ?> entryWriterChain();
}

View File

@@ -2,29 +2,120 @@ 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.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.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.impl.ReadWriteExtensionImpl; import de.siphalor.tweed5.data.extension.impl.ReadWriteExtensionImpl;
import de.siphalor.tweed5.dataapi.api.TweedDataReader; import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
import java.util.function.Function;
public interface ReadWriteExtension extends TweedExtension { public interface ReadWriteExtension extends TweedExtension {
Class<? extends ReadWriteExtension> DEFAULT = ReadWriteExtensionImpl.class; Class<? extends ReadWriteExtension> DEFAULT = ReadWriteExtensionImpl.class;
void setEntryReaderWriterDefinition(ConfigEntry<?> entry, EntryReaderWriterDefinition readerWriterDefinition); static <T> Consumer<ConfigEntry<T>> entryReaderWriter(
TweedEntryReaderWriter<T, ? extends ConfigEntry<T>> entryReaderWriter
) {
return entryReaderWriter(entryReaderWriter, entryReaderWriter);
}
ReadWriteContextExtensionsData createReadWriteContextExtensionsData(); static <T> Consumer<ConfigEntry<T>> entryReaderWriter(
TweedEntryReader<T, ? extends ConfigEntry<T>> entryReader,
TweedEntryWriter<T, ? extends ConfigEntry<T>> entryWriter
) {
return entry -> {
ReadWriteExtension extension = entry.container().extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("No ReadWriteExtension present"));
extension.setEntryReader(entry, entryReader);
extension.setEntryWriter(entry, entryWriter);
};
}
<T extends @Nullable Object> T read( static <T> Consumer<ConfigEntry<T>> entryReader(TweedEntryReader<T, ? extends ConfigEntry<T>> entryReader) {
return entry -> {
ReadWriteExtension extension = entry.container().extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("No ReadWriteExtension present"));
extension.setEntryReader(entry, entryReader);
};
}
static <T> Consumer<ConfigEntry<T>> entryWriter(TweedEntryWriter<T, ? extends ConfigEntry<T>> entryWriter) {
return entry -> {
ReadWriteExtension extension = entry.container().extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("No ReadWriteExtension present"));
extension.setEntryWriter(entry, entryWriter);
};
}
static <T extends @Nullable Object> Function<ConfigEntry<T>, T> read(TweedDataReader reader) {
return read(reader, null);
}
static <T extends @Nullable Object> Function<ConfigEntry<T>, T> read(
TweedDataReader reader, TweedDataReader reader,
@Nullable Consumer<Patchwork> contextExtensionsDataCustomizer
) {
return entry -> {
try {
ReadWriteExtension extension = entry.container().extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("No ReadWriteExtension present"));
Patchwork contextExtensionsData = extension.createReadWriteContextExtensionsData();
if (contextExtensionsDataCustomizer != null) {
contextExtensionsDataCustomizer.accept(contextExtensionsData);
}
return extension.read(reader, entry, contextExtensionsData);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
static <T extends @Nullable Object> Consumer<ConfigEntry<T>> write(TweedDataVisitor writer, T value) {
return write(writer, value, null);
}
static <T extends @Nullable Object> Consumer<ConfigEntry<T>> write(
TweedDataVisitor writer,
T value,
@Nullable Consumer<Patchwork> contextExtensionsDataCustomizer
) {
return entry -> {
try {
ReadWriteExtension extension = entry.container().extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("No ReadWriteExtension present"));
Patchwork contextExtensionsData = extension.createReadWriteContextExtensionsData();
if (contextExtensionsDataCustomizer != null) {
contextExtensionsDataCustomizer.accept(contextExtensionsData);
}
extension.write(writer, value, entry, contextExtensionsData);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
<T, C extends ConfigEntry<T>> void setEntryReaderWriter(
ConfigEntry<T> entry, ConfigEntry<T> entry,
ReadWriteContextExtensionsData contextExtensionsData TweedEntryReader<T, C> entryReader,
) throws TweedEntryReadException; TweedEntryWriter<T, C> entryWriter
);
<T, C extends ConfigEntry<T>> void setEntryReader(ConfigEntry<T> entry, TweedEntryReader<T, C> entryReader);
<T, C extends ConfigEntry<T>> void setEntryWriter(ConfigEntry<T> entry, TweedEntryWriter<T, C> entryWriter);
Patchwork createReadWriteContextExtensionsData();
<T extends @Nullable Object> T read(TweedDataReader reader, ConfigEntry<T> entry, Patchwork contextExtensionsData)
throws TweedEntryReadException;
<T extends @Nullable Object> void write( <T extends @Nullable Object> void write(
TweedDataVisitor writer, TweedDataVisitor writer,
T value, T value,
ConfigEntry<T> entry, ConfigEntry<T> entry,
ReadWriteContextExtensionsData contextExtensionsData Patchwork contextExtensionsData
) throws TweedEntryWriteException; ) throws TweedEntryWriteException;
<T, C extends ConfigEntry<T>> TweedEntryReader<T, C> getReaderChain(C entry);
<T, C extends ConfigEntry<T>> TweedEntryWriter<T, C> getWriterChain(C entry);
} }

View File

@@ -1,7 +1,8 @@
package de.siphalor.tweed5.data.extension.api; package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData; import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface TweedReadContext { public interface TweedReadContext {
ReadWriteContextExtensionsData extensionsData(); ReadWriteExtension readWriteExtension();
Patchwork extensionsData();
} }

View File

@@ -1,7 +1,8 @@
package de.siphalor.tweed5.data.extension.api; package de.siphalor.tweed5.data.extension.api;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData; import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface TweedWriteContext { public interface TweedWriteContext {
ReadWriteContextExtensionsData extensionsData(); ReadWriteExtension readWriteExtension();
Patchwork extensionsData();
} }

View File

@@ -1,6 +0,0 @@
package de.siphalor.tweed5.data.extension.api.extension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface ReadWriteContextExtensionsData extends Patchwork<ReadWriteContextExtensionsData> {
}

View File

@@ -1,7 +1,7 @@
package de.siphalor.tweed5.data.extension.api.extension; package de.siphalor.tweed5.data.extension.api.extension;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
public interface ReadWriteExtensionSetupContext { public interface ReadWriteExtensionSetupContext {
<E> RegisteredExtensionData<ReadWriteContextExtensionsData, E> registerReadWriteContextExtensionData(Class<E> extensionDataClass); <E> PatchworkPartAccess<E> registerReadWriteContextExtensionData(Class<E> extensionDataClass);
} }

View File

@@ -3,5 +3,6 @@ package de.siphalor.tweed5.data.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.ConfigEntry; import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader; import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter; import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import org.jspecify.annotations.Nullable;
public interface TweedEntryReaderWriter<T, C extends ConfigEntry<T>> extends TweedEntryReader<T, C>, TweedEntryWriter<T, C> {} public interface TweedEntryReaderWriter<T extends @Nullable Object, C extends ConfigEntry<T>> extends TweedEntryReader<T, C>, TweedEntryWriter<T, C> {}

View File

@@ -3,56 +3,41 @@ package de.siphalor.tweed5.data.extension.impl;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
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.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.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext; import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer; import de.siphalor.tweed5.core.api.middleware.DefaultMiddlewareContainer;
import de.siphalor.tweed5.data.extension.api.*; 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.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension; 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.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedDataWriteException;
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.PatchworkFactory;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator; import lombok.Data;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import lombok.Setter;
import lombok.Value;
import lombok.val; import lombok.val;
import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@AutoService(ReadWriteExtension.class) @AutoService(ReadWriteExtension.class)
public class ReadWriteExtensionImpl implements ReadWriteExtension { public class ReadWriteExtensionImpl implements ReadWriteExtension {
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
private final RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> private final PatchworkPartAccess<CustomEntryData> customEntryDataAccess;
readerWriterDefinitionExtension;
private final RegisteredExtensionData<EntryExtensionsData, ReadWriteEntryDataExtension> readWriteEntryDataExtension;
private DefaultMiddlewareContainer<TweedEntryReader<?, ?>> private DefaultMiddlewareContainer<TweedEntryReader<?, ?>>
entryReaderMiddlewareContainer entryReaderMiddlewareContainer
= new DefaultMiddlewareContainer<>(); = new DefaultMiddlewareContainer<>();
private DefaultMiddlewareContainer<TweedEntryWriter<?, ?>> private DefaultMiddlewareContainer<TweedEntryWriter<?, ?>>
entryWriterMiddlewareContainer entryWriterMiddlewareContainer
= new DefaultMiddlewareContainer<>(); = new DefaultMiddlewareContainer<>();
private final Map<Class<?>, RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, ?>> private @Nullable PatchworkFactory readWriteContextPatchworkFactory;
readWriteContextExtensionsDataClasses
= new HashMap<>();
private @Nullable PatchworkClass<@NonNull ReadWriteContextExtensionsData> readWriteContextExtensionsDataPatchwork;
public ReadWriteExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) { public ReadWriteExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext context) {
this.configContainer = configContainer; this.configContainer = configContainer;
this.readerWriterDefinitionExtension = context.registerEntryExtensionData(EntryReaderWriterDefinition.class); this.customEntryDataAccess = context.registerEntryExtensionData(CustomEntryData.class);
this.readWriteEntryDataExtension = context.registerEntryExtensionData(ReadWriteEntryDataExtension.class);
} }
@Override @Override
@@ -64,23 +49,8 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
public void extensionsFinalized() { public void extensionsFinalized() {
Collection<TweedExtension> extensions = configContainer.extensions(); Collection<TweedExtension> extensions = configContainer.extensions();
ReadWriteExtensionSetupContext setupContext = new ReadWriteExtensionSetupContext() { PatchworkFactory.Builder readWriteContextPatchworkFactorBuilder = PatchworkFactory.builder();
@Override ReadWriteExtensionSetupContext setupContext = readWriteContextPatchworkFactorBuilder::registerPart;
public <E> RegisteredExtensionData<ReadWriteContextExtensionsData, E> registerReadWriteContextExtensionData(
Class<E> extensionDataClass
) {
if (readWriteContextExtensionsDataClasses.containsKey(extensionDataClass)) {
throw new IllegalArgumentException("Context extension "
+ extensionDataClass.getName()
+ " is already registered");
}
RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, E>
registeredExtensionData
= new RegisteredExtensionDataImpl<>();
readWriteContextExtensionsDataClasses.put(extensionDataClass, registeredExtensionData);
return registeredExtensionData;
}
};
entryReaderMiddlewareContainer = new DefaultMiddlewareContainer<>(); entryReaderMiddlewareContainer = new DefaultMiddlewareContainer<>();
entryWriterMiddlewareContainer = new DefaultMiddlewareContainer<>(); entryWriterMiddlewareContainer = new DefaultMiddlewareContainer<>();
@@ -102,79 +72,62 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
} }
} }
readWriteContextPatchworkFactory = readWriteContextPatchworkFactorBuilder.build();
entryReaderMiddlewareContainer.seal(); entryReaderMiddlewareContainer.seal();
entryWriterMiddlewareContainer.seal(); entryWriterMiddlewareContainer.seal();
}
val patchworkClassCreator = PatchworkClassCreator.<ReadWriteContextExtensionsData>builder() @Override
.patchworkInterface(ReadWriteContextExtensionsData.class) public <T, C extends ConfigEntry<T>> void setEntryReaderWriter(
.classPackage("de.siphalor.tweed5.data.extension.generated") ConfigEntry<T> entry,
.classPrefix("ReadWriteContextExtensionsData$") TweedEntryReader<T, C> entryReader,
.build(); TweedEntryWriter<T, C> entryWriter
) {
CustomEntryData customEntryData = getOrCreateCustomEntryData(entry);
customEntryData.readerDefinition(entryReader);
customEntryData.writerDefinition(entryWriter);
}
try { @Override
readWriteContextExtensionsDataPatchwork = public <T, C extends ConfigEntry<T>> void setEntryReader(ConfigEntry<T> entry, TweedEntryReader<T, C> entryReader) {
patchworkClassCreator.createClass(readWriteContextExtensionsDataClasses.keySet()); getOrCreateCustomEntryData(entry).readerDefinition(entryReader);
for (PatchworkClassPart patchworkClassPart : readWriteContextExtensionsDataPatchwork.parts()) { }
RegisteredExtensionDataImpl<ReadWriteContextExtensionsData, ?>
registeredExtension @Override
= readWriteContextExtensionsDataClasses.get(patchworkClassPart.partInterface()); public <T, C extends ConfigEntry<T>> void setEntryWriter(ConfigEntry<T> entry, TweedEntryWriter<T, C> entryWriter) {
registeredExtension.setter = patchworkClassPart.fieldSetter(); getOrCreateCustomEntryData(entry).writerDefinition(entryWriter);
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new IllegalStateException(
"Failed to generate read write context extensions' data patchwork class",
e
);
}
} }
@Override @Override
public void initEntry(ConfigEntry<?> configEntry) { public void initEntry(ConfigEntry<?> configEntry) {
TweedEntryReader<?, ?> baseReader; CustomEntryData customEntryData = getOrCreateCustomEntryData(configEntry);
TweedEntryWriter<?, ?> baseWriter; customEntryData.readerChain(entryReaderMiddlewareContainer.process(customEntryData.readerDefinition()));
if (configEntry.extensionsData().isPatchworkPartSet(EntryReaderWriterDefinition.class)) { customEntryData.writerChain(entryWriterMiddlewareContainer.process(customEntryData.writerDefinition()));
EntryReaderWriterDefinition rwDefintion = (EntryReaderWriterDefinition) configEntry.extensionsData(); }
baseReader = rwDefintion.reader();
baseWriter = rwDefintion.writer(); private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
} else { CustomEntryData entryData = entry.extensionsData().get(customEntryDataAccess);
baseReader = TweedEntryReaderWriterImpls.NOOP_READER_WRITER; if (entryData == null) {
baseWriter = TweedEntryReaderWriterImpls.NOOP_READER_WRITER; entryData = new CustomEntryData();
entry.extensionsData().set(customEntryDataAccess, entryData);
} }
return entryData;
readWriteEntryDataExtension.set(
configEntry.extensionsData(), new ReadWriteEntryDataExtensionImpl(
entryReaderMiddlewareContainer.process(baseReader),
entryWriterMiddlewareContainer.process(baseWriter)
)
);
} }
@Override @Override
public void setEntryReaderWriterDefinition( public Patchwork createReadWriteContextExtensionsData() {
@NonNull ConfigEntry<?> entry, assert readWriteContextPatchworkFactory != null;
@NonNull EntryReaderWriterDefinition readerWriterDefinition return readWriteContextPatchworkFactory.create();
) {
readerWriterDefinitionExtension.set(entry.extensionsData(), readerWriterDefinition);
} }
@Override
public ReadWriteContextExtensionsData createReadWriteContextExtensionsData() {
try {
assert readWriteContextExtensionsDataPatchwork != null;
return (ReadWriteContextExtensionsData) readWriteContextExtensionsDataPatchwork.constructor().invoke();
} catch (Throwable e) {
throw new IllegalStateException("Failed to instantiate read write context extensions' data", e);
}
}
@Override
public <T extends @Nullable Object> T read( public <T extends @Nullable Object> T read(
@NonNull TweedDataReader reader, TweedDataReader reader,
@NonNull ConfigEntry<T> entry, ConfigEntry<T> entry,
@NonNull ReadWriteContextExtensionsData contextExtensionsData Patchwork contextExtensionsData
) throws TweedEntryReadException { ) throws TweedEntryReadException {
try { try {
return getReaderChain(entry).read(reader, entry, new TweedReadWriteContextImpl(contextExtensionsData)); return getReaderChain(entry).read(reader, entry, new TweedReadWriteContextImpl(this, contextExtensionsData));
} catch (TweedDataReadException e) { } catch (TweedDataReadException e) {
throw new TweedEntryReadException("Failed to read entry", e); throw new TweedEntryReadException("Failed to read entry", e);
} }
@@ -182,46 +135,35 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
@Override @Override
public <T extends @Nullable Object> void write( public <T extends @Nullable Object> void write(
@NonNull TweedDataVisitor writer, TweedDataVisitor writer,
@Nullable T value, @Nullable T value,
@NonNull ConfigEntry<T> entry, ConfigEntry<T> entry,
@NonNull ReadWriteContextExtensionsData contextExtensionsData Patchwork contextExtensionsData
) throws TweedEntryWriteException { ) throws TweedEntryWriteException {
try { try {
getWriterChain(entry).write(writer, value, entry, new TweedReadWriteContextImpl(contextExtensionsData)); getWriterChain(entry).write(writer, value, entry, new TweedReadWriteContextImpl(this, contextExtensionsData));
} catch (TweedDataWriteException e) { } catch (TweedDataWriteException e) {
throw new TweedEntryWriteException("Failed to write entry", e); throw new TweedEntryWriteException("Failed to write entry", e);
} }
} }
@Value @Data
private static class ReadWriteEntryDataExtensionImpl implements ReadWriteEntryDataExtension { private static class CustomEntryData {
TweedEntryReader<?, ?> entryReaderChain; private TweedEntryReader<?, ?> readerDefinition = TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
TweedEntryWriter<?, ?> entryWriterChain; private TweedEntryWriter<?, ?> writerDefinition = TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
private TweedEntryReader<?, ?> readerChain;
private TweedEntryWriter<?, ?> writerChain;
} }
@Setter @Override
private static class RegisteredExtensionDataImpl<U extends Patchwork<@NonNull U>, E> public <T, C extends ConfigEntry<T>> TweedEntryReader<T, C> getReaderChain(C entry) {
implements RegisteredExtensionData<U, E> {
private MethodHandle setter;
@Override
public void set(U patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
static <T> TweedEntryReader<T, @NonNull ConfigEntry<T>> getReaderChain(ConfigEntry<T> elementEntry) {
//noinspection unchecked //noinspection unchecked
return (TweedEntryReader<T, @NonNull ConfigEntry<T>>) ((ReadWriteEntryDataExtension) elementEntry.extensionsData()).entryReaderChain(); return (TweedEntryReader<T, C>) entry.extensionsData().get(customEntryDataAccess).readerChain();
} }
static <T> TweedEntryWriter<T, @NonNull ConfigEntry<T>> getWriterChain(ConfigEntry<T> elementEntry) { @Override
public <T, C extends ConfigEntry<T>> TweedEntryWriter<T, C> getWriterChain(C entry) {
//noinspection unchecked //noinspection unchecked
return (TweedEntryWriter<T, @NonNull ConfigEntry<T>>) ((ReadWriteEntryDataExtension) elementEntry.extensionsData()).entryWriterChain(); return (TweedEntryWriter<T, C>) entry.extensionsData().get(customEntryDataAccess).writerChain();
} }
} }

View File

@@ -10,7 +10,6 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.*; import java.util.*;
@@ -63,7 +62,7 @@ public class TweedEntryReaderWriterImpls {
} }
@RequiredArgsConstructor @RequiredArgsConstructor
private static class PrimitiveReaderWriter<T extends @NonNull Object> implements TweedEntryReaderWriter<T, ConfigEntry<T>> { private static class PrimitiveReaderWriter<T extends @Nullable Object> implements TweedEntryReaderWriter<T, ConfigEntry<T>> {
private final Function<TweedDataToken, T> readerCall; private final Function<TweedDataToken, T> readerCall;
private final BiConsumer<TweedDataVisitor, T> writerCall; private final BiConsumer<TweedDataVisitor, T> writerCall;
@@ -79,7 +78,7 @@ public class TweedEntryReaderWriterImpls {
} }
} }
public static class CollectionReaderWriter<T extends @NonNull Object, C extends Collection<T>> implements TweedEntryReaderWriter<C, CollectionConfigEntry<T, C>> { public static class CollectionReaderWriter<T extends @Nullable Object, C extends Collection<T>> implements TweedEntryReaderWriter<C, CollectionConfigEntry<T, C>> {
@Override @Override
public C read(TweedDataReader reader, CollectionConfigEntry<T, C> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException { public C read(TweedDataReader reader, CollectionConfigEntry<T, C> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
assertIsToken(reader.readToken(), TweedDataToken::isListStart, "Expected list start"); assertIsToken(reader.readToken(), TweedDataToken::isListStart, "Expected list start");
@@ -89,9 +88,9 @@ public class TweedEntryReaderWriterImpls {
} }
ConfigEntry<T> elementEntry = entry.elementEntry(); ConfigEntry<T> elementEntry = entry.elementEntry();
TweedEntryReader<T, ConfigEntry<T>> elementReader = ReadWriteExtensionImpl.getReaderChain(elementEntry); TweedEntryReader<T, ConfigEntry<T>> elementReader = context.readWriteExtension().getReaderChain(elementEntry);
List<T> list = new ArrayList<>(20); List<@Nullable T> list = new ArrayList<>(20);
while (true) { while (true) {
token = reader.peekToken(); token = reader.peekToken();
if (token.isListEnd()) { if (token.isListEnd()) {
@@ -119,7 +118,7 @@ public class TweedEntryReaderWriterImpls {
} }
ConfigEntry<T> elementEntry = entry.elementEntry(); ConfigEntry<T> elementEntry = entry.elementEntry();
TweedEntryWriter<T, ConfigEntry<T>> elementWriter = ReadWriteExtensionImpl.getWriterChain(elementEntry); TweedEntryWriter<T, ConfigEntry<T>> elementWriter = context.readWriteExtension().getWriterChain(elementEntry);
writer.visitListStart(); writer.visitListStart();
for (T element : value) { for (T element : value) {
@@ -129,7 +128,7 @@ public class TweedEntryReaderWriterImpls {
} }
} }
public static class CompoundReaderWriter<T extends @NonNull Object> implements TweedEntryReaderWriter<T, CompoundConfigEntry<T>> { public static class CompoundReaderWriter<T> implements TweedEntryReaderWriter<T, CompoundConfigEntry<T>> {
@Override @Override
public T read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException { public T read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) throws TweedEntryReadException, TweedDataReadException {
assertIsToken(reader.readToken(), TweedDataToken::isMapStart, "Expected map start"); assertIsToken(reader.readToken(), TweedDataToken::isMapStart, "Expected map start");
@@ -145,7 +144,7 @@ public class TweedEntryReaderWriterImpls {
//noinspection unchecked //noinspection unchecked
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key); ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key);
TweedEntryReader<Object, ConfigEntry<Object>> subEntryReaderChain = ReadWriteExtensionImpl.getReaderChain(subEntry); TweedEntryReader<Object, ConfigEntry<Object>> subEntryReaderChain = context.readWriteExtension().getReaderChain(subEntry);
Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context); Object subEntryValue = subEntryReaderChain.read(reader, subEntry, context);
entry.set(compoundValue, key, subEntryValue); entry.set(compoundValue, key, subEntryValue);
} else { } else {
@@ -167,7 +166,7 @@ public class TweedEntryReaderWriterImpls {
String key = e.getKey(); String key = e.getKey();
ConfigEntry<Object> subEntry = e.getValue(); ConfigEntry<Object> subEntry = e.getValue();
TweedEntryWriter<Object, ConfigEntry<Object>> subEntryWriterChain = ReadWriteExtensionImpl.getWriterChain(subEntry); TweedEntryWriter<Object, ConfigEntry<Object>> subEntryWriterChain = context.readWriteExtension().getWriterChain(subEntry);
writer.visitMapEntryKey(key); writer.visitMapEntryKey(key);
subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context); subEntryWriterChain.write(writer, entry.get(value, key), subEntry, context);

View File

@@ -1,11 +1,13 @@
package de.siphalor.tweed5.data.extension.impl; package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedReadContext; import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.TweedWriteContext; import de.siphalor.tweed5.data.extension.api.TweedWriteContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteContextExtensionsData; import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Value; import lombok.Value;
@Value @Value
public class TweedReadWriteContextImpl implements TweedReadContext, TweedWriteContext { public class TweedReadWriteContextImpl implements TweedReadContext, TweedWriteContext {
ReadWriteContextExtensionsData extensionsData; ReadWriteExtension readWriteExtension;
Patchwork extensionsData;
} }

View File

@@ -1,23 +1,18 @@
package de.siphalor.tweed5.data.extension.impl; package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.core.api.container.ConfigContainer; import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.extension.EntryExtensionsData; import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer; import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.CollectionConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.CollectionConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.EntryReaderWriterDefinition;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension; import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
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.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
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;
@@ -27,6 +22,8 @@ import java.io.Writer;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*;
import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap;
import static java.util.Map.entry; import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -36,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
class ReadWriteExtensionImplTest { class ReadWriteExtensionImplTest {
private final StringWriter stringWriter = new StringWriter(); private final StringWriter stringWriter = new StringWriter();
private final ConfigContainer<Map<String, Object>> configContainer = new DefaultConfigContainer<>(); private final ConfigContainer<Map<String, Object>> configContainer = new DefaultConfigContainer<>();
private StaticMapCompoundConfigEntryImpl<Map<String, Object>> rootEntry; private CompoundConfigEntry<Map<String, Object>> rootEntry;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@BeforeEach @BeforeEach
@@ -44,14 +41,16 @@ class ReadWriteExtensionImplTest {
configContainer.registerExtension(ReadWriteExtension.DEFAULT); configContainer.registerExtension(ReadWriteExtension.DEFAULT);
configContainer.finishExtensionSetup(); configContainer.finishExtensionSetup();
SimpleConfigEntryImpl<Integer> intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class); SimpleConfigEntry<Integer> intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
SimpleConfigEntryImpl<Boolean> booleanEntry = new SimpleConfigEntryImpl<>(configContainer, Boolean.class); .apply(entryReaderWriter(intReaderWriter()));
CollectionConfigEntryImpl<Boolean, List<Boolean>> listEntry = new CollectionConfigEntryImpl<>(
CollectionConfigEntry<Boolean, List<Boolean>> listEntry = new CollectionConfigEntryImpl<>(
configContainer, configContainer,
(Class<List<Boolean>>) (Class<?>) List.class, (Class<List<Boolean>>) (Class<?>) List.class,
ArrayList::new, ArrayList::new,
booleanEntry new SimpleConfigEntryImpl<>(configContainer, Boolean.class)
); .apply(entryReaderWriter(booleanReaderWriter()))
).apply(entryReaderWriter(collectionReaderWriter()));
rootEntry = new StaticMapCompoundConfigEntryImpl<>( rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer, configContainer,
@@ -61,16 +60,9 @@ class ReadWriteExtensionImplTest {
entry("int", intEntry), entry("int", intEntry),
entry("list", listEntry) entry("list", listEntry)
)) ))
); ).apply(entryReaderWriter(compoundReaderWriter()));
configContainer.attachTree(rootEntry); configContainer.attachTree(rootEntry);
RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition> readerWriterData = (RegisteredExtensionData<EntryExtensionsData, EntryReaderWriterDefinition>) configContainer.entryDataExtensions().get(EntryReaderWriterDefinition.class);
readerWriterData.set(rootEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.compoundReaderWriter()));
readerWriterData.set(intEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.intReaderWriter()));
readerWriterData.set(listEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.collectionReaderWriter()));
readerWriterData.set(booleanEntry.extensionsData(), new TrivialEntryReaderWriterDefinition(TweedEntryReaderWriters.booleanReaderWriter()));
configContainer.initialize(); configContainer.initialize();
} }
@@ -128,19 +120,4 @@ class ReadWriteExtensionImplTest {
private TweedDataVisitor setupWriter(Function<Writer, TweedDataVisitor> writerFactory) { private TweedDataVisitor setupWriter(Function<Writer, TweedDataVisitor> writerFactory) {
return writerFactory.apply(stringWriter); return writerFactory.apply(stringWriter);
} }
@RequiredArgsConstructor
private static class TrivialEntryReaderWriterDefinition implements EntryReaderWriterDefinition {
private final TweedEntryReaderWriter<?, ?> readerWriter;
@Override
public TweedEntryReader<?, ?> reader() {
return readerWriter;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return readerWriter;
}
}
} }

View File

@@ -74,49 +74,32 @@ public class ReadWritePojoPostProcessor implements TweedPojoWeavingPostProcessor
return; return;
} }
EntryReaderWriterDefinition definition = createDefinitionFromEntryConfig(entryConfig, context); //noinspection rawtypes,unchecked
if (definition != null) { readWriteExtension.setEntryReaderWriter(
readWriteExtension.setEntryReaderWriterDefinition(configEntry, definition); (ConfigEntry) configEntry,
} (TweedEntryReader) resolveReader(entryConfig, context),
(TweedEntryWriter) resolveWriter(entryConfig, context)
);
} }
private @Nullable EntryReaderWriterDefinition createDefinitionFromEntryConfig(EntryReadWriteConfig entryConfig, WeavingContext context) { private TweedEntryReader<?, ?> resolveReader(EntryReadWriteConfig entryConfig, WeavingContext context) {
String readerSpecText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader(); String specText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader();
String writerSpecText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer(); SerdePojoReaderWriterSpec spec = specFromText(specText, context);
SerdePojoReaderWriterSpec readerSpec;
SerdePojoReaderWriterSpec writerSpec;
if (readerSpecText.equals(writerSpecText)) {
readerSpec = writerSpec = specFromText(readerSpecText, context);
} else {
readerSpec = specFromText(readerSpecText, context);
writerSpec = specFromText(writerSpecText, context);
}
if (readerSpec == null && writerSpec == null) {
return null;
}
//noinspection unchecked,rawtypes //noinspection unchecked,rawtypes
TweedEntryReader<?, ?> reader = Optional.ofNullable(readerSpec) return Optional.ofNullable(spec)
.map((spec) -> resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, spec, context)) .map(s -> resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, s, context))
.orElse(((TweedEntryReader) TweedEntryReaderWriterImpls.NOOP_READER_WRITER)); .orElse(((TweedEntryReader) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
}
private TweedEntryWriter<?, ?> resolveWriter(EntryReadWriteConfig entryConfig, WeavingContext context) {
String specText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer();
SerdePojoReaderWriterSpec spec = specFromText(specText, context);
//noinspection unchecked,rawtypes //noinspection unchecked,rawtypes
TweedEntryWriter<?, ?> writer = Optional.ofNullable(writerSpec) return Optional.ofNullable(spec)
.map((spec) -> resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, spec, context)) .map(s -> resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, s, context))
.orElse(((TweedEntryWriter) TweedEntryReaderWriterImpls.NOOP_READER_WRITER)); .orElse(((TweedEntryWriter) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
return new EntryReaderWriterDefinition() {
@Override
public TweedEntryReader<?, ?> reader() {
return reader;
}
@Override
public TweedEntryWriter<?, ?> writer() {
return writer;
}
};
} }
private @Nullable SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) { private @Nullable SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) {

View File

@@ -2,7 +2,8 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving;
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.extension.RegisteredExtensionData; import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.typeutils.api.type.ActualType; import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CollectionWeaving; import de.siphalor.tweed5.weaver.pojo.api.annotation.CollectionWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCollectionConfigEntry; import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCollectionConfigEntry;
@@ -24,24 +25,27 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
.collectionEntryClass(CollectionConfigEntryImpl.class) .collectionEntryClass(CollectionConfigEntryImpl.class)
.build(); .build();
private RegisteredExtensionData<WeavingContext.ExtensionsData, CollectionWeavingConfig> weavingConfigAccess; @Nullable
private PatchworkPartAccess<CollectionWeavingConfig> weavingConfigAccess;
@Override @Override
public void setup(SetupContext context) { public void setup(SetupContext context) {
this.weavingConfigAccess = context.registerWeavingContextExtensionData(CollectionWeavingConfig.class); this.weavingConfigAccess = context.registerWeavingContextExtensionData(CollectionWeavingConfig.class);
} }
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"unchecked"})
@Override @Override
public <T> @Nullable ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) { public <T> @Nullable ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
assert weavingConfigAccess != null;
List<ActualType<?>> collectionTypeParams = valueType.getTypesOfSuperArguments(Collection.class); List<ActualType<?>> collectionTypeParams = valueType.getTypesOfSuperArguments(Collection.class);
if (collectionTypeParams == null) { if (collectionTypeParams == null) {
return null; return null;
} }
try { try {
CollectionWeavingConfig weavingConfig = getOrCreateWeavingConfig(context); CollectionWeavingConfig weavingConfig = getOrCreateWeavingConfig(context);
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy(); Patchwork newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig); newExtensionsData.set(weavingConfigAccess, weavingConfig);
IntFunction<Collection<Object>> constructor = getCollectionConstructor(valueType); IntFunction<Collection<Object>> constructor = getCollectionConstructor(valueType);
@@ -66,10 +70,10 @@ public class CollectionPojoWeaver implements TweedPojoWeaver {
} }
private CollectionWeavingConfig getOrCreateWeavingConfig(WeavingContext context) { private CollectionWeavingConfig getOrCreateWeavingConfig(WeavingContext context) {
CollectionWeavingConfig parent; assert weavingConfigAccess != null;
if (context.extensionsData().isPatchworkPartSet(CollectionWeavingConfig.class)) {
parent = (CollectionWeavingConfig) context.extensionsData(); CollectionWeavingConfig parent = context.extensionsData().get(weavingConfigAccess);
} else { if (parent == null) {
parent = DEFAULT_WEAVING_CONFIG; parent = DEFAULT_WEAVING_CONFIG;
} }

View File

@@ -2,10 +2,11 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving;
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.extension.RegisteredExtensionData;
import de.siphalor.tweed5.namingformat.api.NamingFormat; import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.namingformat.api.NamingFormatCollector; import de.siphalor.tweed5.namingformat.api.NamingFormatCollector;
import de.siphalor.tweed5.namingformat.api.NamingFormats; import de.siphalor.tweed5.namingformat.api.NamingFormats;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving; import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry; import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import de.siphalor.tweed5.typeutils.api.type.ActualType; import de.siphalor.tweed5.typeutils.api.type.ActualType;
@@ -22,7 +23,6 @@ import java.lang.invoke.MethodHandle;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -37,7 +37,8 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
.build(); .build();
private final NamingFormatCollector namingFormatCollector = new NamingFormatCollector(); private final NamingFormatCollector namingFormatCollector = new NamingFormatCollector();
private RegisteredExtensionData<WeavingContext.ExtensionsData, CompoundWeavingConfig> weavingConfigAccess; @Nullable
private PatchworkPartAccess<CompoundWeavingConfig> weavingConfigAccess;
public void setup(SetupContext context) { public void setup(SetupContext context) {
namingFormatCollector.setupFormats(); namingFormatCollector.setupFormats();
@@ -47,13 +48,15 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
@Override @Override
public <T> @Nullable ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) { public <T> @Nullable ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
assert weavingConfigAccess != null;
if (context.annotations().getAnnotation(CompoundWeaving.class) == null) { if (context.annotations().getAnnotation(CompoundWeaving.class) == null) {
return null; return null;
} }
try { try {
CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(context); CompoundWeavingConfig weavingConfig = getOrCreateWeavingConfig(context);
WeavingContext.ExtensionsData newExtensionsData = context.extensionsData().copy(); Patchwork newExtensionsData = context.extensionsData().copy();
weavingConfigAccess.set(newExtensionsData, weavingConfig); newExtensionsData.set(weavingConfigAccess, weavingConfig);
PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueType.declaredType()); PojoClassIntrospector introspector = PojoClassIntrospector.forClass(valueType.declaredType());
@@ -69,10 +72,10 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
} }
private CompoundWeavingConfig getOrCreateWeavingConfig(WeavingContext context) { private CompoundWeavingConfig getOrCreateWeavingConfig(WeavingContext context) {
CompoundWeavingConfig parent; assert weavingConfigAccess != null;
if (context.extensionsData().isPatchworkPartSet(CompoundWeavingConfig.class)) {
parent = (CompoundWeavingConfig) context.extensionsData(); CompoundWeavingConfig parent = context.extensionsData().get(weavingConfigAccess);
} else { if (parent == null) {
parent = DEFAULT_WEAVING_CONFIG; parent = DEFAULT_WEAVING_CONFIG;
} }
@@ -143,10 +146,14 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
private WeavableCompoundConfigEntry.SubEntry weaveCompoundSubEntry( private WeavableCompoundConfigEntry.SubEntry weaveCompoundSubEntry(
PojoClassIntrospector.Property property, PojoClassIntrospector.Property property,
WeavingContext.ExtensionsData newExtensionsData, Patchwork newExtensionsData,
WeavingContext parentContext WeavingContext parentContext
) { ) {
String name = convertName(property.field().getName(), (CompoundWeavingConfig) newExtensionsData); assert weavingConfigAccess != null;
CompoundWeavingConfig weavingConfig = newExtensionsData.get(weavingConfigAccess);
assert weavingConfig != null;
String name = convertName(property.field().getName(), weavingConfig);
WeavingContext subContext = createSubContextForProperty(property, name, newExtensionsData, parentContext); WeavingContext subContext = createSubContextForProperty(property, name, newExtensionsData, parentContext);
ConfigEntry<?> subEntry; ConfigEntry<?> subEntry;
@@ -189,7 +196,7 @@ public class CompoundPojoWeaver implements TweedPojoWeaver {
private WeavingContext createSubContextForProperty( private WeavingContext createSubContextForProperty(
PojoClassIntrospector.Property property, PojoClassIntrospector.Property property,
String name, String name,
WeavingContext.ExtensionsData newExtensionsData, Patchwork newExtensionsData,
WeavingContext parentContext WeavingContext parentContext
) { ) {
return parentContext.subContextBuilder(name) return parentContext.subContextBuilder(name)

View File

@@ -1,7 +1,8 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving; package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.construct.api.TweedConstructFactory; import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData; import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
public interface TweedPojoWeaver extends TweedPojoWeavingFunction { public interface TweedPojoWeaver extends TweedPojoWeavingFunction {
@@ -11,6 +12,6 @@ public interface TweedPojoWeaver extends TweedPojoWeavingFunction {
void setup(SetupContext context); void setup(SetupContext context);
interface SetupContext { interface SetupContext {
<E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(Class<E> dataClass); <E> PatchworkPartAccess<E> registerWeavingContextExtensionData(Class<E> dataClass);
} }
} }

View File

@@ -18,7 +18,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
TweedPojoWeavingFunction.NonNull weavingFunction; TweedPojoWeavingFunction.NonNull weavingFunction;
ConfigContainer<?> configContainer; ConfigContainer<?> configContainer;
String[] path; String[] path;
ExtensionsData extensionsData; Patchwork extensionsData;
AnnotatedElement annotations; AnnotatedElement annotations;
public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer) { public static Builder builder(TweedPojoWeavingFunction.NonNull weavingFunction, ConfigContainer<?> configContainer) {
@@ -40,8 +40,6 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
return weavingFunction.weaveEntry(valueType, context); return weavingFunction.weaveEntry(valueType, context);
} }
public interface ExtensionsData extends Patchwork<ExtensionsData> {}
@Accessors(fluent = true, chain = true) @Accessors(fluent = true, chain = true)
@Setter @Setter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@@ -51,7 +49,7 @@ public class WeavingContext implements TweedPojoWeavingFunction.NonNull {
private final TweedPojoWeavingFunction.NonNull weavingFunction; private final TweedPojoWeavingFunction.NonNull weavingFunction;
private final ConfigContainer<?> configContainer; private final ConfigContainer<?> configContainer;
private final String[] path; private final String[] path;
private ExtensionsData extensionsData; private Patchwork extensionsData;
private AnnotatedElement annotations; private AnnotatedElement annotations;
public WeavingContext build() { public WeavingContext build() {

View File

@@ -62,7 +62,7 @@ public class CollectionConfigEntryImpl<E, T extends Collection<E>> extends BaseC
} }
@Override @Override
public @NotNull T deepCopy(@NotNull T value) { public @NotNull T deepCopy(T value) {
T copy = instantiateCollection(value.size()); T copy = instantiateCollection(value.size());
for (E element : value) { for (E element : value) {
copy.add(elementEntry.deepCopy(element)); copy.add(elementEntry.deepCopy(element));

View File

@@ -7,6 +7,7 @@ import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor; 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.weaver.pojo.api.entry.WeavableCompoundConfigEntry; import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import org.jspecify.annotations.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -97,7 +98,7 @@ public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> impleme
} }
@Override @Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) { public void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value) {
if (visitor.enterCompoundEntry(this, value)) { if (visitor.enterCompoundEntry(this, value)) {
subEntries.forEach((key, entry) -> { subEntries.forEach((key, entry) -> {
if (visitor.enterCompoundSubEntry(key)) { if (visitor.enterCompoundSubEntry(key)) {

View File

@@ -2,22 +2,21 @@ package de.siphalor.tweed5.weaver.pojo.impl.weaving;
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.extension.RegisteredExtensionData; import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkClassCreator; import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.impl.PatchworkClass;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassGenerator;
import de.siphalor.tweed5.patchwork.impl.PatchworkClassPart;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.typeutils.api.type.ActualType; import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver; import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor; import de.siphalor.tweed5.weaver.pojo.api.weaving.postprocess.TweedPojoWeavingPostProcessor;
import lombok.*; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle; import java.util.Arrays;
import java.util.*; import java.util.Collection;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -31,7 +30,8 @@ public class TweedPojoWeaverBootstrapper<T> {
private final ConfigContainer<T> configContainer; private final ConfigContainer<T> configContainer;
private final Collection<TweedPojoWeaver> weavers; private final Collection<TweedPojoWeaver> weavers;
private final Collection<TweedPojoWeavingPostProcessor> postProcessors; private final Collection<TweedPojoWeavingPostProcessor> postProcessors;
private PatchworkClass<WeavingContext.ExtensionsData> contextExtensionsDataClass; @Nullable
private PatchworkFactory contextExtensionsPatchworkFactory;
public static <T> TweedPojoWeaverBootstrapper<T> create(Class<T> pojoClass) { public static <T> TweedPojoWeaverBootstrapper<T> create(Class<T> pojoClass) {
PojoWeaving rootWeavingConfig = expectAnnotation(pojoClass, PojoWeaving.class); PojoWeaving rootWeavingConfig = expectAnnotation(pojoClass, PojoWeaving.class);
@@ -88,44 +88,21 @@ public class TweedPojoWeaverBootstrapper<T> {
} }
private void setupWeavers() { private void setupWeavers() {
Map<Class<?>, RegisteredExtensionDataImpl<?>> registeredExtensions = new HashMap<>(); PatchworkFactory.Builder contextExtensionsPatchworkFactoryBuilder = PatchworkFactory.builder();
TweedPojoWeaver.SetupContext setupContext = new TweedPojoWeaver.SetupContext() { TweedPojoWeaver.SetupContext setupContext = contextExtensionsPatchworkFactoryBuilder::registerPart;
@Override
public <E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(
Class<E> dataClass
) {
RegisteredExtensionDataImpl<E> registeredExtension = new RegisteredExtensionDataImpl<>();
registeredExtensions.put(dataClass, registeredExtension);
return registeredExtension;
}
};
for (TweedPojoWeaver weaver : weavers) { for (TweedPojoWeaver weaver : weavers) {
weaver.setup(setupContext); weaver.setup(setupContext);
} }
PatchworkClassCreator<WeavingContext.ExtensionsData> weavingContextCreator = PatchworkClassCreator.<WeavingContext.ExtensionsData>builder() contextExtensionsPatchworkFactory = contextExtensionsPatchworkFactoryBuilder.build();
.classPackage(this.getClass().getPackage().getName() + ".generated")
.classPrefix("WeavingContext$")
.patchworkInterface(WeavingContext.ExtensionsData.class)
.build();
try {
this.contextExtensionsDataClass = weavingContextCreator.createClass(registeredExtensions.keySet());
for (PatchworkClassPart part : this.contextExtensionsDataClass.parts()) {
RegisteredExtensionDataImpl<?> registeredExtension = registeredExtensions.get(part.partInterface());
registeredExtension.setter(part.fieldSetter());
}
} catch (PatchworkClassGenerator.GenerationException e) {
throw new PojoWeavingException("Failed to create weaving context extensions data");
}
} }
private WeavingContext createWeavingContext() { private WeavingContext createWeavingContext() {
try { try {
WeavingContext.ExtensionsData extensionsData = (WeavingContext.ExtensionsData) contextExtensionsDataClass.constructor().invoke(); assert contextExtensionsPatchworkFactory != null;
Patchwork extensionsData = contextExtensionsPatchworkFactory.create();
return WeavingContext.builder(this::weaveEntry, configContainer) return WeavingContext.builder(this::weaveEntry, configContainer)
.extensionsData(extensionsData) .extensionsData(extensionsData)
@@ -157,18 +134,4 @@ public class TweedPojoWeaverBootstrapper<T> {
} }
} }
} }
@Setter
private static class RegisteredExtensionDataImpl<E> implements RegisteredExtensionData<WeavingContext.ExtensionsData, E> {
private MethodHandle setter;
@Override
public void set(WeavingContext.ExtensionsData patchwork, E extension) {
try {
setter.invokeWithArguments(patchwork, extension);
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
}
} }

View File

@@ -2,24 +2,18 @@ package de.siphalor.tweed5.weaver.pojo.api.weaving;
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.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.RegisteredExtensionData;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl; import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.namingformat.api.NamingFormat; import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import de.siphalor.tweed5.typeutils.api.type.ActualType; import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig; import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.NullUnmarked;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith; import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass; import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.mock;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@NullUnmarked @NullUnmarked
@@ -27,13 +21,12 @@ class CompoundPojoWeaverTest {
@Test @Test
void weave() { void weave() {
PatchworkFactory.Builder weavingContextExtensionDataFactoryBuilder = PatchworkFactory.builder();
CompoundPojoWeaver compoundWeaver = new CompoundPojoWeaver(); CompoundPojoWeaver compoundWeaver = new CompoundPojoWeaver();
compoundWeaver.setup(new TweedPojoWeaver.SetupContext() { compoundWeaver.setup(weavingContextExtensionDataFactoryBuilder::registerPart);
@Override
public <E> RegisteredExtensionData<WeavingContext.ExtensionsData, E> registerWeavingContextExtensionData(Class<E> dataClass) { PatchworkFactory weavingContextExtensionDataFactory = weavingContextExtensionDataFactoryBuilder.build();
return (patchwork, extension) -> ((ExtensionsDataMock) patchwork).weavingConfig = (CompoundWeavingConfig) extension;
}
});
WeavingContext weavingContext = WeavingContext.builder(new TweedPojoWeavingFunction.NonNull() { WeavingContext weavingContext = WeavingContext.builder(new TweedPojoWeavingFunction.NonNull() {
@Override @Override
@@ -46,7 +39,7 @@ class CompoundPojoWeaverTest {
} }
} }
}, mock(ConfigContainer.class)) }, mock(ConfigContainer.class))
.extensionsData(new ExtensionsDataMock(null)) .extensionsData(weavingContextExtensionDataFactory.create())
.annotations(Compound.class) .annotations(Compound.class)
.build(); .build();
@@ -86,39 +79,4 @@ class CompoundPojoWeaverTest {
public static class InnerValue { public static class InnerValue {
public Integer value; public Integer value;
} }
@AllArgsConstructor
private static class ExtensionsDataMock implements WeavingContext.ExtensionsData, CompoundWeavingConfig {
private CompoundWeavingConfig weavingConfig;
@Override
public boolean isPatchworkPartDefined(Class<?> patchworkInterface) {
return patchworkInterface == CompoundWeavingConfig.class;
}
@Override
public boolean isPatchworkPartSet(Class<?> patchworkInterface) {
return weavingConfig != null;
}
@Override
public WeavingContext.ExtensionsData copy() {
return new ExtensionsDataMock(weavingConfig);
}
@Override
public NamingFormat compoundSourceNamingFormat() {
return weavingConfig.compoundSourceNamingFormat();
}
@Override
public NamingFormat compoundTargetNamingFormat() {
return weavingConfig.compoundTargetNamingFormat();
}
@Override
public @Nullable Class<? extends WeavableCompoundConfigEntry> compoundEntryClass() {
return weavingConfig.compoundEntryClass();
}
}
} }