feat(core, serde-ext, weaver-pojo)!: Introduce string map weaving, extend addressable entry API

This commit is contained in:
2026-05-02 18:58:17 +02:00
parent 110bbed5c1
commit 353c5126bc
23 changed files with 545 additions and 56 deletions
@@ -1,5 +1,7 @@
package de.siphalor.tweed5.core.api.entry;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
public interface AddressableStructuredConfigEntry<T> extends StructuredConfigEntry<T> {
@@ -9,5 +11,7 @@ public interface AddressableStructuredConfigEntry<T> extends StructuredConfigEnt
return this;
}
@Nullable ConfigEntry<?> getEntry(String dataKey);
Object get(T value, String dataKey);
}
@@ -11,6 +11,11 @@ public interface CompoundConfigEntry<T> extends MutableStructuredConfigEntry<T>
return this;
}
@Override
default ConfigEntry<?> getEntry(String dataKey) {
return subEntries().get(dataKey);
}
@Override
default void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterStructuredEntry(this)) {
@@ -1,7 +1,7 @@
package de.siphalor.tweed5.core.api.entry;
import de.siphalor.tweed5.core.api.Arity;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.Nullable;
import java.util.function.Consumer;
@@ -17,6 +17,11 @@ public interface NullableConfigEntry<T extends @Nullable Object> extends Address
ConfigEntry<T> nonNullEntry();
@Override
default @Nullable ConfigEntry<?> getEntry(String dataKey) {
return dataKey.equals(NON_NULL_KEY) ? nonNullEntry() : null;
}
@Override
default void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterStructuredEntry(this)) {
@@ -3,6 +3,7 @@ package de.siphalor.tweed5.serde.extension.api.readwrite;
import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.MutableStructuredConfigEntry;
import de.siphalor.tweed5.core.api.entry.NullableConfigEntry;
import de.siphalor.tweed5.serde.extension.api.TweedEntryReader;
import de.siphalor.tweed5.serde.extension.api.TweedEntryWriter;
@@ -79,6 +80,11 @@ public class TweedEntryReaderWriters {
return (TweedEntryReaderWriter<C, @NonNull CollectionConfigEntry<T, C>>) (TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.COLLECTION_READER_WRITER;
}
public static <T, E extends MutableStructuredConfigEntry<T>> TweedEntryReaderWriter<T, E> mutableStructuredReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<T, E>) TweedEntryReaderWriterImpls.MUTABLE_STRUCTURED_READER_WRITER;
}
public static <T> TweedEntryReaderWriter<T, CompoundConfigEntry<T>> compoundReaderWriter() {
//noinspection unchecked
return (TweedEntryReaderWriter<T, @NonNull CompoundConfigEntry<T>>) (TweedEntryReaderWriter<?, ?>) TweedEntryReaderWriterImpls.COMPOUND_READER_WRITER;
@@ -32,7 +32,8 @@ public class TweedEntryReaderWriterImpls {
public static final TweedEntryReaderWriter<Object, NullableConfigEntry<Object>> NULLABLE_READER_WRITER = new NullableReaderWriter<>();
public static final TweedEntryReaderWriter<Collection<Object>, CollectionConfigEntry<Object, Collection<Object>>> COLLECTION_READER_WRITER = new CollectionReaderWriter<>();
public static final TweedEntryReaderWriter<Object, CompoundConfigEntry<Object>> COMPOUND_READER_WRITER = new CompoundReaderWriter<>();
public static final TweedEntryReaderWriter<Object, MutableStructuredConfigEntry<Object>> MUTABLE_STRUCTURED_READER_WRITER = new MutableStructuredReaderWriter<>();
public static final TweedEntryReaderWriter<Object, CompoundConfigEntry<Object>> COMPOUND_READER_WRITER = new MutableStructuredReaderWriter<>();
public static final TweedEntryReaderWriter<Object, ConfigEntry<Object>> NOOP_READER_WRITER = new NoopReaderWriter();
@@ -219,11 +220,15 @@ public class TweedEntryReaderWriterImpls {
}
}
public static class CompoundReaderWriter<T> implements TweedEntryReaderWriter<T, CompoundConfigEntry<T>> {
public static class MutableStructuredReaderWriter<T, E extends MutableStructuredConfigEntry<T>>
implements TweedEntryReaderWriter<T, E> {
@Override
public TweedReadResult<T> read(TweedDataReader reader, CompoundConfigEntry<T> entry, TweedReadContext context) {
public TweedReadResult<T> read(
TweedDataReader reader,
E entry,
TweedReadContext context
) {
return requireToken(reader, TweedDataToken::isMapStart, "Expected map start", context).andThen(_token -> {
Map<String, ConfigEntry<?>> compoundEntries = entry.subEntries();
T compoundValue = entry.instantiateValue();
List<TweedReadIssue> issues = new ArrayList<>();
while (true) {
@@ -235,7 +240,7 @@ public class TweedEntryReaderWriterImpls {
String key = token.readAsString();
//noinspection unchecked
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) compoundEntries.get(key);
ConfigEntry<Object> subEntry = (ConfigEntry<Object>) entry.getEntry(key);
if (subEntry == null) {
TweedReadResult<Object> noopResult = NOOP_READER_WRITER.read(reader, null, context);
issues.addAll(Arrays.asList(noopResult.issues()));
@@ -277,7 +282,12 @@ public class TweedEntryReaderWriterImpls {
}
@Override
public void write(TweedDataVisitor writer, @Nullable T value, CompoundConfigEntry<T> entry, TweedWriteContext context) throws TweedEntryWriteException, TweedDataWriteException {
public void write(
TweedDataVisitor writer,
@Nullable T value,
E entry,
TweedWriteContext context
) throws TweedEntryWriteException, TweedDataWriteException {
requireNonNullWriteValue(value, context);
writer.visitMapStart();
@@ -3,6 +3,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.StringMapPojoWeaver;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TrivialPojoWeaver;
import java.lang.annotation.ElementType;
@@ -13,6 +14,7 @@ import java.lang.annotation.Target;
@AnnotationInheritance(passOn = PojoWeavingExtension.class)
@PojoWeavingExtension(CompoundPojoWeaver.class)
@PojoWeavingExtension(CollectionPojoWeaver.class)
@PojoWeavingExtension(StringMapPojoWeaver.class)
@PojoWeavingExtension(TrivialPojoWeaver.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@@ -0,0 +1,14 @@
package de.siphalor.tweed5.weaver.pojo.api.annotation;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableStringMapConfigEntry;
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.TYPE_USE})
public @interface StringMapWeaving {
Class<? extends WeavableStringMapConfigEntry> entryClass() default WeavableStringMapConfigEntry.class;
}
@@ -0,0 +1,20 @@
package de.siphalor.tweed5.weaver.pojo.api.entry;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.MutableStructuredConfigEntry;
import java.util.Map;
import java.util.function.Supplier;
public interface WeavableStringMapConfigEntry<V, M extends Map<String, V>> extends MutableStructuredConfigEntry<M> {
@SuppressWarnings("rawtypes")
TweedConstructFactory<WeavableStringMapConfigEntry> FACTORY =
TweedConstructFactory.builder(WeavableStringMapConfigEntry.class)
.typedArg(ConfigContainer.class)
.namedArg("mapClass", Class.class) // map class
.namedArg("mapConstructor", Supplier.class) // map constructor
.namedArg("valueEntry", ConfigEntry.class)
.build();
}
@@ -9,8 +9,7 @@ import de.siphalor.tweed5.weaver.pojo.api.annotation.CollectionWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCollectionConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.entry.CollectionConfigEntryImpl;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.collection.CollectionWeavingConfig;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.collection.CollectionWeavingConfigImpl;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.config.CollectionWeavingConfig;
import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
@@ -21,7 +20,7 @@ import java.util.*;
import java.util.function.IntFunction;
public class CollectionPojoWeaver implements TweedPojoWeavingExtension {
private static final CollectionWeavingConfig DEFAULT_WEAVING_CONFIG = CollectionWeavingConfigImpl.builder()
private static final CollectionWeavingConfig DEFAULT_WEAVING_CONFIG = CollectionWeavingConfig.builder()
.collectionEntryClass(CollectionConfigEntryImpl.class)
.build();
@@ -80,7 +79,7 @@ public class CollectionPojoWeaver implements TweedPojoWeavingExtension {
return parent;
}
return CollectionWeavingConfigImpl.withOverrides(parent, local);
return CollectionWeavingConfig.withOverrides(parent, local);
}
private @Nullable CollectionWeavingConfig createWeavingConfigFromAnnotations(AnnotatedElement annotations) {
@@ -89,7 +88,7 @@ public class CollectionPojoWeaver implements TweedPojoWeavingExtension {
return null;
}
CollectionWeavingConfigImpl.CollectionWeavingConfigImplBuilder builder = CollectionWeavingConfigImpl.builder();
CollectionWeavingConfig.CollectionWeavingConfigBuilder builder = CollectionWeavingConfig.builder();
if (annotation.entryClass() != null) {
builder.collectionEntryClass(annotation.entryClass());
}
@@ -15,8 +15,7 @@ import de.siphalor.tweed5.typeutils.api.type.TypeAnnotationLayer;
import de.siphalor.tweed5.weaver.pojo.impl.entry.StaticPojoCompoundConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoClassIntrospector;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfig;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.compound.CompoundWeavingConfigImpl;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.config.CompoundWeavingConfig;
import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
@@ -30,7 +29,7 @@ import java.util.stream.Collectors;
* A weaver that weaves classes with the {@link CompoundWeaving} annotation as compound entries.
*/
public class CompoundPojoWeaver implements TweedPojoWeavingExtension {
private static final CompoundWeavingConfig DEFAULT_WEAVING_CONFIG = CompoundWeavingConfigImpl.builder()
private static final CompoundWeavingConfig DEFAULT_WEAVING_CONFIG = CompoundWeavingConfig.builder()
.compoundSourceNamingFormat(NamingFormats.camelCase())
.compoundTargetNamingFormat(NamingFormats.camelCase())
.compoundEntryClass(StaticPojoCompoundConfigEntry.class)
@@ -84,7 +83,7 @@ public class CompoundPojoWeaver implements TweedPojoWeavingExtension {
return parent;
}
return CompoundWeavingConfigImpl.withOverrides(parent, local);
return CompoundWeavingConfig.withOverrides(parent, local);
}
private @Nullable CompoundWeavingConfig createWeavingConfigFromAnnotations(AnnotatedElement annotations) {
@@ -93,7 +92,7 @@ public class CompoundPojoWeaver implements TweedPojoWeavingExtension {
return null;
}
CompoundWeavingConfigImpl.CompoundWeavingConfigImplBuilder builder = CompoundWeavingConfigImpl.builder();
CompoundWeavingConfig.CompoundWeavingConfigBuilder builder = CompoundWeavingConfig.builder();
builder.compoundSourceNamingFormat(NamingFormats.camelCase());
if (!annotation.namingFormat().isEmpty()) {
builder.compoundTargetNamingFormat(getNamingFormatById(annotation.namingFormat()));
@@ -0,0 +1,135 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
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.annotation.StringMapWeaving;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableStringMapConfigEntry;
import de.siphalor.tweed5.weaver.pojo.impl.entry.StringMapConfigEntryImpl;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.PojoWeavingException;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.config.StringMapWeavingConfig;
import org.jspecify.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.AnnotatedElement;
import java.util.*;
import java.util.function.Supplier;
/**
* A weaver that weaves classes with the {@link StringMapWeaving} annotation as string map entries.
* The type must be a {@link Map} with {@link String} keys.
*/
public class StringMapPojoWeaver implements TweedPojoWeavingExtension {
private static final StringMapWeavingConfig DEFAULT_WEAVING_CONFIG = StringMapWeavingConfig.builder()
.stringMapEntryClass(StringMapConfigEntryImpl.class)
.build();
@Nullable
private PatchworkPartAccess<StringMapWeavingConfig> weavingConfigAccess;
@Override
public void setup(SetupContext context) {
this.weavingConfigAccess = context.registerWeavingContextExtensionData(StringMapWeavingConfig.class);
}
@SuppressWarnings({"unchecked"})
@Override
public <T> @Nullable ConfigEntry<T> weaveEntry(ActualType<T> valueType, WeavingContext context) {
assert weavingConfigAccess != null;
List<ActualType<?>> mapTypeParams = valueType.getTypesOfSuperArguments(Map.class);
if (mapTypeParams == null || mapTypeParams.get(0).declaredType() != String.class) {
return null;
}
try {
StringMapWeavingConfig weavingConfig = getOrCreateWeavingConfig(context);
Patchwork newExtensionsData = context.extensionsData().copy();
newExtensionsData.set(weavingConfigAccess, weavingConfig);
ActualType<?> valueTypeParam = mapTypeParams.get(1);
Supplier<Map<String, Object>> constructor = getMapConstructor(valueType);
ConfigEntry<?> valueEntry = context.weaveEntry(
valueTypeParam,
newExtensionsData,
ProtoWeavingContext.subContextFor(context, "value", valueTypeParam)
);
return WeavableStringMapConfigEntry.FACTORY
.construct(Objects.requireNonNull(weavingConfig.stringMapEntryClass()))
.typedArg(ConfigContainer.class, context.configContainer())
.namedArg("mapClass", valueType.declaredType())
.namedArg("mapConstructor", constructor)
.namedArg("valueEntry", valueEntry)
.finish();
} catch (Exception e) {
throw new PojoWeavingException("Exception occurred trying to weave string map for class " + valueType, e);
}
}
private StringMapWeavingConfig getOrCreateWeavingConfig(WeavingContext context) {
assert weavingConfigAccess != null;
StringMapWeavingConfig parent = context.extensionsData().get(weavingConfigAccess);
if (parent == null) {
parent = DEFAULT_WEAVING_CONFIG;
}
StringMapWeavingConfig local = createWeavingConfigFromAnnotations(context.annotations());
if (local == null) {
return parent;
}
return StringMapWeavingConfig.withOverrides(parent, local);
}
private @Nullable StringMapWeavingConfig createWeavingConfigFromAnnotations(AnnotatedElement annotations) {
StringMapWeaving annotation = annotations.getAnnotation(StringMapWeaving.class);
if (annotation == null) {
return null;
}
StringMapWeavingConfig.StringMapWeavingConfigBuilder builder = StringMapWeavingConfig.builder();
if (annotation.entryClass() != WeavableStringMapConfigEntry.class) {
builder.stringMapEntryClass(annotation.entryClass());
}
return builder.build();
}
private Supplier<Map<String, Object>> getMapConstructor(ActualType<?> type) {
Class<?> declaredType = type.declaredType();
if (declaredType == Map.class || declaredType == HashMap.class) {
return HashMap::new;
} else if (declaredType == LinkedHashMap.class) {
return LinkedHashMap::new;
} else if (declaredType == TreeMap.class) {
return TreeMap::new;
}
try {
return findCompatibleConstructor(type);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new PojoWeavingException("Could not find no-args constructor for " + type, e);
}
}
@SuppressWarnings("unchecked")
private Supplier<Map<String, Object>> findCompatibleConstructor(ActualType<?> type) throws
NoSuchMethodException,
IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle constructor = lookup.findConstructor(type.declaredType(), MethodType.methodType(void.class));
return () -> {
try {
return (Map<String, Object>) constructor.invoke();
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
}
@@ -0,0 +1,100 @@
package de.siphalor.tweed5.weaver.pojo.impl.entry;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.core.api.Arity;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.BaseConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryValueVisitor;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.entry.SubEntryKey;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableStringMapConfigEntry;
import org.jspecify.annotations.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier;
public class StringMapConfigEntryImpl<V, M extends Map<String, V>>
extends BaseConfigEntry<M>
implements WeavableStringMapConfigEntry<V, M> {
private final Supplier<M> mapConstructor;
private final ConfigEntry<V> valueEntry;
public StringMapConfigEntryImpl(
ConfigContainer<?> container,
@ConstructParameter(name = "mapClass") Class<M> mapClass,
@ConstructParameter(name = "mapConstructor") Supplier<M> mapConstructor,
@ConstructParameter(name = "valueEntry") ConfigEntry<V> valueEntry
) {
super(container, mapClass);
this.mapConstructor = mapConstructor;
this.valueEntry = valueEntry;
}
@Override
public M instantiateValue() {
return mapConstructor.get();
}
@Override
public void set(M value, String dataKey, Object subValue) {
//noinspection unchecked
value.put(dataKey, (V) subValue);
}
@Override
public @Nullable ConfigEntry<?> getEntry(String dataKey) {
return valueEntry;
}
@Override
public Object get(M value, String dataKey) {
return value.get(dataKey);
}
@Override
public Map<String, ConfigEntry<?>> subEntries() {
return Collections.singletonMap(":value", valueEntry);
}
@Override
public void visitInOrder(ConfigEntryVisitor visitor) {
if (visitor.enterStructuredEntry(this)) {
if (visitor.enterStructuredSubEntry(":value", Arity.ANY)) {
valueEntry.visitInOrder(visitor);
visitor.leaveStructuredSubEntry(":value", Arity.ANY);
}
visitor.leaveStructuredEntry(this);
}
}
@Override
public void visitInOrder(ConfigEntryValueVisitor visitor, M value) {
if (value == null) {
return;
}
if (visitor.enterStructuredEntry(this, value)) {
for (Map.Entry<String, V> entry : value.entrySet()) {
SubEntryKey subEntryKey = SubEntryKey.addressable(":value", entry.getKey(), entry.getKey());
if (visitor.enterSubEntry(subEntryKey)) {
valueEntry.visitInOrder(visitor, entry.getValue());
visitor.leaveSubEntry(subEntryKey);
}
}
visitor.leaveStructuredEntry(this, value);
}
}
@Override
public M deepCopy(M value) {
if (value == null) {
return null;
}
M copy = mapConstructor.get();
for (Map.Entry<String, V> entry : value.entrySet()) {
copy.put(entry.getKey(), valueEntry.deepCopy(entry.getValue()));
}
return copy;
}
}
@@ -1,9 +0,0 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.collection;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCollectionConfigEntry;
import org.jspecify.annotations.Nullable;
public interface CollectionWeavingConfig {
@SuppressWarnings("rawtypes")
@Nullable Class<? extends WeavableCollectionConfigEntry> collectionEntryClass();
}
@@ -1,4 +0,0 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojo.impl.weaving.collection;
import org.jspecify.annotations.NullMarked;
@@ -1,14 +0,0 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
import org.jspecify.annotations.Nullable;
public interface CompoundWeavingConfig {
@Nullable NamingFormat compoundSourceNamingFormat();
@Nullable NamingFormat compoundTargetNamingFormat();
@SuppressWarnings("rawtypes")
@Nullable Class<? extends WeavableCompoundConfigEntry> compoundEntryClass();
}
@@ -1,4 +0,0 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
import org.jspecify.annotations.NullMarked;
@@ -1,4 +1,4 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.collection;
package de.siphalor.tweed5.weaver.pojo.impl.weaving.config;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCollectionConfigEntry;
import lombok.Builder;
@@ -7,13 +7,13 @@ import org.jspecify.annotations.Nullable;
@Builder
@Value
public class CollectionWeavingConfigImpl implements CollectionWeavingConfig {
public class CollectionWeavingConfig {
@SuppressWarnings("rawtypes")
@Nullable Class<? extends WeavableCollectionConfigEntry> collectionEntryClass;
public static CollectionWeavingConfigImpl withOverrides(CollectionWeavingConfig self, CollectionWeavingConfig overrides) {
return CollectionWeavingConfigImpl.builder()
public static CollectionWeavingConfig withOverrides(CollectionWeavingConfig self, CollectionWeavingConfig overrides) {
return CollectionWeavingConfig.builder()
.collectionEntryClass(overrides.collectionEntryClass() != null ? overrides.collectionEntryClass() : self.collectionEntryClass())
.build();
}
@@ -1,4 +1,4 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
package de.siphalor.tweed5.weaver.pojo.impl.weaving.config;
import de.siphalor.tweed5.namingformat.api.NamingFormat;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableCompoundConfigEntry;
@@ -8,15 +8,15 @@ import org.jspecify.annotations.Nullable;
@Builder
@Value
public class CompoundWeavingConfigImpl implements CompoundWeavingConfig {
public class CompoundWeavingConfig {
@Nullable NamingFormat compoundSourceNamingFormat;
@Nullable NamingFormat compoundTargetNamingFormat;
@SuppressWarnings("rawtypes")
@Nullable Class<? extends WeavableCompoundConfigEntry> compoundEntryClass;
public static CompoundWeavingConfigImpl withOverrides(CompoundWeavingConfig self, CompoundWeavingConfig overrides) {
return CompoundWeavingConfigImpl.builder()
public static CompoundWeavingConfig withOverrides(CompoundWeavingConfig self, CompoundWeavingConfig overrides) {
return CompoundWeavingConfig.builder()
.compoundSourceNamingFormat(overrides.compoundSourceNamingFormat() != null ? overrides.compoundSourceNamingFormat() : self.compoundSourceNamingFormat())
.compoundTargetNamingFormat(overrides.compoundTargetNamingFormat() != null ? overrides.compoundTargetNamingFormat() : self.compoundTargetNamingFormat())
.compoundEntryClass(overrides.compoundEntryClass() != null ? overrides.compoundEntryClass() : self.compoundEntryClass())
@@ -0,0 +1,20 @@
package de.siphalor.tweed5.weaver.pojo.impl.weaving.config;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableStringMapConfigEntry;
import lombok.Builder;
import lombok.Value;
import org.jspecify.annotations.Nullable;
@Builder
@Value
public class StringMapWeavingConfig {
@SuppressWarnings("rawtypes")
@Nullable Class<? extends WeavableStringMapConfigEntry> stringMapEntryClass;
public static StringMapWeavingConfig withOverrides(StringMapWeavingConfig self, StringMapWeavingConfig overrides) {
return StringMapWeavingConfig.builder()
.stringMapEntryClass(overrides.stringMapEntryClass() != null ? overrides.stringMapEntryClass() : self.stringMapEntryClass())
.build();
}
}
@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojo.impl.weaving.config;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,170 @@
package de.siphalor.tweed5.weaver.pojo.api.weaving;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkFactory;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.annotation.StringMapWeaving;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isSimpleEntryForClass;
import static de.siphalor.tweed5.weaver.pojo.test.ConfigEntryAssertions.isStringMapEntryForClass;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@SuppressWarnings("unused")
class StringMapPojoWeaverTest {
private StringMapPojoWeaver stringMapWeaver;
private PatchworkFactory weavingContextExtensionDataFactory;
@BeforeEach
void setup() {
PatchworkFactory.Builder builder = PatchworkFactory.builder();
stringMapWeaver = new StringMapPojoWeaver();
stringMapWeaver.setup(builder::registerPart);
weavingContextExtensionDataFactory = builder.build();
}
private WeavingContext.WeavingFn createWeavingFn(AtomicReference<WeavingContext.@Nullable WeavingFn> ref) {
WeavingContext.WeavingFn fn = new WeavingContext.WeavingFn() {
@Override
public <T> ConfigEntry<T> weaveEntry(
ActualType<T> valueType,
Patchwork extensionsData,
ProtoWeavingContext protoContext
) {
WeavingContext context = WeavingContext.builder()
.configContainer(protoContext.configContainer())
.annotations(valueType)
.extensionsData(extensionsData.copy())
.path(protoContext.path())
.weavingFunction(Objects.requireNonNull(ref.get()))
.build();
return Objects.requireNonNullElseGet(
stringMapWeaver.weaveEntry(valueType, context),
() -> new SimpleConfigEntryImpl<>(context.configContainer(), valueType.declaredType())
);
}
@Override
public <T> ConfigEntry<T> weavePseudoEntry(
WeavingContext parentContext,
String pseudoEntryName,
Patchwork extensionsData
) {
assert false;
return null;
}
};
ref.set(fn);
return fn;
}
@Test
void weaveStringToIntegerMap() {
AtomicReference<WeavingContext.@Nullable WeavingFn> weavingFnRef = new AtomicReference<>();
WeavingContext.WeavingFn weavingFn = createWeavingFn(weavingFnRef);
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(weavingFn)
.extensionsData(weavingContextExtensionDataFactory.create())
.annotations(StringToIntegerMap.class)
.path(new String[0])
.configContainer(mock(ConfigContainer.class))
.build();
ConfigEntry<StringToIntegerMap> resultEntry = stringMapWeaver.weaveEntry(
ActualType.ofClass(StringToIntegerMap.class),
weavingContext
);
assertThat(resultEntry).satisfies(isStringMapEntryForClass(StringToIntegerMap.class, mapEntry ->
assertThat(mapEntry.subEntries())
.hasEntrySatisfying(":value", isSimpleEntryForClass(Integer.class))
.hasSize(1)
));
}
@Test
void weaveStringToStringMap() {
AtomicReference<WeavingContext.@Nullable WeavingFn> weavingFnRef = new AtomicReference<>();
WeavingContext.WeavingFn weavingFn = createWeavingFn(weavingFnRef);
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(weavingFn)
.extensionsData(weavingContextExtensionDataFactory.create())
.annotations(StringToStringLinkedMap.class)
.path(new String[0])
.configContainer(mock(ConfigContainer.class))
.build();
ConfigEntry<StringToStringLinkedMap> resultEntry = stringMapWeaver.weaveEntry(
ActualType.ofClass(StringToStringLinkedMap.class),
weavingContext
);
assertThat(resultEntry).satisfies(isStringMapEntryForClass(StringToStringLinkedMap.class, mapEntry ->
assertThat(mapEntry.subEntries())
.hasEntrySatisfying(":value", isSimpleEntryForClass(String.class))
.hasSize(1)
));
}
@Test
void returnsNullWithoutAnnotation() {
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(mock(WeavingContext.WeavingFn.class))
.extensionsData(weavingContextExtensionDataFactory.create())
.annotations(UnannotatedStringMap.class)
.path(new String[0])
.configContainer(mock(ConfigContainer.class))
.build();
ConfigEntry<UnannotatedStringMap> resultEntry = stringMapWeaver.weaveEntry(
ActualType.ofClass(UnannotatedStringMap.class),
weavingContext
);
assertThat(resultEntry).isNull();
}
@Test
void returnsNullForNonStringKey() {
WeavingContext weavingContext = WeavingContext.builder()
.weavingFunction(mock(WeavingContext.WeavingFn.class))
.extensionsData(weavingContextExtensionDataFactory.create())
.annotations(IntegerKeyMap.class)
.path(new String[0])
.configContainer(mock(ConfigContainer.class))
.build();
ConfigEntry<IntegerKeyMap> resultEntry = stringMapWeaver.weaveEntry(
ActualType.ofClass(IntegerKeyMap.class),
weavingContext
);
assertThat(resultEntry).isNull();
}
@StringMapWeaving
public static class StringToIntegerMap extends HashMap<String, Integer> {}
@StringMapWeaving
public static class StringToStringLinkedMap extends LinkedHashMap<String, String> {}
public static class UnannotatedStringMap extends HashMap<String, Integer> {}
@StringMapWeaving
public static class IntegerKeyMap extends HashMap<Integer, String> {}
}
@@ -4,6 +4,7 @@ import de.siphalor.tweed5.core.api.entry.CollectionConfigEntry;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.weaver.pojo.api.entry.WeavableStringMapConfigEntry;
import java.util.function.Consumer;
@@ -35,6 +36,23 @@ public class ConfigEntryAssertions {
);
}
@SuppressWarnings("rawtypes")
public static Consumer<Object> isStringMapEntryForClass(
Class<?> mapClass,
Consumer<WeavableStringMapConfigEntry<?, ?>> condition
) {
return object -> assertThat(object)
.as("Should be a string map config entry for class " + mapClass.getName())
.asInstanceOf(type(WeavableStringMapConfigEntry.class))
.as("String map entry for class " + mapClass.getSimpleName())
.satisfies(
mapEntry -> assertThat(mapEntry.valueClass())
.as("Value class of string map entry should match")
.isEqualTo(mapClass),
condition::accept
);
}
public static Consumer<Object> isCollectionEntryForClass(
Class<?> collectionClass,
Consumer<CollectionConfigEntry<?, ?>> condition