[weaver-pojo-serde-extension] Implement auto serde for POJO weaving
This commit is contained in:
@@ -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(
|
||||
ConfigEntry<T> entry,
|
||||
TweedEntryReader<T, C> entryReader,
|
||||
|
||||
@@ -18,7 +18,6 @@ import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
|
||||
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
|
||||
import lombok.Data;
|
||||
import lombok.val;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -78,6 +77,26 @@ public class ReadWriteExtensionImpl implements ReadWriteExtension {
|
||||
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
|
||||
public <T, C extends ConfigEntry<T>> void setEntryReaderWriter(
|
||||
ConfigEntry<T> entry,
|
||||
|
||||
@@ -6,5 +6,6 @@ dependencies {
|
||||
api(project(":tweed5-weaver-pojo"))
|
||||
api(project(":tweed5-serde-extension"))
|
||||
|
||||
testImplementation(project(":tweed5-default-extensions"))
|
||||
testImplementation(project(":tweed5-serde-hjson"))
|
||||
}
|
||||
@@ -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.WeavingContext;
|
||||
import de.siphalor.tweed5.weaver.pojoext.serde.impl.SerdePojoReaderWriterSpec;
|
||||
import de.siphalor.tweed5.weaver.pojoext.serde.impl.ReaderWriterLoader;
|
||||
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.*;
|
||||
|
||||
@CommonsLog
|
||||
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 ReaderWriterLoader readerWriterLoader = new ReaderWriterLoader();
|
||||
|
||||
public ReadWritePojoWeavingProcessor(ConfigContainer<?> configContainer) {
|
||||
this.readWriteExtension = configContainer.extension(ReadWriteExtension.class)
|
||||
@@ -40,38 +37,7 @@ public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension
|
||||
|
||||
private void loadProviders() {
|
||||
ServiceLoader<TweedReaderWriterProvider> serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class);
|
||||
|
||||
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);
|
||||
}
|
||||
serviceLoader.forEach(readerWriterLoader::load);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,98 +47,54 @@ public class ReadWritePojoWeavingProcessor implements TweedPojoWeavingExtension
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//noinspection rawtypes,unchecked
|
||||
readWriteExtension.setEntryReaderWriter(
|
||||
(ConfigEntry) configEntry,
|
||||
(TweedEntryReader) resolveReader(entryConfig, context),
|
||||
(TweedEntryWriter) resolveWriter(entryConfig, context)
|
||||
(TweedEntryReader) resolveReader(entryConfig),
|
||||
(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();
|
||||
SerdePojoReaderWriterSpec spec = specFromText(specText, context);
|
||||
|
||||
//noinspection unchecked,rawtypes
|
||||
return Optional.ofNullable(spec)
|
||||
.map(s -> resolveReaderWriterFromSpec((Class<TweedEntryReader<?, ?>>)(Object) TweedEntryReader.class, readerFactories, s, context))
|
||||
.orElse(((TweedEntryReader) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
|
||||
SerdePojoReaderWriterSpec spec = specFromText(specText);
|
||||
if (spec == null) {
|
||||
return 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();
|
||||
SerdePojoReaderWriterSpec spec = specFromText(specText, context);
|
||||
|
||||
//noinspection unchecked,rawtypes
|
||||
return Optional.ofNullable(spec)
|
||||
.map(s -> resolveReaderWriterFromSpec((Class<TweedEntryWriter<?, ?>>)(Object) TweedEntryWriter.class, writerFactories, s, context))
|
||||
.orElse(((TweedEntryWriter) TweedEntryReaderWriterImpls.NOOP_READER_WRITER));
|
||||
SerdePojoReaderWriterSpec spec = specFromText(specText);
|
||||
if (spec == null) {
|
||||
return 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()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return SerdePojoReaderWriterSpec.parse(specText);
|
||||
} catch (SerdePojoReaderWriterSpec.ParseException e) {
|
||||
log.warn(
|
||||
"Failed to parse definition for reader or writer on entry "
|
||||
+ 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",
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to parse definition for reader or writer: \"" + specText + "\"",
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojoext.serde.api.auto;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
|
||||
|
||||
public enum AutoReadWriteNullability {
|
||||
NON_NULL,
|
||||
NULLABLE,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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 + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test;
|
||||
import java.io.StringReader;
|
||||
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;
|
||||
|
||||
class ReadWritePojoWeavingProcessorTest {
|
||||
@@ -34,18 +36,11 @@ class ReadWritePojoWeavingProcessorTest {
|
||||
ConfigContainer<AnnotatedConfig> configContainer = weaverBootstrapper.weave();
|
||||
configContainer.initialize();
|
||||
|
||||
ReadWriteExtension readWriteExtension = configContainer.extension(ReadWriteExtension.class).orElseThrow();
|
||||
|
||||
AnnotatedConfig config = new AnnotatedConfig(123, "test", new TestClass(987));
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
HjsonWriter hjsonWriter = new HjsonWriter(stringWriter, new HjsonWriter.Options());
|
||||
readWriteExtension.write(
|
||||
hjsonWriter,
|
||||
config,
|
||||
configContainer.rootEntry(),
|
||||
readWriteExtension.createReadWriteContextExtensionsData()
|
||||
);
|
||||
configContainer.rootEntry().apply(write(hjsonWriter, config));
|
||||
|
||||
assertThat(stringWriter).hasToString("""
|
||||
{
|
||||
@@ -62,11 +57,8 @@ class ReadWritePojoWeavingProcessorTest {
|
||||
\ttest: { inner: 29 }
|
||||
}"""
|
||||
)));
|
||||
assertThat(readWriteExtension.read(
|
||||
reader,
|
||||
configContainer.rootEntry(),
|
||||
readWriteExtension.createReadWriteContextExtensionsData()
|
||||
)).isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
|
||||
assertThat(configContainer.rootEntry().call(read(reader)))
|
||||
.isEqualTo(new AnnotatedConfig(987, "abdef", new TestClass(29)));
|
||||
}
|
||||
|
||||
@AutoService(TweedReaderWriterProvider.class)
|
||||
@@ -89,7 +81,7 @@ class ReadWritePojoWeavingProcessorTest {
|
||||
|
||||
@PojoWeaving(extensions = ReadWriteExtension.class)
|
||||
@DefaultWeavingExtensions
|
||||
@PojoWeavingExtension(de.siphalor.tweed5.weaver.pojoext.serde.api.ReadWritePojoWeavingProcessor.class)
|
||||
@PojoWeavingExtension(ReadWritePojoWeavingProcessor.class)
|
||||
@CompoundWeaving
|
||||
@EntryReadWriteConfig("tweed5.compound")
|
||||
// lombok
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.siphalor.tweed5.weaver.pojo.api.annotation;
|
||||
|
||||
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.TrivialPojoWeaver;
|
||||
|
||||
@@ -11,6 +12,7 @@ import java.lang.annotation.Target;
|
||||
|
||||
@AnnotationInheritance(passOn = PojoWeavingExtension.class)
|
||||
@PojoWeavingExtension(CompoundPojoWeaver.class)
|
||||
@PojoWeavingExtension(CollectionPojoWeaver.class)
|
||||
@PojoWeavingExtension(TrivialPojoWeaver.class)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
|
||||
|
||||
@@ -28,7 +28,8 @@ class TweedPojoWeaverBootstrapperTest {
|
||||
.hasEntrySatisfying("primitiveInteger", isSimpleEntryForClass(int.class))
|
||||
.hasEntrySatisfying("boxedDouble", isSimpleEntryForClass(Double.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 ->
|
||||
assertThat(innerCompound.subEntries())
|
||||
.hasEntrySatisfying("string", isSimpleEntryForClass(String.class))
|
||||
|
||||
Reference in New Issue
Block a user