[core, weaver-pojo] Introduce NullableConfigEntry and pseudo entries during weaving

This commit is contained in:
2025-10-28 19:15:29 +01:00
parent ffbecb9158
commit 370324668d
21 changed files with 418 additions and 90 deletions

View File

@@ -2,7 +2,6 @@ package de.siphalor.tweed5.core.api.entry;
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;
import java.util.function.Consumer;
@@ -26,7 +25,7 @@ public interface ConfigEntry<T extends @Nullable Object> {
}
void visitInOrder(ConfigEntryVisitor visitor);
void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value);
void visitInOrder(ConfigEntryValueVisitor visitor, T value);
T deepCopy(@NonNull T value);
T deepCopy(T value);
}

View File

@@ -0,0 +1,17 @@
package de.siphalor.tweed5.core.api.entry;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
public interface NullableConfigEntry<T extends @Nullable Object> extends StructuredConfigEntry<T> {
String NON_NULL_KEY = ":nonNull";
@Override
default NullableConfigEntry<T> apply(Consumer<ConfigEntry<T>> function) {
StructuredConfigEntry.super.apply(function);
return this;
}
ConfigEntry<T> nonNullEntry();
}

View File

@@ -16,7 +16,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
public class DefaultConfigContainer<T> implements ConfigContainer<T> {
public class DefaultConfigContainer<T extends @Nullable Object> implements ConfigContainer<T> {
@Getter
private ConfigContainerSetupPhase setupPhase = ConfigContainerSetupPhase.EXTENSIONS_SETUP;
private final Set<Class<? extends TweedExtension>> requestedExtensions = new HashSet<>();

View File

@@ -0,0 +1,52 @@
package de.siphalor.tweed5.core.impl.entry;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.BaseConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Map;
@Getter
public class NullableConfigEntryImpl<T extends @Nullable Object> extends BaseConfigEntry<T> implements NullableConfigEntry<T> {
private final ConfigEntry<T> nonNullEntry;
public NullableConfigEntryImpl(
ConfigContainer<?> container,
Class<T> valueClass,
ConfigEntry<T> nonNullEntry
) {
super(container, valueClass);
this.nonNullEntry = nonNullEntry;
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return Collections.singletonMap(NON_NULL_KEY, nonNullEntry);
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (value != null) {
if (visitor.enterStructuredEntry(this, value)) {
if (visitor.enterStructuredSubEntry(NON_NULL_KEY, NON_NULL_KEY)) {
nonNullEntry.visitInOrder(visitor, value);
visitor.leaveStructuredSubEntry(NON_NULL_KEY, NON_NULL_KEY);
}
visitor.leaveStructuredEntry(this, value);
}
}
}
@Override
public T deepCopy(T value) {
if (value != null) {
return nonNullEntry.deepCopy(value);
}
return null;
}
}

View File

@@ -1,10 +1,12 @@
package de.siphalor.tweed5.defaultextensions.comment.impl;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.NullableConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
@@ -36,7 +38,7 @@ class CommentExtensionImplTest {
private DefaultConfigContainer<@NonNull Map<String, Object>> configContainer;
private CompoundConfigEntry<Map<String, Object>> rootEntry;
private SimpleConfigEntry<Integer> intEntry;
private NullableConfigEntry<Integer> intEntry;
private SimpleConfigEntry<String> stringEntry;
private SimpleConfigEntry<Long> noCommentEntry;
@@ -49,9 +51,13 @@ class CommentExtensionImplTest {
configContainer.registerExtensions(extraExtensions);
configContainer.finishExtensionSetup();
intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
intEntry = new NullableConfigEntryImpl<>(
configContainer, Integer.class,
new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter()))
.apply(baseComment("It is an integer"));
.apply(baseComment("It is an integer")))
.apply(entryReaderWriter(nullableReaderWriter()))
.apply(baseComment("This is nullable"));
stringEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()))
.apply(baseComment("It is a string"));
@@ -80,7 +86,7 @@ class CommentExtensionImplTest {
configContainer.initialize();
CommentExtension commentExtension = configContainer.extension(CommentExtension.class).orElseThrow();
assertEquals("It is an integer", commentExtension.getFullComment(intEntry));
assertEquals("This is nullable", commentExtension.getFullComment(intEntry));
assertEquals("It is a string", commentExtension.getFullComment(stringEntry));
assertNull(commentExtension.getFullComment(noCommentEntry));
}
@@ -91,7 +97,7 @@ class CommentExtensionImplTest {
configContainer.initialize();
CommentExtension commentExtension = configContainer.extension(CommentExtension.class).orElseThrow();
assertEquals("The comment is:\nIt is an integer\nEND", commentExtension.getFullComment(intEntry));
assertEquals("The comment is:\nThis is nullable\nEND", commentExtension.getFullComment(intEntry));
assertEquals("The comment is:\nIt is a string\nEND", commentExtension.getFullComment(stringEntry));
assertEquals("The comment is:\n\nEND", commentExtension.getFullComment(noCommentEntry));
}
@@ -120,6 +126,7 @@ class CommentExtensionImplTest {
// This is the root value.
// It is the topmost value in the tree.
{
\t// This is nullable
\t// It is an integer
\tint: 123
\t// It is a string

View File

@@ -5,6 +5,7 @@ import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.CollectionConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.NullableConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
@@ -51,28 +52,30 @@ class PatchExtensionImplTest {
var int1Entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter()));
var int2Entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(
nullableReader(intReaderWriter()),
nullableWriter(intReaderWriter())
));
var listEntry = new CollectionConfigEntryImpl<>(
var int2Entry = new NullableConfigEntryImpl<>(
configContainer, Integer.class,
new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter())));
var listEntry = new NullableConfigEntryImpl<>(
configContainer,
(Class<List<Integer>>)(Class) List.class,
new CollectionConfigEntryImpl<>(
configContainer,
(Class<List<Integer>>)(Class) List.class,
ArrayList::new,
new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter()))
)
.apply(entryReaderWriter(
nullableReader(collectionReaderWriter()),
nullableWriter(collectionReaderWriter())
));
).apply(entryReaderWriter(collectionReaderWriter()))
).apply(entryReaderWriter(nullableReaderWriter()));
var nestedInt1Entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter()));
var nestedInt2Entry = new SimpleConfigEntryImpl<>(configContainer, Integer.class)
.apply(entryReaderWriter(intReaderWriter()));
var compoundEntry = new StaticMapCompoundConfigEntryImpl<>(
var compoundEntry = new NullableConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class) Map.class,
new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class) Map.class,
HashMap::new,
@@ -80,11 +83,8 @@ class PatchExtensionImplTest {
entry("int1", nestedInt1Entry),
entry("int2", nestedInt2Entry)
))
)
.apply(entryReaderWriter(
nullableReader(compoundReaderWriter()),
nullableWriter(compoundReaderWriter())
));
).apply(entryReaderWriter(compoundReaderWriter()))
).apply(entryReaderWriter(nullableReaderWriter()));
rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,

View File

@@ -1,6 +1,5 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import de.siphalor.tweed5.dataapi.api.TweedDataTokens;

View File

@@ -3,12 +3,14 @@ package de.siphalor.tweed5.data.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Collection;
@@ -51,12 +53,25 @@ public class TweedEntryReaderWriters {
return (TweedEntryReaderWriter<T, @NonNull ConfigEntry<T>>)(TweedEntryReaderWriter) TweedEntryReaderWriterImpls.ENUM_READER_WRITER;
}
public static <T, C extends ConfigEntry<T>> TweedEntryReader<T, C> nullableReader(TweedEntryReader<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableReader<>(delegate);
public static <T extends @Nullable Object> TweedEntryReaderWriter<T, NullableConfigEntry<T>> nullableReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<T, @NonNull NullableConfigEntry<T>>) (TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.NULLABLE_READER_WRITER;
}
/**
* @deprecated You probably want to use {@link #nullableReaderWriter()} instead.
*/
@Deprecated
public static <T, C extends ConfigEntry<T>> TweedEntryReader<T, C> nullableReader(TweedEntryReader<T, C> delegate) {
return new TweedEntryReaderWriterImpls.FixedNullableReader<>(delegate);
}
/**
* @deprecated You probably want to use {@link #nullableReaderWriter()} instead.
*/
@Deprecated
public static <T, C extends ConfigEntry<T>> TweedEntryWriter<T, C> nullableWriter(TweedEntryWriter<T, C> delegate) {
return new TweedEntryReaderWriterImpls.NullableWriter<>(delegate);
return new TweedEntryReaderWriterImpls.FixedNullableWriter<>(delegate);
}
public static <T, C extends Collection<T>> TweedEntryReaderWriter<C, CollectionConfigEntry<T, C>> collectionReaderWriter() {

View File

@@ -4,6 +4,10 @@ import com.google.auto.service.AutoService;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.TweedReaderWriterProvider;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*;
@@ -39,22 +43,32 @@ public class DefaultTweedEntryReaderWriterImplsProvider implements TweedReaderWr
context.registerWriterFactory("tweed5.string", new StaticReaderWriterFactory<>(stringReaderWriter()));
context.registerReaderFactory("tweed5.enum", new StaticReaderWriterFactory<>(enumReaderWriter()));
context.registerWriterFactory("tweed5.enum", new StaticReaderWriterFactory<>(enumReaderWriter()));
context.registerReaderFactory("tweed5.nullable", new NullableReaderWriterFactory<>(
TweedEntryReaderWriters::nullableReader
));
context.registerWriterFactory("tweed5.nullable", new NullableReaderWriterFactory<>(
TweedEntryReaderWriters::nullableWriter
));
context.registerReaderFactory("tweed5.collection", new StaticReaderWriterFactory<>(collectionReaderWriter()));
context.registerWriterFactory("tweed5.collection", new StaticReaderWriterFactory<>(collectionReaderWriter()));
context.registerReaderFactory("tweed5.compound", new StaticReaderWriterFactory<>(compoundReaderWriter()));
context.registerWriterFactory("tweed5.compound", new StaticReaderWriterFactory<>(compoundReaderWriter()));
}
context.registerReaderFactory("tweed5.nullable", delegateReaders -> {
if (delegateReaders.length != 1) {
throw new IllegalArgumentException("Nullable reader requires a single delegate argument, got " + delegateReaders.length);
@RequiredArgsConstructor
private static class NullableReaderWriterFactory<T> implements ReaderWriterFactory<T> {
private final Function<T, T> delegateBasedFactory;
@Override
public T create(T... delegateReaderWriters) {
if (delegateReaderWriters.length == 0) {
//noinspection unchecked
return (T) TweedEntryReaderWriters.nullableReaderWriter();
} else if (delegateReaderWriters.length == 1) {
return delegateBasedFactory.apply(delegateReaderWriters[0]);
} else {
throw new IllegalArgumentException("Nullable readers and writers may have only one or zero delegates");
}
return nullableReader(delegateReaders[0]);
});
context.registerWriterFactory("tweed5.nullable", delegateWriters -> {
if (delegateWriters.length != 1) {
throw new IllegalArgumentException("Nullable writer requires a single delegate argument, got " + delegateWriters.length);
}
return nullableWriter(delegateWriters[0]);
});
}
}

View File

@@ -3,6 +3,7 @@ package de.siphalor.tweed5.data.extension.impl;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.dataapi.api.*;
@@ -28,13 +29,48 @@ public class TweedEntryReaderWriterImpls {
public static final TweedEntryReaderWriter<String, ConfigEntry<String>> STRING_READER_WRITER = new PrimitiveReaderWriter<>(TweedDataToken::readAsString, TweedDataVisitor::visitString);
public static final TweedEntryReaderWriter<Enum<?>, ConfigEntry<Enum<?>>> ENUM_READER_WRITER = new EnumReaderWriter<>();
public static final TweedEntryReaderWriter<Object, NullableConfigEntry<Object>> NULLABLE_READER_WRITER = new NullableReaderWriter<>();
public static final TweedEntryReaderWriter<Collection<Object>, CollectionConfigEntry<Object, Collection<Object>>> COLLECTION_READER_WRITER = new CollectionReaderWriter<>();
public static final TweedEntryReaderWriter<Object, CompoundConfigEntry<Object>> COMPOUND_READER_WRITER = new CompoundReaderWriter<>();
public static final TweedEntryReaderWriter<Object, ConfigEntry<Object>> NOOP_READER_WRITER = new NoopReaderWriter();
public static class NullableReaderWriter<T extends @Nullable Object> implements TweedEntryReaderWriter<T, NullableConfigEntry<T>> {
@Override
public T read(TweedDataReader reader, NullableConfigEntry<T> entry, TweedReadContext context)
throws TweedEntryReadException {
try {
if (reader.peekToken().isNull()) {
reader.readToken();
return null;
}
} catch (TweedDataReadException e) {
throw new TweedEntryReadException(e, context);
}
TweedEntryReader<T, ConfigEntry<T>> nonNullReader = context.readWriteExtension().getReaderChain(entry.nonNullEntry());
return nonNullReader.read(reader, entry.nonNullEntry(), context);
}
@Override
public void write(
TweedDataVisitor writer,
@Nullable T value,
NullableConfigEntry<T> entry,
TweedWriteContext context
) throws TweedEntryWriteException, TweedDataWriteException {
if (value == null) {
writer.visitNull();
} else {
TweedEntryWriter<T, ConfigEntry<T>> nonNullWriter = context.readWriteExtension().getWriterChain(entry.nonNullEntry());
nonNullWriter.write(writer, value, entry.nonNullEntry(), context);
}
}
}
@Deprecated
@RequiredArgsConstructor
public static class NullableReader<T extends @Nullable Object, C extends ConfigEntry<T>> implements TweedEntryReader<T, C> {
public static class FixedNullableReader<T extends @Nullable Object, C extends ConfigEntry<T>> implements TweedEntryReader<T, C> {
private final TweedEntryReader<T, C> delegate;
@Override
@@ -51,8 +87,9 @@ public class TweedEntryReaderWriterImpls {
}
}
@Deprecated
@RequiredArgsConstructor
public static class NullableWriter<T extends @Nullable Object, C extends ConfigEntry<T>> implements TweedEntryWriter<T, C> {
public static class FixedNullableWriter<T extends @Nullable Object, C extends ConfigEntry<T>> implements TweedEntryWriter<T, C> {
private final TweedEntryWriter<T, C> delegate;
@Override

View File

@@ -2,10 +2,12 @@ package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
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.TweedReaderWriterProvider;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
@@ -113,6 +115,15 @@ public class AutoReadWritePojoWeavingProcessor implements TweedPojoWeavingExtens
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
if (configEntry instanceof NullableConfigEntry) {
readWriteExtension.setEntryReaderWriter(
configEntry,
TweedEntryReaderWriters.nullableReaderWriter(),
TweedEntryReaderWriters.nullableReaderWriter()
);
return;
}
assert customDataAccess != null;
CustomData customData = context.extensionsData().get(customDataAccess);
if (customData == null || customData.mappings().isEmpty()) {

View File

@@ -5,6 +5,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @see AutoNullableReadWritePojoWeavingProcessor
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface AutoNullableReadWriteBehavior {

View File

@@ -17,6 +17,11 @@ import org.jspecify.annotations.Nullable;
import java.lang.annotation.Annotation;
/**
* @deprecated The recommended approach is to use {@link de.siphalor.tweed5.weaver.pojo.api.weaving.NullablePojoWeaver}
* and set @{@link Nullable} on all nullable entries.
*/
@Deprecated
public class AutoNullableReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension {
private final ReadWriteExtension readWriteExtension;
private @Nullable PatchworkPartAccess<CustomData> customDataAccess;
@@ -71,14 +76,14 @@ public class AutoNullableReadWritePojoWeavingProcessor implements TweedPojoWeavi
if (definedEntryReader != null) {
readWriteExtension.setEntryReader(
configEntry,
new TweedEntryReaderWriterImpls.NullableReader<>(definedEntryReader)
new TweedEntryReaderWriterImpls.FixedNullableReader<>(definedEntryReader)
);
}
val definedEntryWriter = readWriteExtension.getDefinedEntryWriter(configEntry);
if (definedEntryWriter != null) {
readWriteExtension.setEntryWriter(
configEntry,
new TweedEntryReaderWriterImpls.NullableWriter<>(definedEntryWriter)
new TweedEntryReaderWriterImpls.FixedNullableWriter<>(definedEntryWriter)
);
}
}

View File

@@ -1,5 +1,9 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
/**
* @see AutoNullableReadWriteBehavior
*/
@Deprecated
public enum AutoReadWriteNullability {
NON_NULL,
NULLABLE,

View File

@@ -4,6 +4,7 @@ import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
@@ -12,9 +13,11 @@ import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.DefaultWeavingExtensions;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.NullablePojoWeaver;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import lombok.Data;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -24,6 +27,7 @@ import java.util.List;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.write;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@NullUnmarked
class AutoReadWritePojoWeavingProcessorTest {
@@ -50,7 +54,11 @@ class AutoReadWritePojoWeavingProcessorTest {
assertReaderAndWriter(primitiveIntEntry, TweedEntryReaderWriterImpls.INT_READER_WRITER);
var boxedLongEntry = rootEntry.subEntries().get("boxedLong");
assertReaderAndWriter(boxedLongEntry, TweedEntryReaderWriterImpls.LONG_READER_WRITER);
assertReaderAndWriter(boxedLongEntry, TweedEntryReaderWriterImpls.NULLABLE_READER_WRITER);
assertThat(boxedLongEntry).asInstanceOf(type(NullableConfigEntry.class))
.satisfies(nullableEntry -> assertReaderAndWriter(
nullableEntry.nonNullEntry(), TweedEntryReaderWriterImpls.LONG_READER_WRITER
));
var stringEntry = rootEntry.subEntries().get("string");
assertReaderAndWriter(stringEntry, TweedEntryReaderWriterImpls.STRING_READER_WRITER);
@@ -115,6 +123,7 @@ class AutoReadWritePojoWeavingProcessorTest {
}
@PojoWeaving(extensions = ReadWriteExtension.class)
@PojoWeavingExtension(NullablePojoWeaver.class)
@DefaultWeavingExtensions
@PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class)
@DefaultReadWriteMappings
@@ -122,7 +131,7 @@ class AutoReadWritePojoWeavingProcessorTest {
@Data
public static class AnnotatedConfig {
private int primitiveInt;
private Long boxedLong;
private @Nullable Long boxedLong;
private String string;
private List<@CompoundWeaving Nested> nesteds;
}

View File

@@ -85,22 +85,22 @@ class AutoNullableReadWritePojoWeavingProcessorTest {
private void assertNullableReader(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryReader(entry))
.isInstanceOf(TweedEntryReaderWriterImpls.NullableReader.class);
.isInstanceOf(TweedEntryReaderWriterImpls.FixedNullableReader.class);
}
private void assertNonNullableReader(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryReader(entry))
.isNotInstanceOf(TweedEntryReaderWriterImpls.NullableReader.class);
.isNotInstanceOf(TweedEntryReaderWriterImpls.FixedNullableReader.class);
}
private void assertNullableWriter(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryWriter(entry))
.isInstanceOf(TweedEntryReaderWriterImpls.NullableWriter.class);
.isInstanceOf(TweedEntryReaderWriterImpls.FixedNullableWriter.class);
}
private void assertNonNullableWriter(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryWriter(entry))
.isNotInstanceOf(TweedEntryReaderWriterImpls.NullableWriter.class);
.isNotInstanceOf(TweedEntryReaderWriterImpls.FixedNullableWriter.class);
}
@SneakyThrows

View File

@@ -0,0 +1,66 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.entry.NullableConfigEntryImpl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import lombok.AllArgsConstructor;
import lombok.Setter;
import org.jspecify.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.util.Locale;
public class NullablePojoWeaver implements TweedPojoWeavingExtension {
private @Nullable PatchworkPartAccess<CustomData> customDataAccess;
@Override
public void setup(SetupContext context) {
customDataAccess = context.registerWeavingContextExtensionData(CustomData.class);
}
@Override
public <T> void beforeWeaveEntry(ActualType<T> valueType, Patchwork extensionsData, ProtoWeavingContext context) {
assert customDataAccess != null;
Boolean nullable = null;
for (Annotation annotation : valueType.getAnnotations()) {
String annotationName = annotation.annotationType().getSimpleName().toLowerCase(Locale.ROOT);
switch (annotationName) {
case "nullable":
nullable = true;
break;
case "nonnull":
case "notnull":
nullable = false;
break;
}
}
extensionsData.set(customDataAccess, new CustomData(Boolean.TRUE.equals(nullable)));
}
@Override
public @Nullable <T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
assert customDataAccess != null;
CustomData customData = context.extensionsData().get(customDataAccess);
if (customData == null || !customData.nullable) {
return null;
}
customData.nullable(false);
return new NullableConfigEntryImpl<>(
context.configContainer(),
valueType.declaredType(),
context.weavePseudoEntry(context, "nonNull", context.extensionsData())
);
}
@AllArgsConstructor
@Setter
private static class CustomData {
private boolean nullable;
}
}

View File

@@ -20,6 +20,8 @@ public class WeavingContext {
WeavingFn weavingFunction;
ConfigContainer<?> configContainer;
String[] path;
ActualType<?> valueType;
boolean isPseudo;
Patchwork extensionsData;
AnnotatedElement annotations;
@@ -27,7 +29,12 @@ public class WeavingContext {
return weavingFunction.weaveEntry(valueType, extensionsData, context);
}
public <T> ConfigEntry<T> weavePseudoEntry(WeavingContext parentContext, String pseudoEntryName, Patchwork extensionsData) {
return weavingFunction.weavePseudoEntry(parentContext, pseudoEntryName, extensionsData);
}
public interface WeavingFn {
<T> ConfigEntry<T> weaveEntry(ActualType<T> valueType, Patchwork extensionsData, ProtoWeavingContext context);
<T> ConfigEntry<T> weavePseudoEntry(WeavingContext parentContext, String pseudoEntryName, Patchwork extensionsData);
}
}

View File

@@ -79,7 +79,7 @@ public class StaticPojoCompoundConfigEntry<T> extends BaseConfigEntry<T> impleme
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, @Nullable T value) {
public void visitInOrder(ConfigEntryValueVisitor visitor, T value) {
if (visitor.enterStructuredEntry(this, value)) {
subEntries.forEach((key, entry) -> {
if (visitor.enterStructuredSubEntry(key, key)) {

View File

@@ -33,6 +33,21 @@ public class TweedPojoWeaverBootstrapper<T> {
private final AnnotatedElement pojoAnnotations;
private final ConfigContainer<T> configContainer;
private final Collection<TweedPojoWeavingExtension> weavingExtensions;
private final WeavingContext.WeavingFn weavingContextFn = new WeavingContext.WeavingFn() {
@Override
public <U> ConfigEntry<U> weaveEntry(
ActualType<U> valueType,
Patchwork extensionsData,
ProtoWeavingContext context
) {
return TweedPojoWeaverBootstrapper.this.weaveEntry(valueType, extensionsData, context);
}
@Override
public <U> ConfigEntry<U> weavePseudoEntry(WeavingContext parentContext, String pseudoEntryName, Patchwork extensionsData) {
return TweedPojoWeaverBootstrapper.this.weavePseudoEntry(parentContext, pseudoEntryName, extensionsData);
}
};
@Nullable
private PatchworkFactory weavingExtensionsPatchworkFactory;
@@ -119,18 +134,19 @@ public class TweedPojoWeaverBootstrapper<T> {
}
private <U> ConfigEntry<U> weaveEntry(
ActualType<U> dataClass,
ActualType<U> valueType,
Patchwork extensionsData,
ProtoWeavingContext protoContext
) {
extensionsData = extensionsData.copy();
runBeforeWeaveHooks(dataClass, extensionsData, protoContext);
runBeforeWeaveHooks(valueType, extensionsData, protoContext);
WeavingContext context = WeavingContext.builder()
.parent(protoContext.parent())
.weavingFunction(this::weaveEntry)
.weavingFunction(weavingContextFn)
.configContainer(configContainer)
.valueType(valueType)
.path(protoContext.path())
.extensionsData(extensionsData)
.annotations(new AnnotationInheritanceAwareAnnotatedElement(protoContext.annotations()))
@@ -138,17 +154,70 @@ public class TweedPojoWeaverBootstrapper<T> {
for (TweedPojoWeavingExtension weavingExtension : weavingExtensions) {
try {
ConfigEntry<U> configEntry = weavingExtension.weaveEntry(dataClass, context);
ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context);
if (configEntry != null) {
runAfterWeaveHooks(dataClass, configEntry, context);
runAfterWeaveHooks(valueType, configEntry, context);
return configEntry;
}
} catch (Exception e) {
log.error("Failed to run Tweed POJO weaver (" + weavingExtension.getClass().getName() + ")", e);
log.error(
"Failed to run Tweed POJO weaver (" + weavingExtension.getClass().getName() + ")"
+ " for entry at " + Arrays.toString(context.path()),
e
);
}
}
throw new PojoWeavingException("Failed to weave " + dataClass + ": No matching weavers found");
throw new PojoWeavingException(
"Failed to weave entry for " + valueType + " at " + Arrays.toString(context.path())
+ ": No matching weavers found"
);
}
private <U> ConfigEntry<U> weavePseudoEntry(
WeavingContext parentContext,
String pseudoEntryName,
Patchwork extensionsData
) {
extensionsData = extensionsData.copy();
//noinspection unchecked
ActualType<U> valueType = (ActualType<U>) parentContext.valueType();
String[] path = Arrays.copyOf(parentContext.path(), parentContext.path().length + 1);
path[path.length - 1] = ":" + pseudoEntryName;
WeavingContext context = WeavingContext.builder()
.parent(parentContext)
.weavingFunction(weavingContextFn)
.configContainer(configContainer)
.path(path)
.valueType(parentContext.valueType())
.isPseudo(true)
.extensionsData(extensionsData)
.annotations(parentContext.annotations())
.build();
for (TweedPojoWeavingExtension weavingExtension : weavingExtensions) {
try {
ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context);
if (configEntry != null) {
runAfterWeaveHooks(valueType, configEntry, context);
return configEntry;
}
} catch (Exception e) {
log.error(
"Failed to run Tweed POJO weaver (" + weavingExtension.getClass().getName() + ")"
+ " for pseudo entry at " + Arrays.toString(context.path()),
e
);
}
}
throw new PojoWeavingException(
"Failed to weave pseudo entry for " + valueType + " at " + Arrays.toString(context.path())
+ ": No matching weavers found"
);
}
private <U> void runBeforeWeaveHooks(

View File

@@ -7,10 +7,11 @@ import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import org.jspecify.annotations.NullUnmarked;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isCompoundEntryForClassWith;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
@@ -18,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@SuppressWarnings("unused")
@NullUnmarked
class CompoundPojoWeaverTest {
@Test
@@ -30,8 +30,8 @@ class CompoundPojoWeaverTest {
PatchworkFactory weavingContextExtensionDataFactory = weavingContextExtensionDataFactoryBuilder.build();
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(new WeavingContext.WeavingFn() {
AtomicReference<WeavingContext.@Nullable WeavingFn> weavingFunction = new AtomicReference<>();
weavingFunction.set(new WeavingContext.WeavingFn() {
@Override
public <T> ConfigEntry<T> weaveEntry(
ActualType<T> valueType,
@@ -43,14 +43,27 @@ class CompoundPojoWeaverTest {
.annotations(valueType)
.extensionsData(extensionsData.copy())
.path(protoContext.path())
.weavingFunction(protoContext.parent()::weaveEntry)
.weavingFunction(Objects.requireNonNull(weavingFunction.get()))
.build();
return Objects.requireNonNullElseGet(
compoundWeaver.weaveEntry(valueType, context),
() -> new SimpleConfigEntryImpl<>(context.configContainer(), valueType.declaredType())
);
}
})
@Override
public <T> ConfigEntry<T> weavePseudoEntry(
WeavingContext parentContext,
String pseudoEntryName,
Patchwork extensionsData
) {
assert false;
return null;
}
});
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(Objects.requireNonNull(weavingFunction.get()))
.extensionsData(weavingContextExtensionDataFactory.create())
.annotations(Compound.class)
.path(new String[0])