[comment-loader-extension] Introduce comment loader extension

This commit is contained in:
2025-08-05 18:15:53 +02:00
parent 32831c7c22
commit 6f2e715b2a
13 changed files with 467 additions and 4 deletions

View File

@@ -0,0 +1,16 @@
package de.siphalor.tweed5.commentloaderextension.api;
import de.siphalor.tweed5.commentloaderextension.impl.CommentLoaderExtensionImpl;
import de.siphalor.tweed5.core.api.extension.TweedExtension;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
public interface CommentLoaderExtension extends TweedExtension {
Class<? extends CommentLoaderExtension> DEFAULT = CommentLoaderExtensionImpl.class;
String EXTENSION_ID = "comment-loader";
void loadComments(TweedDataReader reader, CommentPathProcessor pathProcessor);
default String getId() {
return EXTENSION_ID;
}
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.commentloaderextension.api;
public interface CommentPathProcessor {
MatchStatus matches(String path);
String process(String path);
enum MatchStatus {
YES,
NO,
MAYBE_DEEPER,
}
}

View File

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

View File

@@ -0,0 +1,212 @@
package de.siphalor.tweed5.commentloaderextension.impl;
import de.siphalor.tweed5.commentloaderextension.api.CommentLoaderExtension;
import de.siphalor.tweed5.commentloaderextension.api.CommentPathProcessor;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.container.ConfigContainerSetupPhase;
import de.siphalor.tweed5.core.api.extension.TweedExtensionSetupContext;
import de.siphalor.tweed5.core.api.middleware.Middleware;
import de.siphalor.tweed5.dataapi.api.IntuitiveVisitingTweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataReadException;
import de.siphalor.tweed5.dataapi.api.TweedDataReader;
import de.siphalor.tweed5.dataapi.api.TweedDataVisitor;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentModifyingExtension;
import de.siphalor.tweed5.defaultextensions.comment.api.CommentProducer;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTracking;
import de.siphalor.tweed5.defaultextensions.pather.api.PathTrackingConfigEntryVisitor;
import de.siphalor.tweed5.patchwork.api.PatchworkPartAccess;
import lombok.Getter;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
import org.jspecify.annotations.Nullable;
import java.util.*;
@CommonsLog
public class CommentLoaderExtensionImpl implements CommentLoaderExtension, CommentModifyingExtension {
private final ConfigContainer<?> configContainer;
private final PatchworkPartAccess<String> loadedCommentAccess;
private @Nullable CommentExtension commentExtension;
public CommentLoaderExtensionImpl(ConfigContainer<?> configContainer, TweedExtensionSetupContext setupContext) {
this.configContainer = configContainer;
setupContext.registerExtension(CommentExtension.class);
loadedCommentAccess = setupContext.registerEntryExtensionData(String.class);
}
@Override
public void extensionsFinalized() {
commentExtension = configContainer.extension(CommentExtension.class)
.orElseThrow(() -> new IllegalStateException("CommentExtension not found"));
}
@Override
public Middleware<CommentProducer> commentMiddleware() {
return new Middleware<CommentProducer>() {
@Override
public String id() {
return EXTENSION_ID;
}
@Override
public Set<String> mustComeBefore() {
return Collections.singleton(Middleware.DEFAULT_START);
}
@Override
public Set<String> mustComeAfter() {
return Collections.emptySet();
}
@Override
public CommentProducer process(CommentProducer inner) {
return entry -> {
String loadedComment = entry.extensionsData().get(loadedCommentAccess);
String innerComment = inner.createComment(entry);
if (loadedComment != null) {
if (innerComment.isEmpty()) {
return loadedComment;
} else {
return innerComment + loadedComment;
}
}
return innerComment;
};
}
};
}
@Override
public void loadComments(TweedDataReader reader, CommentPathProcessor pathProcessor) {
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.EXTENSIONS_SETUP) <= 0) {
throw new IllegalStateException("Comments cannot be loaded before the extensions are finalized");
}
CollectingCommentsVisitor collectingCommentsVisitor = new CollectingCommentsVisitor(pathProcessor);
try {
new IntuitiveVisitingTweedDataReader(collectingCommentsVisitor).readMap(reader);
} catch (TweedDataReadException e) {
log.error("Failed to load comments", e);
}
Map<String, String> commentsByKey = collectingCommentsVisitor.commentsByKey();
PathTracking pathTracking = new PathTracking();
configContainer.rootEntry().visitInOrder(new PathTrackingConfigEntryVisitor(
entry -> {
String key = pathTracking.currentPath();
if (!key.isEmpty() && key.charAt(0) == '.') {
key = key.substring(1);
}
entry.extensionsData().set(loadedCommentAccess, commentsByKey.get(key));
},
pathTracking
));
if (configContainer.setupPhase().compareTo(ConfigContainerSetupPhase.INITIALIZED) >= 0) {
assert commentExtension != null;
commentExtension.recomputeFullComments();
}
}
private static class CollectingCommentsVisitor implements TweedDataVisitor {
private final CommentPathProcessor pathProcessor;
@Getter
private final Map<String, String> commentsByKey = new HashMap<>();
private final Deque<State> stateStack = new ArrayDeque<>();
private State currentState = new State(CommentPathProcessor.MatchStatus.MAYBE_DEEPER, "");
public CollectingCommentsVisitor(CommentPathProcessor pathProcessor) {
this.pathProcessor = pathProcessor;
stateStack.push(currentState);
}
@Override
public void visitNull() {}
@Override
public void visitBoolean(boolean value) {}
@Override
public void visitByte(byte value) {}
@Override
public void visitShort(short value) {}
@Override
public void visitInt(int value) {}
@Override
public void visitLong(long value) {}
@Override
public void visitFloat(float value) {}
@Override
public void visitDouble(double value) {}
@Override
public void visitString(String value) {
if (currentState.matchStatus() == CommentPathProcessor.MatchStatus.YES) {
commentsByKey.put(pathProcessor.process(currentState.key()), value);
}
}
@Override
public void visitListStart() {
stateStack.push(State.IGNORED);
}
@Override
public void visitListEnd() {
stateStack.pop();
}
@Override
public void visitMapStart() {
stateStack.push(currentState);
currentState = State.IGNORED;
}
@Override
public void visitMapEntryKey(String key) {
State state = stateStack.peek();
assert state != null;
if (state.matchStatus() == CommentPathProcessor.MatchStatus.NO) {
return;
}
String fullPath;
if (state.key().isEmpty()) {
fullPath = key;
} else {
fullPath = state.key() + "." + key;
}
CommentPathProcessor.MatchStatus matchStatus = pathProcessor.matches(fullPath);
if (matchStatus == CommentPathProcessor.MatchStatus.NO) {
currentState = State.IGNORED;
} else {
currentState = new State(matchStatus, fullPath);
}
}
@Override
public void visitMapEnd() {
currentState = stateStack.pop();
}
@Override
public void visitDecoration(TweedDataDecoration decoration) {}
@Value
private static class State {
private static final State IGNORED = new State(CommentPathProcessor.MatchStatus.NO, "");
CommentPathProcessor.MatchStatus matchStatus;
String key;
}
}
}

View File

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

View File

@@ -0,0 +1,112 @@
package de.siphalor.tweed5.commentloaderextension.impl;
import com.google.gson.GsonBuilder;
import de.siphalor.tweed5.commentloaderextension.api.CommentLoaderExtension;
import de.siphalor.tweed5.commentloaderextension.api.CommentPathProcessor;
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.defaultextensions.comment.api.CommentExtension;
import de.siphaolor.tweed5.data.gson.GsonReader;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
class CommentLoaderExtensionImplTest {
@SuppressWarnings({"unchecked", "rawtypes"})
@Test
@SneakyThrows
void test() {
var configContainer = new DefaultConfigContainer<Map<String, Object>>();
configContainer.registerExtension(CommentLoaderExtension.class);
configContainer.finishExtensionSetup();
var nestedIntEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class);
var nestedEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>) (Class) Map.class,
HashMap::newHashMap,
sequencedMap(List.of(
entry("int", nestedIntEntry)
))
);
var intEntry = new SimpleConfigEntryImpl<>(configContainer, Integer.class);
var rootEntry = new StaticMapCompoundConfigEntryImpl<>(
configContainer,
(Class<Map<String, Object>>)(Class) Map.class,
HashMap::newHashMap,
sequencedMap(List.of(
entry("nested", nestedEntry),
entry("int", intEntry)
))
);
configContainer.attachTree(rootEntry);
configContainer.initialize();
CommentLoaderExtension extension = configContainer.extension(CommentLoaderExtension.class).orElseThrow();
// language=json
var text = """
{
"test": {
"description": "Root comment"
},
"test.int.description": "What an int!",
"test.nested": {
"description": "Comment for nested entry",
"int.description": "A cool nested entry"
}
}
""";
try (var reader = new GsonReader(new GsonBuilder().create().newJsonReader(new StringReader(text)))) {
extension.loadComments(
reader,
new CommentPathProcessor() {
private static final String PREFIX_RAW = "test";
private static final String PREFIX = PREFIX_RAW + ".";
private static final String SUFFIX = ".description";
@Override
public MatchStatus matches(String path) {
if (path.equals(PREFIX_RAW)) {
return MatchStatus.MAYBE_DEEPER;
} else if (path.startsWith(PREFIX)) {
if (path.endsWith(SUFFIX)) {
return MatchStatus.YES;
} else {
return MatchStatus.MAYBE_DEEPER;
}
} else {
return MatchStatus.NO;
}
}
@Override
public String process(String path) {
return path.substring(
PREFIX.length(),
Math.max(PREFIX.length(), path.length() - SUFFIX.length())
);
}
}
);
}
CommentExtension commentExtension = configContainer.extension(CommentExtension.class).orElseThrow();
assertThat(commentExtension.getFullComment(rootEntry)).isEqualTo("Root comment");
assertThat(commentExtension.getFullComment(nestedEntry)).isEqualTo("Comment for nested entry");
assertThat(commentExtension.getFullComment(nestedIntEntry)).isEqualTo("A cool nested entry");
assertThat(commentExtension.getFullComment(intEntry)).isEqualTo("What an int!");
}
}