feat(core, serde-ext, weaver-pojo)!: Introduce string map weaving, extend addressable entry API
This commit is contained in:
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Breaking**@`core`: Added `getEntry` method to `AddressableStructuredConfigEntry`
|
||||
that allows to get a subentry by its data key.
|
||||
- `serde-extension`: Introduce a more generic `ReaderWriter` for `MutableStructuredConfigEntry`s.
|
||||
- `weaver-pojo`: Make maps with string keys into their own weavable entry type.
|
||||
|
||||
## [0.8.1] - 2026-04-26
|
||||
|
||||
### Fixed
|
||||
|
||||
+4
@@ -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)) {
|
||||
|
||||
+6
-1
@@ -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)) {
|
||||
|
||||
+6
@@ -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;
|
||||
|
||||
+16
-6
@@ -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();
|
||||
|
||||
+2
@@ -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})
|
||||
|
||||
+14
@@ -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;
|
||||
}
|
||||
+20
@@ -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();
|
||||
}
|
||||
+4
-5
@@ -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());
|
||||
}
|
||||
|
||||
+4
-5
@@ -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()));
|
||||
|
||||
+135
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+100
@@ -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;
|
||||
}
|
||||
}
|
||||
-9
@@ -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();
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojo.impl.weaving.collection;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
-14
@@ -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();
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojo.impl.weaving.compound;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+4
-4
@@ -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();
|
||||
}
|
||||
+4
-4
@@ -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())
|
||||
+20
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojo.impl.weaving.config;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+170
@@ -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> {}
|
||||
}
|
||||
+18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user