[comment-loader-extension] Introduce comment loader extension
This commit is contained in:
12
tweed5-comment-loader-extension/build.gradle.kts
Normal file
12
tweed5-comment-loader-extension/build.gradle.kts
Normal file
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id("de.siphalor.tweed5.base-module")
|
||||
id("de.siphalor.tweed5.minecraft.mod.dummy")
|
||||
id("de.siphalor.tweed5.shadow.explicit")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":tweed5-core"))
|
||||
implementation(project(":tweed5-default-extensions"))
|
||||
|
||||
testImplementation(project(":tweed5-serde-gson"))
|
||||
}
|
||||
2
tweed5-comment-loader-extension/gradle.properties
Normal file
2
tweed5-comment-loader-extension/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
minecraft.mod.name = Tweed 5 Comment Loader Extension
|
||||
minecraft.mod.description = Tweed 5 module that allows dynamically loading comments from data files, e.g., for i18n
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.commentloaderextension.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.commentloaderextension.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user