[attributes] Introduce attributes extensions

This commit is contained in:
2025-07-27 01:18:32 +02:00
parent e4ea5fdfc2
commit c9a609d457
28 changed files with 1627 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
plugins {
id("dev.panuszewski.typesafe-conventions") version "0.6.0"
id("dev.panuszewski.typesafe-conventions") version "0.7.3"
}
rootProject.name = "tweed5-conventions"

View File

@@ -2,6 +2,7 @@ rootProject.name = "tweed5"
include("test-utils")
include("tweed5-annotation-inheritance")
include("tweed5-attributes-extension")
include("tweed5-construct")
include("tweed5-core")
include("tweed5-default-extensions")
@@ -13,4 +14,5 @@ include("tweed5-serde-hjson")
include("tweed5-type-utils")
include("tweed5-utils")
include("tweed5-weaver-pojo")
include("tweed5-weaver-pojo-attributes-extension")
include("tweed5-weaver-pojo-serde-extension")

View File

@@ -0,0 +1,10 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
implementation(project(":tweed5-core"))
compileOnly(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-extension"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,38 @@
package de.siphalor.tweed5.attributesextension.api;
import de.siphalor.tweed5.attributesextension.impl.AttributesExtensionImpl;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import org.jspecify.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public interface AttributesExtension extends TweedExtension {
Class<? extends AttributesExtension> DEFAULT = AttributesExtensionImpl.class;
static <C extends ConfigEntry<?>> Consumer<C> attribute(String key, String value) {
return entry -> entry.container().extension(AttributesExtension.class)
.orElseThrow(() -> new IllegalStateException("No attributes extension registered"))
.setAttribute(entry, key, value);
}
static <C extends ConfigEntry<?>> Consumer<C> attributeDefault(String key, String value) {
return entry -> entry.container().extension(AttributesExtension.class)
.orElseThrow(() -> new IllegalStateException("No attributes extension registered"))
.setAttributeDefault(entry, key, value);
}
default void setAttribute(ConfigEntry<?> entry, String key, String value) {
setAttribute(entry, key, Collections.singletonList(value));
}
void setAttribute(ConfigEntry<?> entry, String key, List<String> values);
default void setAttributeDefault(ConfigEntry<?> entry, String key, String value) {
setAttributeDefault(entry, key, Collections.singletonList(value));
}
void setAttributeDefault(ConfigEntry<?> entry, String key, List<String> values);
List<String> getAttributeValues(ConfigEntry<?> entry, String key);
@Nullable String getAttributeValue(ConfigEntry<?> entry, String key);
}

View File

@@ -0,0 +1,5 @@
package de.siphalor.tweed5.attributesextension.api;
public interface AttributesRelatedExtension {
default void afterAttributesInitialized() {}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.attributesextension.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.attributesextension.api.serde.filter;
import de.siphalor.tweed5.attributesextension.impl.serde.filter.AttributesReadWriteFilterExtensionImpl;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.patchwork.api.Patchwork;
public interface AttributesReadWriteFilterExtension extends TweedExtension {
Class<? extends AttributesReadWriteFilterExtension> DEFAULT = AttributesReadWriteFilterExtensionImpl.class;
void markAttributeForFiltering(String key);
void addFilter(Patchwork contextExtensionsData, String key, String value);
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.attributesextension.api.serde.filter;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,224 @@
package de.siphalor.tweed5.attributesextension.impl;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.attributesextension.api.AttributesRelatedExtension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.utils.api.collection.ImmutableArrayBackedMap;
import lombok.Data;
import lombok.var;
import org.jspecify.annotations.Nullable;
import java.util.*;
public class AttributesExtensionImpl implements AttributesExtension {
private final ConfigContainer<?> configContainer;
private final PatchworkPartAccess<CustomEntryData> dataAccess;
private boolean initialized;
public AttributesExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext setupContext) {
this.configContainer = configContainer;
this.dataAccess = setupContext.registerEntryExtensionData(CustomEntryData.class);
}
@Override
public String getId() {
return "attributes";
}
@Override
public void setAttribute(ConfigEntry<?> entry, String key, List<String> values) {
requireEditable();
var attributes = getOrCreateEditableAttributes(entry);
attributes.compute(key, (k, existingValues) -> {
if (existingValues == null) {
return new ArrayList<>(values);
} else {
existingValues.addAll(values);
return existingValues;
}
});
}
@Override
public void setAttributeDefault(ConfigEntry<?> entry, String key, List<String> values) {
requireEditable();
var attributeDefaults = getOrCreateEditableAttributeDefaults(entry);
attributeDefaults.compute(key, (k, existingValues) -> {
if (existingValues == null) {
return new ArrayList<>(values);
} else {
existingValues.addAll(values);
return existingValues;
}
});
}
private void requireEditable() {
if (initialized) {
throw new IllegalStateException("Attributes are only editable until the config has been initialized");
}
}
private Map<String, List<String>> getOrCreateEditableAttributes(ConfigEntry<?> entry) {
CustomEntryData data = getOrCreateCustomEntryData(entry);
var attributes = data.attributes();
if (attributes == null) {
attributes = new HashMap<>();
data.attributes(attributes);
}
return attributes;
}
private Map<String, List<String>> getOrCreateEditableAttributeDefaults(ConfigEntry<?> entry) {
CustomEntryData data = getOrCreateCustomEntryData(entry);
var attributeDefaults = data.attributeDefaults();
if (attributeDefaults == null) {
attributeDefaults = new HashMap<>();
data.attributeDefaults(attributeDefaults);
}
return attributeDefaults;
}
private CustomEntryData getOrCreateCustomEntryData(ConfigEntry<?> entry) {
CustomEntryData customEntryData = entry.extensionsData().get(dataAccess);
if (customEntryData == null) {
customEntryData = new CustomEntryData();
entry.extensionsData().set(dataAccess, customEntryData);
}
return customEntryData;
}
@Override
public void initialize() {
configContainer.rootEntry().visitInOrder(new ConfigEntryVisitor() {
private final Deque<Map<String, List<String>>> defaults = new ArrayDeque<>(
Collections.singletonList(Collections.emptyMap())
);
@Override
public boolean enterCompoundEntry(ConfigEntry<?> entry) {
enterEntry(entry);
return true;
}
@Override
public boolean enterCollectionEntry(ConfigEntry<?> entry) {
enterEntry(entry);
return true;
}
private void enterEntry(ConfigEntry<?> entry) {
var data = entry.extensionsData().get(dataAccess);
var currentDefaults = defaults.getFirst();
if (data == null) {
defaults.push(currentDefaults);
return;
}
var entryDefaults = data.attributeDefaults();
data.attributeDefaults(null);
if (entryDefaults == null || entryDefaults.isEmpty()) {
defaults.push(currentDefaults);
return;
}
defaults.push(mergeMapsAndSeal(currentDefaults, entryDefaults));
visitEntry(entry);
}
@Override
public void leaveCompoundEntry(ConfigEntry<?> entry) {
defaults.pop();
}
@Override
public void leaveCollectionEntry(ConfigEntry<?> entry) {
defaults.pop();
}
@Override
public void visitEntry(ConfigEntry<?> entry) {
var data = getOrCreateCustomEntryData(entry);
var currentDefaults = defaults.getFirst();
if (data.attributes() == null || data.attributes().isEmpty()) {
data.attributes(currentDefaults);
} else {
data.attributes(mergeMapsAndSeal(currentDefaults, data.attributes()));
}
}
private Map<String, List<String>> mergeMapsAndSeal(
Map<String, List<String>> base,
Map<String, List<String>> overrides
) {
if (overrides.isEmpty()) {
return ImmutableArrayBackedMap.ofEntries(base.entrySet());
} else if (base.isEmpty()) {
return ImmutableArrayBackedMap.ofEntries(overrides.entrySet());
}
List<Map.Entry<String, List<String>>> entries = new ArrayList<>(base.size() + overrides.size());
overrides.forEach((key, value) ->
entries.add(new AbstractMap.SimpleEntry<>(key, Collections.unmodifiableList(value)))
);
base.forEach((key, value) -> {
if (!overrides.containsKey(key)) {
entries.add(new AbstractMap.SimpleEntry<>(key, Collections.unmodifiableList(value)));
}
});
return ImmutableArrayBackedMap.ofEntries(entries);
}
});
initialized = true;
for (TweedExtension extension : configContainer.extensions()) {
if (extension instanceof AttributesRelatedExtension) {
((AttributesRelatedExtension) extension).afterAttributesInitialized();
}
}
}
@Override
public List<String> getAttributeValues(ConfigEntry<?> entry, String key) {
requireInitialized();
CustomEntryData data = entry.extensionsData().get(dataAccess);
return Optional.ofNullable(data)
.map(CustomEntryData::attributes)
.map(attributes -> attributes.get(key))
.orElse(Collections.emptyList());
}
@Override
public @Nullable String getAttributeValue(ConfigEntry<?> entry, String key) {
requireInitialized();
CustomEntryData data = entry.extensionsData().get(dataAccess);
return Optional.ofNullable(data)
.map(CustomEntryData::attributes)
.map(attributes -> attributes.get(key))
.map(values -> values.isEmpty() ? null : values.get(values.size() - 1))
.orElse(null);
}
private void requireInitialized() {
if (!initialized) {
throw new IllegalStateException("Attributes are only available after the config has been initialized");
}
}
@Data
private static class CustomEntryData {
private @Nullable Map<String, List<String>> attributes;
private @Nullable Map<String, List<String>> attributeDefaults;
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.attributesextension.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,404 @@
package de.siphalor.tweed5.attributesextension.impl.serde.filter;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.attributesextension.api.AttributesRelatedExtension;
import de.siphalor.tweed5.attributesextension.api.serde.filter.AttributesReadWriteFilterExtension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntryVisitor;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.TweedEntryReadException;
import de.siphalor.tweed5.data.extension.api.TweedEntryReader;
import de.siphalor.tweed5.data.extension.api.TweedEntryWriter;
import de.siphalor.tweed5.data.extension.api.TweedReadContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters;
import de.siphalor.tweed5.data.extension.impl.TweedEntryReaderWriterImpls;
import de.siphalor.tweed5.dataapi.api.DelegatingTweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataUnsupportedValueException;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.patchwork.api.Patchwork;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import de.siphalor.tweed5.utils.api.UniqueSymbol;
import lombok.*;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.*;
public class AttributesReadWriteFilterExtensionImpl
implements AttributesReadWriteFilterExtension, AttributesRelatedExtension, ReadWriteRelatedExtension {
private static final String ID = "attributes-serde-filter";
private static final Set<String> MIDDLEWARES_MUST_COME_BEFORE = new HashSet<>(Arrays.asList(
Middleware.DEFAULT_START,
"validation"
));
private static final Set<String> MIDDLEWARES_MUST_COME_AFTER = Collections.emptySet();
private static final UniqueSymbol TWEED_DATA_NOTHING_VALUE = new UniqueSymbol("nothing (skip value)");
private final ConfigContainer<?> configContainer;
private @Nullable AttributesExtension attributesExtension;
private final Set<String> filterableAttributes = new HashSet<>();
private final PatchworkPartAccess<EntryCustomData> entryDataAccess;
private @Nullable PatchworkPartAccess<ReadWriteContextCustomData> readWriteContextDataAccess;
public AttributesReadWriteFilterExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext setupContext) {
this.configContainer = configContainer;
entryDataAccess = setupContext.registerEntryExtensionData(EntryCustomData.class);
}
@Override
public String getId() {
return ID;
}
@Override
public void setupReadWriteExtension(ReadWriteExtensionSetupContext context) {
readWriteContextDataAccess = context.registerReadWriteContextExtensionData(ReadWriteContextCustomData.class);
context.registerReaderMiddleware(new ReaderMiddleware());
context.registerWriterMiddleware(new WriterMiddleware());
}
@Override
public void markAttributeForFiltering(String key) {
requireUninitialized();
filterableAttributes.add(key);
}
@Override
public void afterAttributesInitialized() {
attributesExtension = configContainer.extension(AttributesExtension.class)
.orElseThrow(() -> new IllegalStateException(
"You must register a " + AttributesExtension.class.getSimpleName()
+ " before initializing the " + AttributesReadWriteFilterExtension.class.getSimpleName()
));
configContainer.rootEntry().visitInOrder(new ConfigEntryVisitor() {
private final Deque<Map<String, Set<String>>> attributesCollectors = new ArrayDeque<>();
@Override
public void visitEntry(ConfigEntry<?> entry) {
Map<String, Set<String>> currentAttributesCollector = attributesCollectors.peekFirst();
if (currentAttributesCollector != null) {
for (String filterableAttribute : filterableAttributes) {
List<String> values = attributesExtension.getAttributeValues(entry, filterableAttribute);
if (!values.isEmpty()) {
currentAttributesCollector.computeIfAbsent(filterableAttribute, k -> new HashSet<>()).addAll(values);
}
}
}
}
@Override
public boolean enterCollectionEntry(ConfigEntry<?> entry) {
attributesCollectors.push(new HashMap<>());
visitEntry(entry);
return true;
}
@Override
public void leaveCollectionEntry(ConfigEntry<?> entry) {
leaveContainerEntry(entry);
}
@Override
public boolean enterCompoundEntry(ConfigEntry<?> entry) {
attributesCollectors.push(new HashMap<>());
visitEntry(entry);
return true;
}
@Override
public void leaveCompoundEntry(ConfigEntry<?> entry) {
leaveContainerEntry(entry);
}
private void leaveContainerEntry(ConfigEntry<?> entry) {
Map<String, Set<String>> entryAttributesCollector = attributesCollectors.pop();
entry.extensionsData().set(entryDataAccess, new EntryCustomData(entryAttributesCollector));
Map<String, Set<String>> outerAttributesCollector = attributesCollectors.peekFirst();
if (outerAttributesCollector != null) {
entryAttributesCollector.forEach((key, value) ->
outerAttributesCollector.computeIfAbsent(key, k -> new HashSet<>()).addAll(value)
);
}
}
});
}
@Override
public void addFilter(Patchwork contextExtensionsData, String key, String value) {
requireInitialized();
var contextCustomData = getOrCreateReadWriteContextCustomData(contextExtensionsData);
addFilterToRWContextData(key, value, contextCustomData);
}
private ReadWriteContextCustomData getOrCreateReadWriteContextCustomData(Patchwork patchwork) {
assert readWriteContextDataAccess != null;
ReadWriteContextCustomData readWriteContextCustomData = patchwork.get(readWriteContextDataAccess);
if (readWriteContextCustomData == null) {
readWriteContextCustomData = new ReadWriteContextCustomData();
patchwork.set(readWriteContextDataAccess, readWriteContextCustomData);
}
return readWriteContextCustomData;
}
private void addFilterToRWContextData(String key, String value, ReadWriteContextCustomData contextCustomData) {
if (filterableAttributes.contains(key)) {
contextCustomData.attributeFilters().computeIfAbsent(key, k -> new HashSet<>()).add(value);
} else {
throw new IllegalArgumentException("The attribute " + key + " has not been marked for filtering");
}
}
private void requireUninitialized() {
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) {
throw new IllegalStateException(
"Attribute optimization is only editable until the config has been initialized"
);
}
}
private void requireInitialized() {
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) < 0) {
throw new IllegalStateException("Config container must already be initialized");
}
}
private class ReaderMiddleware implements Middleware<TweedEntryReader<?, ?>> {
@Override
public String id() {
return ID;
}
@Override
public Set<String> mustComeBefore() {
return MIDDLEWARES_MUST_COME_BEFORE;
}
@Override
public Set<String> mustComeAfter() {
return MIDDLEWARES_MUST_COME_AFTER;
}
@Override
public TweedEntryReader<?, ?> process(TweedEntryReader<?, ?> inner) {
assert readWriteContextDataAccess != null;
//noinspection unchecked
TweedEntryReader<Object, ConfigEntry<Object>> innerCasted
= (TweedEntryReader<Object, @NonNull ConfigEntry<Object>>) inner;
return new TweedEntryReader<@Nullable Object, ConfigEntry<Object>>() {
@Override
public @Nullable Object read(
TweedDataReader reader,
ConfigEntry<Object> entry,
TweedReadContext context
) throws TweedEntryReadException {
ReadWriteContextCustomData contextData = context.extensionsData().get(readWriteContextDataAccess);
if (contextData == null || doFiltersMatch(entry, contextData)) {
return innerCasted.read(reader, entry, context);
}
TweedEntryReaderWriterImpls.NOOP_READER_WRITER.read(reader, entry, context);
// TODO: this should result in a noop instead of a null value
return null;
}
};
}
}
private class WriterMiddleware implements Middleware<TweedEntryWriter<?, ?>> {
@Override
public String id() {
return ID;
}
@Override
public Set<String> mustComeBefore() {
return MIDDLEWARES_MUST_COME_BEFORE;
}
@Override
public Set<String> mustComeAfter() {
return MIDDLEWARES_MUST_COME_AFTER;
}
@Override
public TweedEntryWriter<?, ?> process(TweedEntryWriter<?, ?> inner) {
assert readWriteContextDataAccess != null;
//noinspection unchecked
TweedEntryWriter<Object, ConfigEntry<Object>> innerCasted
= (TweedEntryWriter<Object, @NonNull ConfigEntry<Object>>) inner;
return (TweedEntryWriter<@Nullable Object, @NonNull ConfigEntry<@Nullable Object>>)
(writer, value, entry, context) -> {
ReadWriteContextCustomData contextData = context.extensionsData()
.get(readWriteContextDataAccess);
if (contextData == null || contextData.attributeFilters().isEmpty()) {
innerCasted.write(writer, value, entry, context);
return;
}
if (!contextData.writerInstalled()) {
writer = new MapEntryKeyDeferringWriter(writer);
contextData.writerInstalled(true);
}
if (doFiltersMatch(entry, contextData)) {
innerCasted.write(writer, value, entry, context);
} else {
try {
writer.visitValue(TWEED_DATA_NOTHING_VALUE);
} catch (TweedDataUnsupportedValueException ignored) {}
}
};
}
}
private boolean doFiltersMatch(ConfigEntry<?> entry, ReadWriteContextCustomData contextData) {
assert attributesExtension != null;
EntryCustomData entryCustomData = entry.extensionsData().get(entryDataAccess);
if (entryCustomData == null) {
for (Map.Entry<String, Set<String>> attributeFilter : contextData.attributeFilters().entrySet()) {
List<String> values = attributesExtension.getAttributeValues(entry, attributeFilter.getKey());
//noinspection SlowListContainsAll
if (!values.containsAll(attributeFilter.getValue())) {
return false;
}
}
return true;
}
for (Map.Entry<String, Set<String>> attributeFilter : contextData.attributeFilters().entrySet()) {
Set<String> values = entryCustomData.optimizedAttributes()
.getOrDefault(attributeFilter.getKey(), Collections.emptySet());
if (!values.containsAll(attributeFilter.getValue())) {
return false;
}
}
return true;
}
private static class MapEntryKeyDeferringWriter extends DelegatingTweedDataVisitor {
private final Deque<Boolean> mapContext = new ArrayDeque<>();
private final Deque<TweedDataDecoration> preDecorationQueue = new ArrayDeque<>();
private final Deque<TweedDataDecoration> postDecorationQueue = new ArrayDeque<>();
private @Nullable String mapEntryKey;
protected MapEntryKeyDeferringWriter(TweedDataVisitor delegate) {
super(delegate);
mapContext.push(false);
}
@Override
public void visitMapStart() {
beforeValueWrite();
mapContext.push(true);
delegate.visitMapStart();
}
@Override
public void visitMapEntryKey(String key) {
if (mapEntryKey != null) {
throw new IllegalStateException("The map entry key has already been visited");
} else {
mapEntryKey = key;
}
}
@Override
public void visitMapEnd() {
if (mapEntryKey != null) {
throw new IllegalArgumentException("Reached end of map while waiting for value for key " + mapEntryKey);
}
TweedDataDecoration decoration;
while ((decoration = preDecorationQueue.pollFirst()) != null) {
super.visitDecoration(decoration);
}
super.visitMapEnd();
mapContext.pop();
}
@Override
public void visitListStart() {
beforeValueWrite();
mapContext.push(false);
delegate.visitListStart();
}
@Override
public void visitListEnd() {
super.visitListEnd();
mapContext.pop();
}
@Override
public void visitValue(@Nullable Object value) throws TweedDataUnsupportedValueException {
if (value == TWEED_DATA_NOTHING_VALUE) {
preDecorationQueue.clear();
postDecorationQueue.clear();
mapEntryKey = null;
return;
}
super.visitValue(value);
}
@Override
public void visitDecoration(TweedDataDecoration decoration) {
if (Boolean.TRUE.equals(mapContext.peekFirst())) {
if (mapEntryKey == null) {
preDecorationQueue.addLast(decoration);
} else {
postDecorationQueue.addLast(decoration);
}
} else {
super.visitDecoration(decoration);
}
}
@Override
protected void beforeValueWrite() {
super.beforeValueWrite();
if (mapEntryKey != null) {
TweedDataDecoration decoration;
while ((decoration = preDecorationQueue.pollFirst()) != null) {
super.visitDecoration(decoration);
}
super.visitMapEntryKey(mapEntryKey);
mapEntryKey = null;
while ((decoration = postDecorationQueue.pollFirst()) != null) {
super.visitDecoration(decoration);
}
}
}
}
@Getter
@RequiredArgsConstructor
private static class EntryCustomData {
private final Map<String, Set<String>> optimizedAttributes;
}
@Getter
@Setter
private static class ReadWriteContextCustomData {
private final Map<String, Set<String>> attributeFilters = new HashMap<>();
private boolean writerInstalled;
}
}

View File

@@ -0,0 +1,6 @@
@ApiStatus.Internal
@NullMarked
package de.siphalor.tweed5.attributesextension.impl.serde.filter;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,183 @@
package de.siphalor.tweed5.attributesextension.impl.serde.filter;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.attributesextension.api.serde.filter.AttributesReadWriteFilterExtension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.SimpleConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import de.siphalor.tweed5.core.impl.entry.StaticMapCompoundConfigEntryImpl;
import de.siphalor.tweed5.data.extension.api.ReadWriteExtension;
import de.siphalor.tweed5.data.hjson.HjsonLexer;
import de.siphalor.tweed5.data.hjson.HjsonReader;
import de.siphalor.tweed5.data.hjson.HjsonWriter;
import org.jspecify.annotations.NullUnmarked;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static de.siphalor.tweed5.attributesextension.api.AttributesExtension.attribute;
import static de.siphalor.tweed5.attributesextension.api.AttributesExtension.attributeDefault;
import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.*;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.compoundReaderWriter;
import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.stringReaderWriter;
import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.map;
@NullUnmarked
class AttributesReadWriteFilterExtensionImplTest {
ConfigContainer<Map<String, Object>> configContainer;
CompoundConfigEntry<Map<String, Object>> rootEntry;
SimpleConfigEntry<String> firstEntry;
SimpleConfigEntry<String> secondEntry;
CompoundConfigEntry<Map<String, Object>> nestedEntry;
SimpleConfigEntry<String> nestedFirstEntry;
SimpleConfigEntry<String> nestedSecondEntry;
AttributesReadWriteFilterExtension attributesReadWriteFilterExtension;
@BeforeEach
void setUp() {
configContainer = new DefaultConfigContainer<>();
configContainer.registerExtension(AttributesReadWriteFilterExtensionImpl.class);
configContainer.registerExtension(ReadWriteExtension.class);
configContainer.registerExtension(AttributesExtension.class);
configContainer.finishExtensionSetup();
nestedFirstEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()))
.apply(attribute("type", "a"))
.apply(attribute("sync", "true"));
nestedSecondEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()));
//noinspection unchecked
nestedEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (Class<?>) Map.class,
HashMap::new,
sequencedMap(List.of(entry("first", nestedFirstEntry), entry("second", nestedSecondEntry)))
)
.apply(entryReaderWriter(compoundReaderWriter()))
.apply(attributeDefault("type", "c"));
firstEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()))
.apply(attribute("type", "a"))
.apply(attribute("sync", "true"));
secondEntry = new SimpleConfigEntryImpl<>(configContainer, String.class)
.apply(entryReaderWriter(stringReaderWriter()));
//noinspection unchecked
rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (Class<?>) Map.class,
HashMap::new,
sequencedMap(List.of(
entry("first", firstEntry),
entry("second", secondEntry),
entry("nested", nestedEntry)
))
)
.apply(entryReaderWriter(compoundReaderWriter()))
.apply(attributeDefault("type", "b"));
configContainer.attachTree(rootEntry);
attributesReadWriteFilterExtension = configContainer.extension(AttributesReadWriteFilterExtension.class)
.orElseThrow();
attributesReadWriteFilterExtension.markAttributeForFiltering("type");
attributesReadWriteFilterExtension.markAttributeForFiltering("sync");
configContainer.initialize();
}
@ParameterizedTest
@CsvSource(quoteCharacter = '`', textBlock = """
a,`{
\tfirst: 1st
\tnested: {
\t\tfirst: n 1st
\t}
}
`
b,`{
\tsecond: 2nd
}
`
c,`{
\tnested: {
\t\tsecond: n 2nd
\t}
}
`
"""
)
void writeWithType(String type, String serialized) {
var writer = new StringWriter();
configContainer.rootEntry().apply(write(
new HjsonWriter(writer, new HjsonWriter.Options()),
Map.of("first", "1st", "second", "2nd", "nested", Map.of("first", "n 1st", "second", "n 2nd")),
patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "type" , type)
));
assertThat(writer.toString()).isEqualTo(serialized);
}
@Test
void writeWithSync() {
var writer = new StringWriter();
configContainer.rootEntry().apply(write(
new HjsonWriter(writer, new HjsonWriter.Options()),
Map.of("first", "1st", "second", "2nd", "nested", Map.of("first", "n 1st", "second", "n 2nd")),
patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "sync" , "true")
));
assertThat(writer.toString()).isEqualTo("""
{
\tfirst: 1st
\tnested: {
\t\tfirst: n 1st
\t}
}
""");
}
@Test
void readWithType() {
HjsonReader reader = new HjsonReader(new HjsonLexer(new StringReader("""
{
first: 1st
second: 2nd
nested: {
first: n 1st
second: n 2nd
}
}
""")));
Map<String, Object> readValue = configContainer.rootEntry().call(read(
reader,
patchwork -> attributesReadWriteFilterExtension.addFilter(patchwork, "type", "a")
));
assertThat(readValue)
.containsEntry("first", "1st")
.containsEntry("second", null)
.hasEntrySatisfying(
"nested", nested -> assertThat(nested)
.asInstanceOf(map(String.class, Object.class))
.containsEntry("first", "n 1st")
.containsEntry("second", null)
);
}
}

View File

@@ -15,6 +15,9 @@ public interface TweedExtension {
default void extensionsFinalized() {
}
default void initialize() {
}
default void initEntry(ConfigEntry<?> configEntry) {
}
}

View File

@@ -217,6 +217,10 @@ public class DefaultConfigContainer<T> implements ConfigContainer<T> {
public void initialize() {
requireSetupPhase(ConfigContainerSetupPhase.TREE_ATTACHED);
for (TweedExtension extension : extensions()) {
extension.initialize();
}
assert rootEntry != null;
rootEntry.visitInOrder(entry -> {
for (TweedExtension extension : extensions()) {

View File

@@ -9,6 +9,13 @@ import static org.junit.jupiter.api.Assertions.*;
class AcyclicGraphSorterTest {
@Test
void trivialSort() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(2);
sorter.addEdge(0, 1);
assertArrayEquals(new int[]{ 0, 1 }, assertDoesNotThrow(sorter::sort));
}
@Test
void sort1() {
AcyclicGraphSorter sorter = new AcyclicGraphSorter(4);
@@ -73,4 +80,4 @@ class AcyclicGraphSorterTest {
AcyclicGraphSorter.GraphCycleException exception = assertThrows(AcyclicGraphSorter.GraphCycleException.class, sorter::sort);
assertEquals(Arrays.asList(0, 1), exception.cycleIndeces());
}
}
}

View File

@@ -15,6 +15,7 @@ import java.util.regex.Pattern;
public class HjsonWriter implements TweedDataVisitor {
private static final int PREFILL_INDENT = 10;
private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\\n|\\r\\n");
private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?$");
private final Writer writer;
private final Options options;
@@ -101,8 +102,11 @@ public class HjsonWriter implements TweedDataVisitor {
if (value.isEmpty() || "true".equals(value) || "false".equals(value) || "null".equals(value)) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
if (NUMBER_PATTERN.matcher(value).matches()) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
int firstCodePoint = value.codePointAt(0);
if (Character.isDigit(firstCodePoint) || Character.isWhitespace(firstCodePoint)) {
if (Character.isWhitespace(firstCodePoint)) {
return HjsonStringType.INLINE_DOUBLE_QUOTE;
}
int lastCodePoint = value.codePointBefore(value.length());

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.utils.api;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UniqueSymbol {
private final String displayName;
@Override
public String toString() {
return "UniqueSymbol@" + System.identityHashCode(this) + "{" + displayName + "}";
}
}

View File

@@ -0,0 +1,195 @@
package de.siphalor.tweed5.utils.api.collection;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Array;
import java.util.*;
import java.util.stream.IntStream;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class ImmutableArrayBackedMap<K, V> implements SortedMap<K, V> {
private final K[] keys;
private final V[] values;
@SuppressWarnings("unchecked")
public static <K extends Comparable<K>, V> SortedMap<K, V> ofEntries(Collection<Map.Entry<K, V>> entries) {
if (entries.isEmpty()) {
return Collections.emptySortedMap();
}
Entry<K, V> any = entries.iterator().next();
int size = entries.size();
K[] keys = (K[]) Array.newInstance(any.getKey().getClass(), size);
V[] values = (V[]) Array.newInstance(any.getValue().getClass(), size);
int i = 0;
Iterator<Entry<K, V>> iterator = entries.stream().sorted(Entry.comparingByKey()).iterator();
while (iterator.hasNext()) {
Entry<K, V> entry = iterator.next();
keys[i] = entry.getKey();
values[i] = entry.getValue();
i++;
}
return new ImmutableArrayBackedMap<>(keys, values);
}
@Override
public @Nullable Comparator<? super K> comparator() {
return null;
}
@Override
public @NonNull SortedMap<K, V> subMap(K fromKey, K toKey) {
int from = findKey(fromKey);
if (from < 0) {
from = -from - 1;
}
if (from == 0) {
return headMap(toKey);
} else if (from >= keys.length) {
return Collections.emptySortedMap();
}
int to = findKey(toKey, from + 1);
if (to < 0) {
to = -to - 1;
}
if (to == keys.length) {
return this;
} else if (to == from) {
return Collections.emptySortedMap();
}
return new ImmutableArrayBackedMap<>(Arrays.copyOfRange(keys, from, to), Arrays.copyOfRange(values, from, to));
}
@Override
public @NonNull SortedMap<K, V> headMap(K toKey) {
int to = findKey(toKey);
if (to < 0) {
to = -to - 1;
}
if (to == keys.length) {
return this;
} else if (to == 0) {
return Collections.emptySortedMap();
}
return new ImmutableArrayBackedMap<>(Arrays.copyOf(keys, to), Arrays.copyOf(values, to));
}
@Override
public @NonNull SortedMap<K, V> tailMap(K fromKey) {
int from = findKey(fromKey);
if (from < 0) {
from = -from - 1;
}
if (from == 0) {
return this;
} else if (from >= keys.length) {
return Collections.emptySortedMap();
}
return new ImmutableArrayBackedMap<>(
Arrays.copyOfRange(keys, from, keys.length),
Arrays.copyOfRange(values, from, keys.length)
);
}
@Override
public K firstKey() {
return keys[0];
}
@Override
public K lastKey() {
return keys[keys.length - 1];
}
@Override
public int size() {
return keys.length;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean containsKey(Object key) {
return findKey(key) >= 0;
}
@Override
public boolean containsValue(Object value) {
return Arrays.binarySearch(values, value) >= 0;
}
@Override
public @Nullable V get(Object key) {
int index = findKey(key);
if (index < 0) {
return null;
}
return values[index];
}
@Override
public @Nullable V put(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public V remove(Object key) {
throw new UnsupportedOperationException();
}
@Override
public void putAll(@NonNull Map<? extends K, ? extends V> m) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Set<K> keySet() {
return new ImmutableArrayBackedSet<>(keys);
}
@Override
public Collection<V> values() {
return new AbstractList<V>() {
@Override
public V get(int index) {
return values[index];
}
@Override
public int size() {
return values.length;
}
};
}
@Override
public Set<Entry<K, V>> entrySet() {
//noinspection unchecked
return new ImmutableArrayBackedSet<Entry<K ,V>>(
IntStream.range(0, keys.length)
.mapToObj(index -> new AbstractMap.SimpleEntry<>(keys[index], values[index]))
.toArray(Entry[]::new)
);
}
private int findKey(Object key) {
return Arrays.binarySearch(keys, key);
}
private int findKey(Object key, int from) {
return Arrays.binarySearch(keys, from, keys.length, key);
}
}

View File

@@ -0,0 +1,169 @@
package de.siphalor.tweed5.utils.api.collection;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import java.lang.reflect.Array;
import java.util.*;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class ImmutableArrayBackedSet<T> implements SortedSet<T> {
private final T[] values;
public static <T extends Comparable<T>> SortedSet<T> of(Collection<T> collection) {
if (collection.isEmpty()) {
return Collections.emptySortedSet();
}
T first = collection.iterator().next();
//noinspection unchecked
return new ImmutableArrayBackedSet<>(
collection.stream().sorted().toArray(length -> (T[]) Array.newInstance(first.getClass(), length))
);
}
@SafeVarargs
public static <T extends Comparable<T>> SortedSet<T> of(T... values) {
if (values.length == 0) {
return Collections.emptySortedSet();
}
Arrays.sort(values);
return new ImmutableArrayBackedSet<>(values);
}
@Override
public @Nullable Comparator<? super T> comparator() {
return null;
}
@Override
public @NonNull SortedSet<T> subSet(T fromElement, T toElement) {
int from = Arrays.binarySearch(values, fromElement);
if (from < 0) {
from = -from - 1;
}
if (from == 0) {
return headSet(toElement);
}
int to = Arrays.binarySearch(values, toElement);
if (to < 0) {
to = -to - 1;
}
if (to == values.length) {
return this;
}
return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, from, to));
}
@Override
public @NonNull SortedSet<T> headSet(T toElement) {
int to = Arrays.binarySearch(values, toElement);
if (to < 0) {
to = -to - 1;
}
if (to == values.length) {
return this;
}
return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, 0, to));
}
@Override
public @NonNull SortedSet<T> tailSet(T fromElement) {
int from = Arrays.binarySearch(values, fromElement);
if (from < 0) {
from = -from - 1;
}
if (from == 0) {
return this;
}
return new ImmutableArrayBackedSet<>(Arrays.copyOfRange(values, from, values.length));
}
@Override
public T first() {
return values[0];
}
@Override
public T last() {
return values[values.length - 1];
}
@Override
public int size() {
return values.length;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean contains(Object o) {
return Arrays.binarySearch(values, o) >= 0;
}
@Override
public @NonNull Iterator<T> iterator() {
return Arrays.stream(values).iterator();
}
@Override
public @NonNull Object[] toArray() {
return Arrays.copyOf(values, values.length);
}
@Override
public @NonNull <T1> T1[] toArray(@NonNull T1[] a) {
// basically copied from ArrayList#toArray(Object[])
if (a.length < values.length) {
//noinspection unchecked
return (T1[]) toArray();
}
//noinspection SuspiciousSystemArraycopy
System.arraycopy(values, 0, a, 0, values.length);
if (a.length > values.length) {
//noinspection DataFlowIssue
a[values.length] = null;
}
return a;
}
@Override
public boolean add(T t) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(@NonNull Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(@NonNull Collection<? extends T> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(@NonNull Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(@NonNull Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,194 @@
package de.siphalor.tweed5.utils.api.collection;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ImmutableArrayBackedMapTest {
@Test
void ofEntriesEmpty() {
assertThat(ImmutableArrayBackedMap.ofEntries(Collections.emptyList())).isSameAs(Collections.emptySortedMap());
}
@Test
void ofEntries() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10),
Map.entry(3, 30)
));
assertThat(map).containsExactly(Map.entry(1, 10), Map.entry(2, 20), Map.entry(3, 30));
}
@Test
void comparator() {
assertThat(ImmutableArrayBackedMap.ofEntries(List.of(Map.entry(1, 10))).comparator()).isNull();
}
@Test
void subMap() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(40, 400),
Map.entry(20, 200),
Map.entry(10, 100),
Map.entry(30, 300)
));
assertThat(map.subMap(1, 100)).isSameAs(map);
assertThat(map.subMap(10, 40)).containsExactly(Map.entry(10, 100), Map.entry(20, 200), Map.entry(30, 300));
assertThat(map.subMap(0, 20)).containsExactly(Map.entry(10, 100));
assertThat(map.subMap(0, 1)).isEmpty();
assertThat(map.subMap(41, 100)).isEmpty();
}
@Test
void headMap() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(40, 400),
Map.entry(20, 200),
Map.entry(10, 100),
Map.entry(30, 300)
));
assertThat(map.headMap(41)).isSameAs(map);
assertThat(map.headMap(40)).containsExactly(Map.entry(10, 100), Map.entry(20, 200), Map.entry(30, 300));
assertThat(map.headMap(10)).isEmpty();
assertThat(map.headMap(0)).isEmpty();
}
@Test
void tailMap() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(40, 400),
Map.entry(20, 200),
Map.entry(10, 100),
Map.entry(30, 300)
));
assertThat(map.tailMap(0)).isSameAs(map);
assertThat(map.tailMap(10)).isSameAs(map);
assertThat(map.tailMap(30)).containsExactly(Map.entry(30, 300), Map.entry(40, 400));
assertThat(map.tailMap(41)).isEmpty();
}
@Test
void firstKey() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map.firstKey()).isEqualTo(1);
}
@Test
void lastKey() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map.lastKey()).isEqualTo(2);
}
@Test
void size() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map).hasSize(2);
assertThat(map).isNotEmpty();
}
@Test
void containsKey() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map.containsKey(0)).isFalse();
assertThat(map.containsKey(1)).isTrue();
assertThat(map.containsKey(2)).isTrue();
assertThat(map.containsKey(3)).isFalse();
}
@Test
void containsValue() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map.containsValue(0)).isFalse();
assertThat(map.containsValue(10)).isTrue();
assertThat(map.containsValue(20)).isTrue();
assertThat(map.containsValue(30)).isFalse();
}
@Test
void get() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThat(map.get(0)).isNull();
assertThat(map.get(1)).isEqualTo(10);
assertThat(map.get(2)).isEqualTo(20);
assertThat(map.get(3)).isNull();
}
@Test
void unsupported() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10)
));
assertThatThrownBy(() -> map.put(5, 50)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> map.putAll(Map.of(3, 30, 5, 50))).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> map.remove(2)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(map::clear).isInstanceOf(UnsupportedOperationException.class);
}
@Test
void keySet() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 20),
Map.entry(1, 10),
Map.entry(3, 30)
));
Set<Integer> keySet = map.keySet();
assertThat(keySet).containsExactly(1, 2, 3).hasSize(3);
assertThatThrownBy(() -> keySet.add(4)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> keySet.remove(2)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(keySet::clear).isInstanceOf(UnsupportedOperationException.class);
}
@Test
void values() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 40),
Map.entry(1, 90),
Map.entry(3, 10)
));
Collection<Integer> values = map.values();
assertThat(values).containsExactly(90, 40, 10).hasSize(3);
assertThatThrownBy(() -> values.add(50)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> values.remove(10)).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(values::clear).isInstanceOf(UnsupportedOperationException.class);
}
@Test
void entrySet() {
SortedMap<Integer, Integer> map = ImmutableArrayBackedMap.ofEntries(List.of(
Map.entry(2, 40),
Map.entry(1, 90),
Map.entry(3, 10)
));
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
assertThat(entrySet).containsExactly(Map.entry(1, 90), Map.entry(2, 40), Map.entry(3, 10)).hasSize(3);
assertThatThrownBy(() -> entrySet.add(Map.entry(4, 50))).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> entrySet.remove(Map.entry(1, 90))).isInstanceOf(UnsupportedOperationException.class);
}
}

View File

@@ -0,0 +1,11 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
api(project(":tweed5-weaver-pojo"))
api(project(":tweed5-attributes-extension"))
testImplementation(project(":tweed5-default-extensions"))
testImplementation(project(":tweed5-serde-hjson"))
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE})
@Repeatable(Attributes.class)
public @interface Attribute {
String key();
String[] values();
}

View File

@@ -0,0 +1,11 @@
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE})
@Repeatable(AttributeDefaults.class)
public @interface AttributeDefault {
String key();
String[] defaultValue();
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
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, ElementType.TYPE_USE})
public @interface AttributeDefaults {
AttributeDefault[] value();
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
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, ElementType.TYPE_USE})
public @interface Attributes {
Attribute[] value();
}

View File

@@ -0,0 +1,75 @@
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
import de.siphalor.tweed5.attributesextension.api.AttributesExtension;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
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 lombok.var;
import org.jetbrains.annotations.ApiStatus;
import java.util.*;
import java.util.function.Function;
public class AttributesPojoWeavingProcessor implements TweedPojoWeavingExtension {
AttributesExtension attributesExtension;
@ApiStatus.Internal
public AttributesPojoWeavingProcessor(ConfigContainer<?> configContainer) {
attributesExtension = configContainer.extension(AttributesExtension.class)
.orElseThrow(() -> new IllegalStateException(
"You must register a " + AttributesExtension.class.getSimpleName()
+ " to use the " + getClass().getSimpleName()
));
}
@Override
public void setup(SetupContext context) {
}
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
var attributeAnnotations = context.annotations().getAnnotationsByType(Attribute.class);
var attributes = collectAttributesFromAnnotations(attributeAnnotations, Attribute::key, Attribute::values);
attributes.forEach((key, values) -> attributesExtension.setAttribute(configEntry, key, values));
var attributeDefaultAnnotations = context.annotations().getAnnotationsByType(AttributeDefault.class);
var attributeDefaults = collectAttributesFromAnnotations(
attributeDefaultAnnotations,
AttributeDefault::key,
AttributeDefault::defaultValue
);
attributeDefaults.forEach((key, values) -> attributesExtension.setAttributeDefault(configEntry, key, values));
}
private <T> Map<String, List<String>> collectAttributesFromAnnotations(
T[] annotations,
Function<T, String> keyGetter,
Function<T, String[]> valueGetter
) {
if (annotations.length == 0) {
return Collections.emptyMap();
}
Map<String, List<String>> attributes;
if (annotations.length == 1) {
return Collections.singletonMap(
keyGetter.apply(annotations[0]),
Arrays.asList(valueGetter.apply(annotations[0]))
);
} else if (annotations.length <= 12) {
attributes = new TreeMap<>();
} else {
attributes = new HashMap<>();
}
for (T annotation : annotations) {
attributes.computeIfAbsent(keyGetter.apply(annotation), k -> new ArrayList<>())
.addAll(Arrays.asList(valueGetter.apply(annotation)));
}
return attributes;
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojoext.attributes.api;
import org.jspecify.annotations.NullMarked;