[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(
|
<T, C extends ConfigEntry<T>> void setEntryReaderWriter(
|
||||||
ConfigEntry<T> entry,
|
ConfigEntry<T> entry,
|
||||||
TweedEntryReader<T, C> entryReader,
|
TweedEntryReader<T, C> entryReader,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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;
|
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})
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user