[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

@@ -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)
);
}
}