[weaver-pojo-serde-extension] Implement auto serde for POJO weaving

This commit is contained in:
2025-06-28 23:30:14 +02:00
parent 4fc8cb4ac7
commit 25faea92d8
19 changed files with 909 additions and 128 deletions

View File

@@ -96,6 +96,9 @@ public interface ReadWriteExtension extends TweedExtension {
}; };
} }
<T, C extends ConfigEntry<T>> @Nullable TweedEntryReader<T, C> getDefinedEntryReader(ConfigEntry<T> entry);
<T, C extends ConfigEntry<T>> @Nullable TweedEntryWriter<T, C> getDefinedEntryWriter(ConfigEntry<T> entry);
<T, C extends ConfigEntry<T>> void setEntryReaderWriter( <T, C extends ConfigEntry<T>> void setEntryReaderWriter(
ConfigEntry<T> entry, ConfigEntry<T> entry,
TweedEntryReader<T, C> entryReader, TweedEntryReader<T, C> entryReader,

View File

@@ -18,7 +18,6 @@ import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess; import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Data; import lombok.Data;
import lombok.val; import lombok.val;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
@@ -78,6 +77,26 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
entryWriterMiddlewareContainer.seal(); entryWriterMiddlewareContainer.seal();
} }
@Override
public @Nullable <T, C extends ConfigEntry<T>> TweedEntryReader<T, C> getDefinedEntryReader(ConfigEntry<T> entry) {
CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
return null;
}
//noinspection unchecked
return (TweedEntryReader<T, C>) customEntryData.readerDefinition();
}
@Override
public @Nullable <T, C extends ConfigEntry<T>> TweedEntryWriter<T, C> getDefinedEntryWriter(ConfigEntry<T> entry) {
CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess);
if (customEntryData == null) {
return null;
}
//noinspection unchecked
return (TweedEntryWriter<T, C>) customEntryData.writerDefinition();
}
@Override @Override
public <T, C extends ConfigEntry<T>> void setEntryReaderWriter( public <T, C extends ConfigEntry<T>> void setEntryReaderWriter(
ConfigEntry<T> entry, ConfigEntry<T> entry,

View File

@@ -6,5 +6,6 @@ dependencies {
api(project(":tweed5-weaver-pojo")) api(project(":tweed5-weaver-pojo"))
api(project(":tweed5-serde-extension")) api(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-default-extensions"))
testImplementation(project(":tweed5-serde-hjson")) testImplementation(project(":tweed5-serde-hjson"))
} }

View File

@@ -11,19 +11,16 @@ import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension; import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext; import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec; import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.ReaderWriterLoader;
import lombok.extern.apachecommons.CommonsLog; import lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*; import java.util.*;
@CommonsLog @CommonsLog
public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension { public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension {
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>>> readerFactories = new HashMap<>();
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>>> writerFactories = new HashMap<>();
private final ReadWriteExtension readWriteExtension; private final ReadWriteExtension readWriteExtension;
private final ReaderWriterLoader readerWriterLoader = new ReaderWriterLoader();
public ReadWritePojoWeavingProcessor(ConfigContainer<?> configContainer) { public ReadWritePojoWeavingProcessor(ConfigContainer<?> configContainer) {
this.readWriteExtension = configContainer.extension(ReadWriteExtension.class) this.readWriteExtension = configContainer.extension(ReadWriteExtension.class)
@@ -40,38 +37,7 @@ public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension
private void loadProviders() { private void loadProviders() {
ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class); ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class);
serviceLoader.forEach(readerWriterLoader::load);
for (TweedReaderWriterProvider readerWriterProvider : serviceLoader) {
TweedReaderWriterProvider.ProviderContext providerContext = new TweedReaderWriterProvider.ProviderContext() {
@Override
public void registerReaderFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>> readerFactory
) {
if (readerFactories.putIfAbsent(id, readerFactory) != null && log.isWarnEnabled()) {
log.warn(
"Found duplicate Tweed entry reader id \"" + id + "\" in provider class "
+ readerWriterProvider.getClass().getName()
);
}
}
@Override
public void registerWriterFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>> writerFactory
) {
if (writerFactories.putIfAbsent(id, writerFactory) != null && log.isWarnEnabled()) {
log.warn(
"Found duplicate Tweed entry writer id \"" + id + "\" in provider class {}"
+ readerWriterProvider.getClass().getName()
);
}
}
};
readerWriterProvider.provideReaderWriters(providerContext);
}
} }
@Override @Override
@@ -81,98 +47,54 @@ public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension
return; return;
} }
try {
//noinspection rawtypes,unchecked //noinspection rawtypes,unchecked
readWriteExtension.setEntryReaderWriter( readWriteExtension.setEntryReaderWriter(
(ConfigEntry) configEntry, (ConfigEntry) configEntry,
(TweedEntryReader) resolveReader(entryConfig, context), (TweedEntryReader) resolveReader(entryConfig),
(TweedEntryWriter) resolveWriter(entryConfig, context) (TweedEntryWriter) resolveWriter(entryConfig)
);
} catch (Exception e) {
log.warn(
"Unexpected exception while resolving serde reader and writer for "
+ Arrays.toString(context.path())
+ ". Entry will not be included in serde.",
e
); );
} }
}
private TweedEntryReader<?, ?> resolveReader(EntryReadWriteConfig entryConfig, WeavingContext context) { private TweedEntryReader<?, ?> resolveReader(EntryReadWriteConfig entryConfig) {
String specText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader(); String specText = entryConfig.reader().isEmpty() ? entryConfig.value() : entryConfig.reader();
SerdePojoReaderWriterSpec spec = specFromText(specText, context); SerdePojoReaderWriterSpec spec = specFromText(specText);
if (spec == null) {
//noinspection unchecked,rawtypes return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
return Optional.ofNullable(spec)
.map(s -> resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, s, context))
.orElse(((TweedEntryReader) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
} }
private TweedEntryWriter<?, ?> resolveWriter(EntryReadWriteConfig entryConfig, WeavingContext context) { return readerWriterLoader.resolveReaderFromSpec(spec);
}
private TweedEntryWriter<?, ?> resolveWriter(EntryReadWriteConfig entryConfig) {
String specText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer(); String specText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer();
SerdePojoReaderWriterSpec spec = specFromText(specText, context); SerdePojoReaderWriterSpec spec = specFromText(specText);
if (spec == null) {
//noinspection unchecked,rawtypes return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
return Optional.ofNullable(spec)
.map(s -> resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, s, context))
.orElse(((TweedEntryWriter) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
} }
private @Nullable SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) { return readerWriterLoader.resolveWriterFromSpec(spec);
}
private @Nullable SerdePojoReaderWriterSpec specFromText(String specText) {
if (specText.isEmpty()) { if (specText.isEmpty()) {
return null; return null;
} }
try { try {
return SerdePojoReaderWriterSpec.parse(specText); return SerdePojoReaderWriterSpec.parse(specText);
} catch (SerdePojoReaderWriterSpec.ParseException e) { } catch (SerdePojoReaderWriterSpec.ParseException e) {
log.warn( throw new IllegalArgumentException(
"Failed to parse definition for reader or writer on entry " "Failed to parse definition for reader or writer: \"" + specText + "\"",
+ Arrays.toString(context.path())
+ ", entry will not be included in serde", e
);
return null;
}
}
private <T> @Nullable T resolveReaderWriterFromSpec(
Class<T> baseClass,
Map<String, TweedReaderWriterProvider.ReaderWriterFactory<T>> factories,
SerdePojoReaderWriterSpec spec,
WeavingContext context
) {
//noinspection unchecked
T[] arguments = spec.arguments()
.stream()
.map(argSpec -> resolveReaderWriterFromSpec(baseClass, factories, argSpec, context))
.toArray(length -> (T[]) Array.newInstance(baseClass, length));
TweedReaderWriterProvider.ReaderWriterFactory<T> factory = factories.get(spec.identifier());
T instance;
try {
if (factory != null) {
instance = factory.create(arguments);
} else {
instance = loadClassIfExists(baseClass, spec.identifier(), arguments);
}
} catch (Exception e) {
log.warn(
"Failed to resolve reader or writer factory \"" + spec.identifier() + "\" for entry "
+ Arrays.toString(context.path()) + ", entry will not be included in serde",
e e
); );
return null;
}
return instance;
}
private <T> @Nullable T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) {
try {
Class<?> clazz = Class.forName(className);
Class<?>[] argClasses = new Class<?>[arguments.length];
Arrays.fill(argClasses, baseClass);
Constructor<?> constructor = clazz.getConstructor(argClasses);
//noinspection unchecked
return (T) constructor.newInstance((Object[]) arguments);
} catch (ClassNotFoundException e) {
return null;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
log.warn("Failed to instantiate class " + className, e);
return null;
} }
} }
} }

View File

@@ -0,0 +1,16 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Repeatable(AutoReadWriteMappings.class)
public @interface AutoReadWriteMapping {
Class<? extends ConfigEntry>[] entryClasses() default { ConfigEntry.class };
Class<?>[] valueClasses();
String spec() default "";
String reader() default "";
String writer() default "";
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface AutoReadWriteMappings {
AutoReadWriteMapping[] value();
}

View File

@@ -0,0 +1,191 @@
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.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.impl.TweedEntryReaderWriterImpls;
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.weaver.pojo.api.weaving.ProtoWeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.ReaderWriterLoader;
import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec;
import lombok.Value;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
import java.util.*;
public class AutoReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension {
private final ReadWriteExtension readWriteExtension;
private final ReaderWriterLoader readerWriterLoader = new ReaderWriterLoader();
private @Nullable PatchworkPartAccess<CustomData> customDataAccess;
@ApiStatus.Internal
public AutoReadWritePojoWeavingProcessor(ConfigContainer<?> configContainer) {
this.readWriteExtension = configContainer.extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException(
"You must register a " + ReadWriteExtension.class.getSimpleName()
+ " to use the " + getClass().getSimpleName()
));
}
@Override
public void setup(SetupContext context) {
customDataAccess = context.registerWeavingContextExtensionData(CustomData.class);
loadProviders();
}
private void loadProviders() {
ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class);
serviceLoader.forEach(readerWriterLoader::load);
}
@Override
public <T> void beforeWeaveEntry(ActualType<T> valueType, Patchwork extensionsData, ProtoWeavingContext context) {
assert customDataAccess != null;
CustomData existingCustomData = extensionsData.get(customDataAccess);
List<Mapping> existingMappings = existingCustomData == null ? Collections.emptyList() : existingCustomData.mappings();
AutoReadWriteMapping[] mappingAnnotations = context.annotations()
.getAnnotationsByType(AutoReadWriteMapping.class);
if (existingCustomData == null || mappingAnnotations.length > 0) {
List<Mapping> mappings;
if (existingMappings.isEmpty() && mappingAnnotations.length == 0) {
mappings = Collections.emptyList();
} else {
mappings = new ArrayList<>(existingMappings.size() + mappingAnnotations.length + 5);
mappings.addAll(existingMappings);
for (AutoReadWriteMapping mappingAnnotation : mappingAnnotations) {
mappings.add(annotationToMapping(mappingAnnotation));
}
}
extensionsData.set(customDataAccess, new CustomData(mappings));
}
}
private Mapping annotationToMapping(AutoReadWriteMapping annotation) {
return new Mapping(
annotation.entryClasses(),
annotation.valueClasses(),
resolveReader(annotation.reader().isEmpty() ? annotation.spec() : annotation.reader()),
resolveWriter(annotation.writer().isEmpty() ? annotation.spec() : annotation.writer())
);
}
private TweedEntryReader<?, ?> resolveReader(String specText) {
if (specText.isEmpty()) {
return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
}
try {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse(specText);
return readerWriterLoader.resolveReaderFromSpec(spec);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to parse definition for reader: \"" + specText + "\"",
e
);
}
}
private TweedEntryWriter<?, ?> resolveWriter(String specText) {
if (specText.isEmpty()) {
return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
}
try {
SerdePojoReaderWriterSpec spec = SerdePojoReaderWriterSpec.parse(specText);
return readerWriterLoader.resolveWriterFromSpec(spec);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to parse definition for writer: \"" + specText + "\"",
e
);
}
}
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
assert customDataAccess != null;
CustomData customData = context.extensionsData().get(customDataAccess);
if (customData == null || customData.mappings().isEmpty()) {
return;
}
Mapping mapping = determineMapping(customData, configEntry, valueType);
if (mapping == null) {
return;
}
//noinspection unchecked
readWriteExtension.setEntryReaderWriter(
(ConfigEntry<Object>) configEntry,
(TweedEntryReader<Object, @org.jspecify.annotations.NonNull ConfigEntry<Object>>) mapping.reader(),
(TweedEntryWriter<Object, @org.jspecify.annotations.NonNull ConfigEntry<Object>>) mapping.writer()
);
}
private @Nullable Mapping determineMapping(CustomData customData, ConfigEntry<?> configEntry, ActualType<?> valueType) {
Mapping strictMapping = customData.strictMappings()
.get(new MappingStrictKey(configEntry.getClass(), valueType.declaredType()));
if (strictMapping != null) {
return strictMapping;
}
for (int i = customData.mappings().size() - 1; i >= 0; i--) {
Mapping mapping = customData.mappings().get(i);
if (mappingMatches(mapping, configEntry, valueType)) {
customData.strictMappings.put(
new MappingStrictKey(configEntry.getClass(), valueType.declaredType()),
mapping
);
return mapping;
}
}
return null;
}
private boolean mappingMatches(Mapping mapping, ConfigEntry<?> configEntry, ActualType<?> valueType) {
return anyClassMatches(mapping.entryClasses, configEntry.getClass())
&& anyClassMatches(mapping.valueClasses, valueType.declaredType());
}
private boolean anyClassMatches(Class<?>[] haystack, Class<?> needle) {
for (Class<?> hay : haystack) {
if (hay.isAssignableFrom(needle)) {
return true;
}
}
return false;
}
@Value
private static class CustomData {
List<Mapping> mappings;
Map<MappingStrictKey, Mapping> strictMappings = new HashMap<>();
}
@Value
private static class Mapping {
Class<?>[] entryClasses;
Class<?>[] valueClasses;
TweedEntryReader<?, ?> reader;
TweedEntryWriter<?, ?> writer;
}
@Value
private static class MappingStrictKey {
Class<?> entryClass;
Class<?> valueClass;
}
}

View File

@@ -0,0 +1,35 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
import de.siphalor.tweed5.annotationinheritance.api.AnnotationInheritance;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collection;
@AnnotationInheritance(passOn = AutoReadWriteMapping.class)
@AutoReadWriteMapping(valueClasses = {boolean.class, Boolean.class}, spec = "tweed5.bool")
@AutoReadWriteMapping(valueClasses = {byte.class, Byte.class}, spec = "tweed5.byte")
@AutoReadWriteMapping(valueClasses = {short.class, Short.class}, spec = "tweed5.short")
@AutoReadWriteMapping(valueClasses = {int.class, Integer.class}, spec = "tweed5.integer")
@AutoReadWriteMapping(valueClasses = {long.class, Long.class}, spec = "tweed5.long")
@AutoReadWriteMapping(valueClasses = {float.class, Float.class}, spec = "tweed5.float")
@AutoReadWriteMapping(valueClasses = {double.class, Double.class}, spec = "tweed5.double")
@AutoReadWriteMapping(valueClasses = String.class, spec = "tweed5.string")
@AutoReadWriteMapping(
entryClasses = CollectionConfigEntry.class,
valueClasses = Collection.class,
spec = "tweed5.collection"
)
@AutoReadWriteMapping(
entryClasses = CompoundConfigEntry.class,
valueClasses = Object.class,
spec = "tweed5.compound"
)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface DefaultReadWriteMappings {
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface AutoNullableReadWriteBehavior {
AutoReadWriteNullability defaultNullability();
}

View File

@@ -0,0 +1,119 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
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.weaver.pojo.api.weaving.ProtoWeavingContext;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import lombok.Value;
import lombok.var;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
import java.lang.annotation.Annotation;
public class AutoNullableReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension {
private final ReadWriteExtension readWriteExtension;
private @Nullable PatchworkPartAccess<CustomData> customDataAccess;
@ApiStatus.Internal
public AutoNullableReadWritePojoWeavingProcessor(ConfigContainer<?> configContainer) {
readWriteExtension = configContainer.extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException(
"You must register a " + ReadWriteExtension.class.getSimpleName()
+ " to use the " + getClass().getSimpleName()
));
}
@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;
AutoReadWriteNullability innerNullability = null;
var behavior = context.annotations().getAnnotation(AutoNullableReadWriteBehavior.class);
if (behavior != null) {
innerNullability = behavior.defaultNullability();
}
AutoReadWriteNullability currentNullability = null;
CustomData customData = extensionsData.get(customDataAccess);
if (customData != null) {
if (customData.innerDefaultNullability() != null) {
extensionsData.set(customDataAccess, new CustomData(
customData.innerDefaultNullability(),
innerNullability
));
return;
}
currentNullability = customData.defaultNullability();
}
if (innerNullability != null) {
extensionsData.set(customDataAccess, new CustomData(currentNullability, innerNullability));
}
}
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
if (getNullability(valueType, context) == AutoReadWriteNullability.NULLABLE) {
var definedEntryReader = readWriteExtension.getDefinedEntryReader(configEntry);
if (definedEntryReader != null) {
readWriteExtension.setEntryReader(
configEntry,
new TweedEntryReaderWriterImpls.NullableReader<>(definedEntryReader)
);
}
var definedEntryWriter = readWriteExtension.getDefinedEntryWriter(configEntry);
if (definedEntryWriter != null) {
readWriteExtension.setEntryWriter(
configEntry,
new TweedEntryReaderWriterImpls.NullableWriter<>(definedEntryWriter)
);
}
}
}
private <T> AutoReadWriteNullability getNullability(ActualType<T> valueType, WeavingContext context) {
if (valueType.declaredType().isPrimitive()) {
return AutoReadWriteNullability.NON_NULL;
}
Annotation[] annotations = context.annotations().getAnnotations();
for (int i = annotations.length - 1; i >= 0; i--) {
String typeName = annotations[i].annotationType().getSimpleName();
if ("nullable".equalsIgnoreCase(typeName)) {
return AutoReadWriteNullability.NULLABLE;
} else if ("nonnull".equalsIgnoreCase(typeName) || "notnull".equalsIgnoreCase(typeName)) {
return AutoReadWriteNullability.NON_NULL;
}
}
return getDefaultNullability(context.extensionsData());
}
private AutoReadWriteNullability getDefaultNullability(Patchwork extensionsData) {
assert customDataAccess != null;
CustomData customData = extensionsData.get(customDataAccess);
if (customData != null && customData.defaultNullability() != null) {
return customData.defaultNullability();
}
return AutoReadWriteNullability.NON_NULL;
}
@Value
private static class CustomData {
@Nullable AutoReadWriteNullability defaultNullability;
@Nullable AutoReadWriteNullability innerDefaultNullability;
}
}

View File

@@ -0,0 +1,6 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
public enum AutoReadWriteNullability {
NON_NULL,
NULLABLE,
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,136 @@
package de.siphalor.tweed5.weaver.pojoext.serde.impl;
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.impl.TweedEntryReaderWriterImpls;
import lombok.Getter;
import lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@CommonsLog
public class ReaderWriterLoader {
@Getter
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>>> readerFactories
= new HashMap<>();
@Getter
private final Map<String, TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>>> writerFactories
= new HashMap<>();
private final TweedReaderWriterProvider.ProviderContext providerContext = new ProviderContext();
public void load(TweedReaderWriterProvider provider) {
try {
provider.provideReaderWriters(providerContext);
} catch (Exception e) {
log.warn(
"Unexpected exception while providing reader and writer factories using "
+ provider.getClass().getName(),
e
);
}
}
public TweedEntryReader<?, ?> resolveReaderFromSpec(SerdePojoReaderWriterSpec spec) {
// noinspection unchecked
TweedEntryReader<?, ?> reader = resolveReaderWriterFromSpec(
(Class<TweedEntryReader<?, ?>>) (Object) TweedEntryReader.class,
readerFactories(),
spec
);
if (reader != null) {
return reader;
}
return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
}
public TweedEntryWriter<?, ?> resolveWriterFromSpec(SerdePojoReaderWriterSpec spec) {
//noinspection unchecked
TweedEntryWriter<?, ?> writer = resolveReaderWriterFromSpec(
(Class<TweedEntryWriter<?,?>>) (Object) TweedEntryWriter.class,
writerFactories(),
spec
);
if (writer != null) {
return writer;
}
return TweedEntryReaderWriterImpls.NOOP_READER_WRITER;
}
private <T> @Nullable T resolveReaderWriterFromSpec(
Class<T> baseClass,
Map<String, TweedReaderWriterProvider.ReaderWriterFactory<T>> factories,
SerdePojoReaderWriterSpec spec
) {
//noinspection unchecked
T[] arguments = spec.arguments()
.stream()
.map(argSpec -> resolveReaderWriterFromSpec(baseClass, factories, argSpec))
.toArray(length -> (T[]) Array.newInstance(baseClass, length));
TweedReaderWriterProvider.ReaderWriterFactory<T> factory = factories.get(spec.identifier());
T instance;
try {
if (factory != null) {
instance = factory.create(arguments);
} else {
instance = loadClassIfExists(baseClass, spec.identifier(), arguments);
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to resolve reader or writer factory from \"" + spec.identifier() + "\"",
e
);
}
return instance;
}
private <T> @Nullable T loadClassIfExists(Class<T> baseClass, String className, T[] arguments) {
try {
Class<?> clazz = Class.forName(className);
Class<?>[] argClasses = new Class<?>[arguments.length];
Arrays.fill(argClasses, baseClass);
Constructor<?> constructor = clazz.getConstructor(argClasses);
//noinspection unchecked
return (T) constructor.newInstance((Object[]) arguments);
} catch (ClassNotFoundException e) {
return null;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
log.warn("Failed to instantiate class " + className, e);
return null;
}
}
private class ProviderContext implements TweedReaderWriterProvider.ProviderContext {
@Override
public void registerReaderFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryReader<?, ?>> readerFactory
) {
if (readerFactories.putIfAbsent(id, readerFactory) != null) {
throw new IllegalArgumentException("Found duplicate Tweed entry reader id \"" + id + "\"");
}
}
@Override
public void registerWriterFactory(
String id,
TweedReaderWriterProvider.ReaderWriterFactory<TweedEntryWriter<?, ?>> writerFactory
) {
if (writerFactories.putIfAbsent(id, writerFactory) != null) {
throw new IllegalArgumentException("Found duplicate Tweed entry reader id \"" + id + "\"");
}
}
}
}

View File

@@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.read;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.write;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class ReadWritePojoWeavingProcessorTest { class ReadWritePojoWeavingProcessorTest {
@@ -34,18 +36,11 @@ class ReadWritePojoWeavingProcessorTest {
ConfigContainer<AnnotatedConfig> configContainer = weaverBootstrapper.weave(); ConfigContainer<AnnotatedConfig> configContainer = weaverBootstrapper.weave();
configContainer.initialize(); configContainer.initialize();
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class).orElseThrow();
AnnotatedConfig config = new AnnotatedConfig(123, "test", new TestClass(987)); AnnotatedConfig config = new AnnotatedConfig(123, "test", new TestClass(987));
StringWriter stringWriter = new StringWriter(); StringWriter stringWriter = new StringWriter();
HjsonWriter hjsonWriter = new HjsonWriter(stringWriter, new HjsonWriter.Options()); HjsonWriter hjsonWriter = new HjsonWriter(stringWriter, new HjsonWriter.Options());
readWriteExtension.write( configContainer.rootEntry().apply(write(hjsonWriter, config));
hjsonWriter,
config,
configContainer.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
);
assertThat(stringWriter).hasToString(""" assertThat(stringWriter).hasToString("""
{ {
@@ -62,11 +57,8 @@ class ReadWritePojoWeavingProcessorTest {
\ttest: { inner: 29 } \ttest: { inner: 29 }
}""" }"""
))); )));
assertThat(readWriteExtension.read( assertThat(configContainer.rootEntry().call(read(reader)))
reader, .isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
configContainer.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
)).isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
} }
@AutoService(TweedReaderWriterProvider.class) @AutoService(TweedReaderWriterProvider.class)
@@ -89,7 +81,7 @@ class ReadWritePojoWeavingProcessorTest {
@PojoWeaving(extensions = ReadWriteExtension.class) @PojoWeaving(extensions = ReadWriteExtension.class)
@DefaultWeavingExtensions @DefaultWeavingExtensions
@PojoWeavingExtension(de.siphalor.tweed5.weaver.pojoext.serde.api.ReadWritePojoWeavingProcessor.class) @PojoWeavingExtension(ReadWritePojoWeavingProcessor.class)
@CompoundWeaving @CompoundWeaving
@EntryReadWriteConfig("tweed5.compound") @EntryReadWriteConfig("tweed5.compound")
// lombok // lombok

View File

@@ -0,0 +1,134 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
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.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriter;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
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.impl.weaving.TweedPojoWeaverBootstrapper;
import lombok.Data;
import org.jspecify.annotations.NullUnmarked;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.write;
import static org.assertj.core.api.Assertions.assertThat;
@NullUnmarked
class AutoReadWritePojoWeavingProcessorTest {
private ConfigContainer<AnnotatedConfig> container;
private ReadWriteExtension readWriteExtension;
@BeforeEach
void setUp() {
var bootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class);
container = bootstrapper.weave();
container.initialize();
readWriteExtension = container.extension(ReadWriteExtension.class).orElseThrow();
}
@SuppressWarnings("unchecked")
@Test
void testConfiguration() {
var rootEntry = (CompoundConfigEntry<AnnotatedConfig>) container.rootEntry();
assertReaderAndWriter(rootEntry, TweedEntryReaderWriterImpls.COMPOUND_READER_WRITER);
var primitiveIntEntry = rootEntry.subEntries().get("primitiveInt");
assertReaderAndWriter(primitiveIntEntry, TweedEntryReaderWriterImpls.INT_READER_WRITER);
var boxedLongEntry = rootEntry.subEntries().get("boxedLong");
assertReaderAndWriter(boxedLongEntry, TweedEntryReaderWriterImpls.LONG_READER_WRITER);
var stringEntry = rootEntry.subEntries().get("string");
assertReaderAndWriter(stringEntry, TweedEntryReaderWriterImpls.STRING_READER_WRITER);
var nestedsEntry = (CollectionConfigEntry<Nested, List<Nested>>) rootEntry.subEntries().get("nesteds");
assertReaderAndWriter(nestedsEntry, TweedEntryReaderWriterImpls.COLLECTION_READER_WRITER);
var nestedEntry = (CompoundConfigEntry<Nested>) nestedsEntry.elementEntry();
assertReaderAndWriter(nestedEntry, TweedEntryReaderWriterImpls.COMPOUND_READER_WRITER);
var nestedValueEntry = nestedEntry.subEntries().get("value");
assertReaderAndWriter(nestedValueEntry, TweedEntryReaderWriterImpls.BOOLEAN_READER_WRITER);
}
private void assertReaderAndWriter(ConfigEntry<?> entry, TweedEntryReaderWriter<?, ?> readerWriter) {
assertThat(readWriteExtension.getDefinedEntryReader(entry)).isSameAs(readerWriter);
assertThat(readWriteExtension.getDefinedEntryWriter(entry)).isSameAs(readerWriter);
}
@Test
void testUsage() {
var bootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class);
var container = bootstrapper.weave();
container.initialize();
AnnotatedConfig config = new AnnotatedConfig()
.primitiveInt(123)
.boxedLong(456L)
.string("test")
.nesteds(Arrays.asList(
new Nested().value(true),
new Nested().value(false),
new Nested().value(true)
));
StringWriter writer = new StringWriter();
container.rootEntry().apply(write(
new HjsonWriter(writer, new HjsonWriter.Options()),
config
));
assertThat(writer.toString()).isEqualTo("""
{
\tprimitiveInt: 123
\tboxedLong: 456
\tstring: test
\tnesteds: [
\t\t{
\t\t\tvalue: true
\t\t}
\t\t{
\t\t\tvalue: false
\t\t}
\t\t{
\t\t\tvalue: true
\t\t}
\t]
}
""");
}
@PojoWeaving(extensions = ReadWriteExtension.class)
@DefaultWeavingExtensions
@PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class)
@DefaultReadWriteMappings
@CompoundWeaving
@Data
public static class AnnotatedConfig {
private int primitiveInt;
private Long boxedLong;
private String string;
private List<@CompoundWeaving Nested> nesteds;
}
@Data
public static class Nested {
boolean value;
}
}

View File

@@ -0,0 +1,172 @@
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriteException;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
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.impl.weaving.TweedPojoWeaverBootstrapper;
import de.siphalor.tweed5.weaver.pojoext.serde.api.ReadWritePojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.AutoReadWritePojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.DefaultReadWriteMappings;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@NullUnmarked
class AutoNullableReadWritePojoWeavingProcessorTest {
private ConfigContainer<AnnotatedConfig> container;
private ReadWriteExtension readWriteExtension;
@BeforeEach
void setUp() {
var bootstrapper = TweedPojoWeaverBootstrapper.create(AnnotatedConfig.class);
container = bootstrapper.weave();
container.initialize();
readWriteExtension = container.extension(ReadWriteExtension.class).orElseThrow();
}
@SuppressWarnings("unchecked")
@Test
void testConfiguration() {
var rootEntry = (CompoundConfigEntry<AnnotatedConfig>) container.rootEntry();
assertNonNullableReaderWriter(rootEntry);
var primitiveIntEntry = rootEntry.subEntries().get("primitiveInt");
assertNonNullableReaderWriter(primitiveIntEntry);
var boxedIntegerEntry = rootEntry.subEntries().get("boxedInteger");
assertNonNullableReaderWriter(boxedIntegerEntry);
var nullableBoxedIntegerEntry = rootEntry.subEntries().get("nullableBoxedInteger");
assertNullableReaderWriter(nullableBoxedIntegerEntry);
var nestedEntry = (CompoundConfigEntry<Nested>) rootEntry.subEntries().get("nested");
assertNonNullableReaderWriter(nestedEntry);
var nestedPrimitiveIntEntry = nestedEntry.subEntries().get("primitiveInt");
assertNonNullableReaderWriter(nestedPrimitiveIntEntry);
var nestedBoxedIntegerEntry = nestedEntry.subEntries().get("boxedInteger");
assertNullableReaderWriter(nestedBoxedIntegerEntry);
var nestedNonNullBoxedIntegerEntry = nestedEntry.subEntries().get("nonNullBoxedInteger");
assertNonNullableReaderWriter(nestedNonNullBoxedIntegerEntry);
}
private void assertNullableReaderWriter(ConfigEntry<?> entry) {
assertNullableReader(entry);
assertNullableWriter(entry);
}
private void assertNonNullableReaderWriter(ConfigEntry<?> entry) {
assertNonNullableReader(entry);
assertNonNullableWriter(entry);
}
private void assertNullableReader(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryReader(entry))
.isInstanceOf(TweedEntryReaderWriterImpls.NullableReader.class);
}
private void assertNonNullableReader(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryReader(entry))
.isNotInstanceOf(TweedEntryReaderWriterImpls.NullableReader.class);
}
private void assertNullableWriter(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryWriter(entry))
.isInstanceOf(TweedEntryReaderWriterImpls.NullableWriter.class);
}
private void assertNonNullableWriter(ConfigEntry<?> entry) {
assertThat(readWriteExtension.getDefinedEntryWriter(entry))
.isNotInstanceOf(TweedEntryReaderWriterImpls.NullableWriter.class);
}
@SneakyThrows
@Test
void testUsage() {
AnnotatedConfig config = new AnnotatedConfig()
.primitiveInt(123)
.boxedInteger(456)
.nullableBoxedInteger(null)
.nested(new Nested().primitiveInt(789).boxedInteger(null).nonNullBoxedInteger(654));
StringWriter writer = new StringWriter();
readWriteExtension.write(
new HjsonWriter(writer, new HjsonWriter.Options()),
config,
container.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
);
assertThat(writer.toString()).isEqualTo("""
{
\tprimitiveInt: 123
\tboxedInteger: 456
\tnullableBoxedInteger: null
\tnested: {
\t\tprimitiveInt: 789
\t\tboxedInteger: null
\t\tnonNullBoxedInteger: 654
\t}
}
""");
config.boxedInteger(null);
assertThatThrownBy(() -> readWriteExtension.write(
new HjsonWriter(new StringWriter(), new HjsonWriter.Options()),
config,
container.rootEntry(),
readWriteExtension.createReadWriteContextExtensionsData()
)).isInstanceOf(TweedEntryWriteException.class)
.hasMessageContaining("at .boxedInteger");
}
@PojoWeaving(extensions = {ReadWriteExtension.class, PatherExtension.class})
@DefaultWeavingExtensions
@PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class)
@PojoWeavingExtension(ReadWritePojoWeavingProcessor.class)
@PojoWeavingExtension(AutoNullableReadWritePojoWeavingProcessor.class)
@DefaultReadWriteMappings
@CompoundWeaving
@Data
@NoArgsConstructor
public static class AnnotatedConfig {
private int primitiveInt;
private Integer boxedInteger;
private @Nullable Integer nullableBoxedInteger;
private Nested nested;
}
@CompoundWeaving
@AutoNullableReadWriteBehavior(defaultNullability = AutoReadWriteNullability.NULLABLE)
@Data
@NoArgsConstructor
public static class Nested {
private int primitiveInt;
private Integer boxedInteger;
private @NonNull Integer nonNullBoxedInteger;
}
}

View File

@@ -1,6 +1,7 @@
package de.siphalor.tweed5.weaver.pojo.api.annotation; package de.siphalor.tweed5.weaver.pojo.api.annotation;
import de.siphalor.tweed5.annotationinheritance.api.AnnotationInheritance; import de.siphalor.tweed5.annotationinheritance.api.AnnotationInheritance;
import de.siphalor.tweed5.weaver.pojo.api.weaving.CollectionPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.CompoundPojoWeaver; import de.siphalor.tweed5.weaver.pojo.api.weaving.CompoundPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver; import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver;
@@ -11,6 +12,7 @@ import java.lang.annotation.Target;
@AnnotationInheritance(passOn = PojoWeavingExtension.class) @AnnotationInheritance(passOn = PojoWeavingExtension.class)
@PojoWeavingExtension(CompoundPojoWeaver.class) @PojoWeavingExtension(CompoundPojoWeaver.class)
@PojoWeavingExtension(CollectionPojoWeaver.class)
@PojoWeavingExtension(TrivialPojoWeaver.class) @PojoWeavingExtension(TrivialPojoWeaver.class)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})

View File

@@ -28,7 +28,8 @@ class TweedPojoWeaverBootstrapperTest {
.hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class)) .hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class))
.hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.class)) .hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.class))
.hasEntrySatisfying("value", isSimpleEntryForClass(InnerValue.class)) .hasEntrySatisfying("value", isSimpleEntryForClass(InnerValue.class))
.hasEntrySatisfying("list", isSimpleEntryForClass(List.class)) .hasEntrySatisfying("list", isCollectionEntryForClass(List.class, list ->
assertThat(list.elementEntry()).satisfies(isSimpleEntryForClass(Integer.class))))
.hasEntrySatisfying("compound", isCompoundEntryForClassWith(InnerCompound.class, innerCompound -> .hasEntrySatisfying("compound", isCompoundEntryForClassWith(InnerCompound.class, innerCompound ->
assertThat(innerCompound.subEntries()) assertThat(innerCompound.subEntries())
.hasEntrySatisfying("string", isSimpleEntryForClass(String.class)) .hasEntrySatisfying("string", isSimpleEntryForClass(String.class))