[*] Generalize the entry system by introducing "structured entries"

This commit is contained in:
2025-08-12 10:04:05 +02:00
parent 6f2e715b2a
commit 83cbe91e35
22 changed files with 307 additions and 288 deletions

View File

@@ -1,62 +1,15 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import org.jspecify.annotations.Nullable;
import de.siphalor.tweed5.defaultextensions.pather.impl.PathTrackingImpl;
import java.util.ArrayDeque;
import java.util.Deque;
public class PathTracking {
private final StringBuilder pathBuilder = new StringBuilder(256);
private final Deque<Context> contextStack = new ArrayDeque<>(50);
private final Deque<String> pathParts = new ArrayDeque<>(50);
private final Deque<Integer> listIndexes = new ArrayDeque<>(10);
public @Nullable Context currentContext() {
return contextStack.peek();
public interface PathTracking {
static PathTracking create() {
return new PathTrackingImpl();
}
public void popContext() {
if (contextStack.pop() == Context.LIST) {
listIndexes.pop();
popPathPart();
}
}
void pushPathPart(String pathPart);
public void pushMapContext() {
contextStack.push(Context.MAP);
}
void popPathPart();
public void pushPathPart(String part) {
pathParts.push(part);
pathBuilder.append(".").append(part);
}
public void popPathPart() {
if (!pathParts.isEmpty()) {
String poppedPart = pathParts.pop();
pathBuilder.setLength(pathBuilder.length() - poppedPart.length() - 1);
}
}
public void pushListContext() {
contextStack.push(Context.LIST);
listIndexes.push(0);
pushPathPart("0");
}
public int incrementListIndex() {
int index = listIndexes.pop() + 1;
listIndexes.push(index);
popPathPart();
pushPathPart(Integer.toString(index));
return index;
}
public String currentPath() {
return pathBuilder.toString();
}
public enum Context {
LIST, MAP,
}
String currentPath();
}

View File

@@ -8,64 +8,35 @@ import org.jspecify.annotations.Nullable;
@RequiredArgsConstructor
public class PathTrackingConfigEntryValueVisitor implements ConfigEntryValueVisitor {
private final ConfigEntryValueVisitor delegate;
private final PathTracking pathTracking;
private final ValuePathTracking pathTracking;
@Override
public <T extends @Nullable Object> void visitEntry(ConfigEntry<T> entry, T value) {
delegate.visitEntry(entry, value);
entryVisited();
}
@Override
public <T> boolean enterCollectionEntry(ConfigEntry<T> entry, T value) {
boolean enter = delegate.enterCollectionEntry(entry, value);
public <T> boolean enterStructuredEntry(ConfigEntry<T> entry, T value) {
return delegate.enterStructuredEntry(entry, value);
}
@Override
public boolean enterStructuredSubEntry(String entryKey, String valueKey) {
boolean enter = delegate.enterStructuredSubEntry(entryKey, valueKey);
if (enter) {
pathTracking.pushListContext();
pathTracking.pushPathPart(entryKey, valueKey);
}
return enter;
}
@Override
public <T> void leaveCollectionEntry(ConfigEntry<T> entry, T value) {
delegate.leaveCollectionEntry(entry, value);
pathTracking.popContext();
entryVisited();
}
@Override
public <T> boolean enterCompoundEntry(ConfigEntry<T> entry, T value) {
boolean enter = delegate.enterCompoundEntry(entry, value);
if (enter) {
pathTracking.pushMapContext();
}
return enter;
}
@Override
public boolean enterCompoundSubEntry(String key) {
boolean enter = delegate.enterCompoundSubEntry(key);
if (enter) {
pathTracking.pushPathPart(key);
}
return enter;
}
@Override
public void leaveCompoundSubEntry(String key) {
delegate.leaveCompoundSubEntry(key);
public void leaveStructuredSubEntry(String entryKey, String valueKey) {
delegate.leaveStructuredSubEntry(entryKey, valueKey);
pathTracking.popPathPart();
}
@Override
public <T> void leaveCompoundEntry(ConfigEntry<T> entry, T value) {
delegate.leaveCompoundEntry(entry, value);
pathTracking.popContext();
entryVisited();
}
private void entryVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
}
public <T> void leaveStructuredEntry(ConfigEntry<T> entry, T value) {
delegate.leaveStructuredEntry(entry, value);
}
}

View File

@@ -12,37 +12,16 @@ public class PathTrackingConfigEntryVisitor implements ConfigEntryVisitor {
@Override
public void visitEntry(ConfigEntry<?> entry) {
delegate.visitEntry(entry);
entryVisited();
}
@Override
public boolean enterCollectionEntry(ConfigEntry<?> entry) {
boolean enter = delegate.enterCollectionEntry(entry);
if (enter) {
pathTracking.pushListContext();
}
return enter;
public boolean enterStructuredEntry(ConfigEntry<?> entry) {
return delegate.enterStructuredEntry(entry);
}
@Override
public void leaveCollectionEntry(ConfigEntry<?> entry) {
delegate.leaveCollectionEntry(entry);
pathTracking.popContext();
entryVisited();
}
@Override
public boolean enterCompoundEntry(ConfigEntry<?> entry) {
boolean enter = delegate.enterCompoundEntry(entry);
if (enter) {
pathTracking.pushMapContext();
}
return enter;
}
@Override
public boolean enterCompoundSubEntry(String key) {
boolean enter = delegate.enterCompoundSubEntry(key);
public boolean enterStructuredSubEntry(String key) {
boolean enter = delegate.enterStructuredSubEntry(key);
if (enter) {
pathTracking.pushPathPart(key);
}
@@ -50,21 +29,13 @@ public class PathTrackingConfigEntryVisitor implements ConfigEntryVisitor {
}
@Override
public void leaveCompoundSubEntry(String key) {
delegate.leaveCompoundSubEntry(key);
public void leaveStructuredSubEntry(String key) {
delegate.leaveStructuredSubEntry(key);
pathTracking.popPathPart();
}
@Override
public void leaveCompoundEntry(ConfigEntry<?> entry) {
delegate.leaveCompoundEntry(entry);
pathTracking.popContext();
entryVisited();
}
private void entryVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
}
public void leaveStructuredEntry(ConfigEntry<?> entry) {
delegate.leaveStructuredEntry(entry);
}
}

View File

@@ -5,10 +5,14 @@ import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import lombok.RequiredArgsConstructor;
import java.util.ArrayDeque;
@RequiredArgsConstructor
public class PathTrackingDataReader implements TweedDataReader {
private final TweedDataReader delegate;
private final PathTracking pathTracking;
private final ArrayDeque<Context> contextStack = new ArrayDeque<>(50);
private final ArrayDeque<Integer> listIndexStack = new ArrayDeque<>(50);
@Override
public TweedDataToken peekToken() throws TweedDataReadException {
@@ -18,21 +22,35 @@ public class PathTrackingDataReader implements TweedDataReader {
@Override
public TweedDataToken readToken() throws TweedDataReadException {
TweedDataToken token = delegate.readToken();
if (token.isListValue()) {
if (contextStack.peek() == Context.LIST) {
int index = listIndexStack.pop() + 1;
if (index != 0) {
pathTracking.popPathPart();
}
pathTracking.pushPathPart(Integer.toString(index));
listIndexStack.push(index);
}
}
if (token.isListStart()) {
pathTracking.pushListContext();
} else if (token.isListValue()) {
pathTracking.incrementListIndex();
contextStack.push(Context.LIST);
listIndexStack.push(-1);
} else if (token.isListEnd()) {
pathTracking.popContext();
contextStack.pop();
int lastIndex = listIndexStack.pop();
if (lastIndex >= 0) {
pathTracking.popPathPart();
}
} else if (token.isMapStart()) {
pathTracking.pushMapContext();
contextStack.push(Context.MAP);
pathTracking.pushPathPart("$");
} else if (token.isMapEntryKey()) {
pathTracking.popPathPart();
pathTracking.pushPathPart(token.readAsString());
} else if (token.isMapEnd()) {
pathTracking.popPathPart();
pathTracking.popContext();
contextStack.pop();
}
return token;
}
@@ -41,4 +59,8 @@ public class PathTrackingDataReader implements TweedDataReader {
public void close() throws Exception {
delegate.close();
}
private enum Context {
LIST, MAP,
}
}

View File

@@ -6,10 +6,14 @@ import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import java.util.ArrayDeque;
@RequiredArgsConstructor
public class PathTrackingDataVisitor implements TweedDataVisitor {
private final TweedDataVisitor delegate;
private final PathTracking pathTracking;
private final ArrayDeque<Context> contextStack = new ArrayDeque<>(50);
private final ArrayDeque<Integer> listIndexStack = new ArrayDeque<>(50);
@Override
public void visitNull() {
@@ -72,42 +76,50 @@ public class PathTrackingDataVisitor implements TweedDataVisitor {
}
private void valueVisited() {
if (pathTracking.currentContext() == PathTracking.Context.LIST) {
pathTracking.incrementListIndex();
} else {
Context context = contextStack.peek();
if (context == Context.MAP_ENTRY) {
contextStack.pop();
pathTracking.popPathPart();
} else if (context == Context.LIST) {
pathTracking.popPathPart();
int index = listIndexStack.pop();
listIndexStack.push(index + 1);
pathTracking.pushPathPart(Integer.toString(index));
}
}
@Override
public void visitListStart() {
delegate.visitListStart();
pathTracking.pushListContext();
contextStack.push(Context.LIST);
listIndexStack.push(0);
pathTracking.pushPathPart("0");
}
@Override
public void visitListEnd() {
delegate.visitListEnd();
pathTracking.popContext();
contextStack.pop();
listIndexStack.pop();
pathTracking.popPathPart();
valueVisited();
}
@Override
public void visitMapStart() {
delegate.visitMapStart();
pathTracking.pushMapContext();
}
@Override
public void visitMapEntryKey(String key) {
delegate.visitMapEntryKey(key);
pathTracking.pushPathPart(key);
contextStack.push(Context.MAP_ENTRY);
}
@Override
public void visitMapEnd() {
delegate.visitMapEnd();
pathTracking.popContext();
valueVisited();
}
@@ -115,4 +127,8 @@ public class PathTrackingDataVisitor implements TweedDataVisitor {
public void visitDecoration(TweedDataDecoration decoration) {
delegate.visitDecoration(decoration);
}
private enum Context {
LIST, MAP_ENTRY,
}
}

View File

@@ -0,0 +1,34 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import lombok.NoArgsConstructor;
@NoArgsConstructor(staticName = "create")
public final class ValuePathTracking implements PathTracking {
private final PathTracking entryPathTracking = PathTracking.create();
private final PathTracking valuePathTracking = PathTracking.create();
@Override
public void pushPathPart(String pathPart) {
this.pushPathPart(pathPart, pathPart);
}
public void pushPathPart(String entryPathPart, String valuePathPart) {
entryPathTracking.pushPathPart(entryPathPart);
valuePathTracking.pushPathPart(valuePathPart);
}
@Override
public void popPathPart() {
valuePathTracking.popPathPart();
entryPathTracking.popPathPart();
}
@Override
public String currentPath() {
return valuePathTracking.currentPath();
}
public String currentEntryPath() {
return entryPathTracking.currentPath();
}
}

View File

@@ -0,0 +1,30 @@
package de.siphalor.tweed5.defaultextensions.pather.impl;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import java.util.ArrayDeque;
import java.util.Deque;
public class PathTrackingImpl implements PathTracking {
private final StringBuilder pathBuilder = new StringBuilder(256);
private final Deque<String> pathParts = new ArrayDeque<>(50);
@Override
public void pushPathPart(String entryPathPart) {
pathParts.push(entryPathPart);
pathBuilder.append(".").append(entryPathPart);
}
@Override
public void popPathPart() {
if (!pathParts.isEmpty()) {
String poppedPart = pathParts.pop();
pathBuilder.setLength(pathBuilder.length() - poppedPart.length() - 1);
}
}
@Override
public String currentPath() {
return pathBuilder.toString();
}
}

View File

@@ -6,7 +6,6 @@ import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.data.extension.api.*;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteExtensionSetupContext;
import de.siphalor.tweed5.data.extension.api.extension.ReadWriteRelatedExtension;
import de.siphalor.tweed5.dataapi.api.DelegatingTweedDataWriter;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
@@ -71,7 +70,7 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
return castedInner.read(reader, entry, context);
}
pathTracking = new PathTracking();
pathTracking = PathTracking.create();
context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
try {
return castedInner.read(new PathTrackingDataReader(reader, pathTracking), entry, context);
@@ -80,7 +79,7 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
if (exceptionPathTracking != null) {
throw new TweedEntryReadException(
"Exception while reading entry at "
+ String.join("/", exceptionPathTracking.currentPath())
+ exceptionPathTracking.currentPath()
+ ": " + e.getMessage(),
e
);
@@ -114,7 +113,7 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
return;
}
pathTracking = new PathTracking();
pathTracking = PathTracking.create();
context.extensionsData().set(rwContextPathTrackingAccess, pathTracking);
try {
castedInner.write(new PathTrackingDataVisitor(writer, pathTracking), value, entry, context);
@@ -123,7 +122,7 @@ public class PatherExtensionImpl implements PatherExtension, ReadWriteRelatedExt
if (exceptionPathTracking != null) {
throw new TweedEntryWriteException(
"Exception while writing entry at "
+ String.join("/", exceptionPathTracking.currentPath())
+ exceptionPathTracking.currentPath()
+ ": " + e.getMessage(),
e
);

View File

@@ -20,6 +20,7 @@ import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryValueVisitor;
import de.siphalor.tweed5.defaultextensions.pather.api.PatherExtension;
import de.siphalor.tweed5.defaultextensions.pather.api.ValuePathTracking;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationProvidingExtension;
@@ -177,7 +178,7 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
@Override
public <T> ValidationIssues validate(ConfigEntry<T> entry, @Nullable T value) {
PathTracking pathTracking = new PathTracking();
ValuePathTracking pathTracking = ValuePathTracking.create();
ValidatingConfigEntryVisitor validatingVisitor = new ValidatingConfigEntryVisitor(pathTracking);
entry.visitInOrder(new PathTrackingConfigEntryValueVisitor(validatingVisitor, pathTracking), value);
@@ -281,22 +282,12 @@ public class ValidationExtensionImpl implements ReadWriteRelatedExtension, Valid
}
@Override
public <T> boolean enterCollectionEntry(ConfigEntry<T> entry, T value) {
public <T> boolean enterStructuredEntry(ConfigEntry<T> entry, T value) {
return true;
}
@Override
public <T> void leaveCollectionEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
}
@Override
public <T> boolean enterCompoundEntry(ConfigEntry<T> entry, T value) {
return true;
}
@Override
public <T> void leaveCompoundEntry(ConfigEntry<T> entry, T value) {
public <T> void leaveStructuredEntry(ConfigEntry<T> entry, T value) {
visitEntry(entry, value);
}
}

View File

@@ -0,0 +1,72 @@
package de.siphalor.tweed5.defaultextensions.pather.api;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataToken;
import de.siphalor.tweed5.dataapi.api.TweedDataTokens;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class PathTrackingDataReaderTest {
@SneakyThrows
@Test
void test() {
PathTracking pathTracking = PathTracking.create();
TweedDataReader mockedDelegate = mock(TweedDataReader.class);
when(mockedDelegate.readToken()).thenReturn(
TweedDataTokens.getMapStart(),
TweedDataTokens.asMapEntryKey(new StringToken("key")),
TweedDataTokens.asMapEntryValue(new StringToken("value")),
TweedDataTokens.asMapEntryKey(new StringToken("list")),
TweedDataTokens.asMapEntryValue(TweedDataTokens.getListStart()),
TweedDataTokens.asListValue(new StringToken("first")),
TweedDataTokens.asListValue(TweedDataTokens.getMapStart()),
TweedDataTokens.asListValue(TweedDataTokens.getMapEnd()),
TweedDataTokens.getListEnd(),
TweedDataTokens.getMapEnd()
);
var reader = new PathTrackingDataReader(mockedDelegate, pathTracking);
assertThat(reader.readToken()).isEqualTo(TweedDataTokens.getMapStart());
assertThat(pathTracking.currentPath()).isEqualTo(".$");
assertThat(reader.readToken().readAsString()).isEqualTo("key");
assertThat(pathTracking.currentPath()).isEqualTo(".key");
assertThat(reader.readToken().readAsString()).isEqualTo("value");
assertThat(reader.readToken().readAsString()).isEqualTo("list");
assertThat(pathTracking.currentPath()).isEqualTo(".list");
assertThat(reader.readToken().isListStart()).isTrue();
assertThat(pathTracking.currentPath()).isEqualTo(".list");
assertThat(reader.readToken().readAsString()).isEqualTo("first");
assertThat(reader.readToken().isMapStart()).isTrue();
assertThat(pathTracking.currentPath()).startsWith(".list.1");
assertThat(reader.readToken().isMapEnd()).isTrue();
assertThat(reader.readToken().isListEnd()).isTrue();
assertThat(pathTracking.currentPath()).isEqualTo(".list");
assertThat(reader.readToken()).isEqualTo(TweedDataTokens.getMapEnd());
assertThat(pathTracking.currentPath()).isEqualTo("");
}
@RequiredArgsConstructor
@EqualsAndHashCode
static class StringToken implements TweedDataToken {
private final String value;
@Override
public boolean canReadAsString() {
return true;
}
@Override
public String readAsString() {
return value;
}
}
}