From 25faea92d8b0a81aaa76b65538fe7675e0dc5b23 Mon Sep 17 00:00:00 2001 From: Siphalor Date: Sat, 28 Jun 2025 23:30:14 +0200 Subject: [PATCH] [weaver-pojo-serde-extension] Implement auto serde for POJO weaving --- .../extension/api/ReadWriteExtension.java | 3 + .../impl/ReadWriteExtensionImpl.java | 21 +- .../build.gradle.kts | 3 +- .../api/ReadWritePojoWeavingProcessor.java | 144 +++---------- .../serde/api/auto/AutoReadWriteMapping.java | 16 ++ .../serde/api/auto/AutoReadWriteMappings.java | 12 ++ .../AutoReadWritePojoWeavingProcessor.java | 191 ++++++++++++++++++ .../api/auto/DefaultReadWriteMappings.java | 35 ++++ .../pojoext/serde/api/auto/package-info.java | 4 + .../AutoNullableReadWriteBehavior.java | 12 ++ ...NullableReadWritePojoWeavingProcessor.java | 119 +++++++++++ .../nullable/AutoReadWriteNullability.java | 6 + .../serde/api/nullable/package-info.java | 4 + .../serde/impl/ReaderWriterLoader.java | 136 +++++++++++++ .../ReadWritePojoWeavingProcessorTest.java | 20 +- ...AutoReadWritePojoWeavingProcessorTest.java | 134 ++++++++++++ ...ableReadWritePojoWeavingProcessorTest.java | 172 ++++++++++++++++ .../annotation/DefaultWeavingExtensions.java | 2 + .../TweedPojoWeaverBootstrapperTest.java | 3 +- 19 files changed, 909 insertions(+), 128 deletions(-) create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMapping.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMappings.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessor.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/DefaultReadWriteMappings.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/package-info.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWriteBehavior.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessor.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoReadWriteNullability.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/package-info.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/ReaderWriterLoader.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessorTest.java create mode 100644 tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessorTest.java diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java index e9f4a0e..4b5ac87 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/api/ReadWriteExtension.java @@ -96,6 +96,9 @@ public interface ReadWriteExtension extends TweedExtension { }; } + > @Nullable TweedEntryReader getDefinedEntryReader(ConfigEntry entry); + > @Nullable TweedEntryWriter getDefinedEntryWriter(ConfigEntry entry); + > void setEntryReaderWriter( ConfigEntry entry, TweedEntryReader entryReader, diff --git a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java index 46f1095..b1e903a 100644 --- a/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java +++ b/tweed5-serde-extension/src/main/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImpl.java @@ -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 > TweedEntryReader getDefinedEntryReader(ConfigEntry entry) { + CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess); + if (customEntryData == null) { + return null; + } + //noinspection unchecked + return (TweedEntryReader) customEntryData.readerDefinition(); + } + + @Override + public @Nullable > TweedEntryWriter getDefinedEntryWriter(ConfigEntry entry) { + CustomEntryData customEntryData = entry.extensionsData().get(customEntryDataAccess); + if (customEntryData == null) { + return null; + } + //noinspection unchecked + return (TweedEntryWriter) customEntryData.writerDefinition(); + } + @Override public > void setEntryReaderWriter( ConfigEntry entry, diff --git a/tweed5-weaver-pojo-serde-extension/build.gradle.kts b/tweed5-weaver-pojo-serde-extension/build.gradle.kts index ec8bac1..629f863 100644 --- a/tweed5-weaver-pojo-serde-extension/build.gradle.kts +++ b/tweed5-weaver-pojo-serde-extension/build.gradle.kts @@ -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")) -} \ No newline at end of file +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessor.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessor.java index e6efb6d..5581cc4 100644 --- a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessor.java +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessor.java @@ -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>> readerFactories = new HashMap<>(); - private final Map>> 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 serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class); - - for (TweedReaderWriterProvider readerWriterProvider : serviceLoader) { - TweedReaderWriterProvider.ProviderContext providerContext = new TweedReaderWriterProvider.ProviderContext() { - @Override - public void registerReaderFactory( - String id, - TweedReaderWriterProvider.ReaderWriterFactory> 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> 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; } - //noinspection rawtypes,unchecked - readWriteExtension.setEntryReaderWriter( - (ConfigEntry) configEntry, - (TweedEntryReader) resolveReader(entryConfig, context), - (TweedEntryWriter) resolveWriter(entryConfig, context) - ); + try { + //noinspection rawtypes,unchecked + readWriteExtension.setEntryReaderWriter( + (ConfigEntry) configEntry, + (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); + SerdePojoReaderWriterSpec spec = specFromText(specText); + if (spec == null) { + return TweedEntryReaderWriterImpls.NOOP_READER_WRITER; + } - //noinspection unchecked,rawtypes - return Optional.ofNullable(spec) - .map(s -> resolveReaderWriterFromSpec((Class>)(Object) TweedEntryReader.class, readerFactories, s, context)) - .orElse(((TweedEntryReader) TweedEntryReaderWriterImpls.NOOP_READER_WRITER)); + return readerWriterLoader.resolveReaderFromSpec(spec); } - private TweedEntryWriter resolveWriter(EntryReadWriteConfig entryConfig, WeavingContext context) { + private TweedEntryWriter resolveWriter(EntryReadWriteConfig entryConfig) { String specText = entryConfig.writer().isEmpty() ? entryConfig.value() : entryConfig.writer(); - SerdePojoReaderWriterSpec spec = specFromText(specText, context); + SerdePojoReaderWriterSpec spec = specFromText(specText); + if (spec == null) { + return TweedEntryReaderWriterImpls.NOOP_READER_WRITER; + } - //noinspection unchecked,rawtypes - return Optional.ofNullable(spec) - .map(s -> resolveReaderWriterFromSpec((Class>)(Object) TweedEntryWriter.class, writerFactories, s, context)) - .orElse(((TweedEntryWriter) TweedEntryReaderWriterImpls.NOOP_READER_WRITER)); + return readerWriterLoader.resolveWriterFromSpec(spec); } - private @Nullable SerdePojoReaderWriterSpec specFromText(String specText, WeavingContext context) { + 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 @Nullable T resolveReaderWriterFromSpec( - Class baseClass, - Map> 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 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 @Nullable T loadClassIfExists(Class 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; } } } diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMapping.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMapping.java new file mode 100644 index 0000000..ca185dd --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMapping.java @@ -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[] entryClasses() default { ConfigEntry.class }; + Class[] valueClasses(); + String spec() default ""; + String reader() default ""; + String writer() default ""; +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMappings.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMappings.java new file mode 100644 index 0000000..086cc17 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWriteMappings.java @@ -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(); +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessor.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessor.java new file mode 100644 index 0000000..432c254 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessor.java @@ -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 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 serviceLoader = ServiceLoader.load(TweedReaderWriterProvider.class); + serviceLoader.forEach(readerWriterLoader::load); + } + + @Override + public void beforeWeaveEntry(ActualType valueType, Patchwork extensionsData, ProtoWeavingContext context) { + assert customDataAccess != null; + + CustomData existingCustomData = extensionsData.get(customDataAccess); + List existingMappings = existingCustomData == null ? Collections.emptyList() : existingCustomData.mappings(); + + AutoReadWriteMapping[] mappingAnnotations = context.annotations() + .getAnnotationsByType(AutoReadWriteMapping.class); + + if (existingCustomData == null || mappingAnnotations.length > 0) { + List 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 void afterWeaveEntry(ActualType valueType, ConfigEntry 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) configEntry, + (TweedEntryReader>) mapping.reader(), + (TweedEntryWriter>) 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 mappings; + Map 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; + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/DefaultReadWriteMappings.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/DefaultReadWriteMappings.java new file mode 100644 index 0000000..dcdbd76 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/DefaultReadWriteMappings.java @@ -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 { +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/package-info.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/package-info.java new file mode 100644 index 0000000..0593f14 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.weaver.pojoext.serde.api.auto; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWriteBehavior.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWriteBehavior.java new file mode 100644 index 0000000..f4b696e --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWriteBehavior.java @@ -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(); +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessor.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessor.java new file mode 100644 index 0000000..08419f5 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessor.java @@ -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 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 void beforeWeaveEntry(ActualType 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 void afterWeaveEntry(ActualType valueType, ConfigEntry 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 AutoReadWriteNullability getNullability(ActualType 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; + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoReadWriteNullability.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoReadWriteNullability.java new file mode 100644 index 0000000..6ccaf96 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoReadWriteNullability.java @@ -0,0 +1,6 @@ +package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable; + +public enum AutoReadWriteNullability { + NON_NULL, + NULLABLE, +} diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/package-info.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/package-info.java new file mode 100644 index 0000000..ce8fc65 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphalor.tweed5.weaver.pojoext.serde.api.nullable; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/ReaderWriterLoader.java b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/ReaderWriterLoader.java new file mode 100644 index 0000000..c457f19 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/main/java/de/siphalor/tweed5/weaver/pojoext/serde/impl/ReaderWriterLoader.java @@ -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>> readerFactories + = new HashMap<>(); + @Getter + private final Map>> 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>) (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>) (Object) TweedEntryWriter.class, + writerFactories(), + spec + ); + if (writer != null) { + return writer; + } + return TweedEntryReaderWriterImpls.NOOP_READER_WRITER; + } + + private @Nullable T resolveReaderWriterFromSpec( + Class baseClass, + Map> 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 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 @Nullable T loadClassIfExists(Class 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> 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> writerFactory + ) { + if (writerFactories.putIfAbsent(id, writerFactory) != null) { + throw new IllegalArgumentException("Found duplicate Tweed entry reader id \"" + id + "\""); + } + } + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java index b00999d..66d016d 100644 --- a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java +++ b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/ReadWritePojoWeavingProcessorTest.java @@ -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 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 diff --git a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessorTest.java b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessorTest.java new file mode 100644 index 0000000..7d397b8 --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/auto/AutoReadWritePojoWeavingProcessorTest.java @@ -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 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) 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>) rootEntry.subEntries().get("nesteds"); + assertReaderAndWriter(nestedsEntry, TweedEntryReaderWriterImpls.COLLECTION_READER_WRITER); + + var nestedEntry = (CompoundConfigEntry) 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; + } +} diff --git a/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessorTest.java b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessorTest.java new file mode 100644 index 0000000..88c3a9c --- /dev/null +++ b/tweed5-weaver-pojo-serde-extension/src/test/java/de/siphalor/tweed5/weaver/pojoext/serde/api/nullable/AutoNullableReadWritePojoWeavingProcessorTest.java @@ -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 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) 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) 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; + } +} diff --git a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/DefaultWeavingExtensions.java b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/DefaultWeavingExtensions.java index 6c8dc23..883f2c3 100644 --- a/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/DefaultWeavingExtensions.java +++ b/tweed5-weaver-pojo/src/main/java/de/siphalor/tweed5/weaver/pojo/api/annotation/DefaultWeavingExtensions.java @@ -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}) diff --git a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java index 8c3b1a7..2e765e4 100644 --- a/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java +++ b/tweed5-weaver-pojo/src/test/java/de/siphalor/tweed5/weaver/pojo/impl/weaving/TweedPojoWeaverBootstrapperTest.java @@ -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))