From 596f580fa0615f9d5c0ec46aec45b575499327ba Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Thu, 26 Feb 2026 18:30:28 +0100 Subject: [PATCH] Add Notice JSON serialize/deserialize API with round-trip test --- buildSrc/src/main/kotlin/Versions.kt | 1 + multification-core/build.gradle.kts | 3 +- .../multification/Multification.java | 9 ++ .../multification/notice/Notice.java | 20 +++ .../multification/notice/NoticeJsonCodec.java | 138 ++++++++++++++++++ .../multification/MultificationTest.java | 23 +++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 multification-core/src/com/eternalcode/multification/notice/NoticeJsonCodec.java diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index d946a06..9375cb6 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -13,6 +13,7 @@ object Versions { const val VELOCITY_API = "3.4.0" const val JETBRAINS_ANNOTATIONS = "26.1.0" + const val GSON = "2.13.2" } diff --git a/multification-core/build.gradle.kts b/multification-core/build.gradle.kts index a4a20f7..85b7e5d 100644 --- a/multification-core/build.gradle.kts +++ b/multification-core/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { compileOnlyApi("net.kyori:adventure-api:${Versions.ADVENTURE_API}") api("org.jetbrains:annotations:${Versions.JETBRAINS_ANNOTATIONS}") + implementation("com.google.code.gson:gson:${Versions.GSON}") testImplementation("net.kyori:adventure-api:${Versions.ADVENTURE_API}") -} \ No newline at end of file +} diff --git a/multification-core/src/com/eternalcode/multification/Multification.java b/multification-core/src/com/eternalcode/multification/Multification.java index 64211a0..f765c99 100644 --- a/multification-core/src/com/eternalcode/multification/Multification.java +++ b/multification-core/src/com/eternalcode/multification/Multification.java @@ -9,6 +9,7 @@ import com.eternalcode.multification.viewer.ViewerProvider; import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.executor.AsyncExecutor; +import com.eternalcode.multification.notice.Notice; import com.eternalcode.multification.notice.NoticeBroadcast; import com.eternalcode.multification.notice.NoticeBroadcastImpl; import com.eternalcode.multification.notice.provider.NoticeProvider; @@ -126,6 +127,14 @@ public void all(NoticeProvider extractor, Formatter... formatters) .send(); } + public String serialize(Notice notice) { + return notice.serialize(this.noticeRegistry); + } + + public Notice deserialize(String raw) { + return Notice.deserialize(raw, this.noticeRegistry); + } + @ApiStatus.Internal public NoticeResolverRegistry getNoticeRegistry() { return noticeRegistry; diff --git a/multification-core/src/com/eternalcode/multification/notice/Notice.java b/multification-core/src/com/eternalcode/multification/notice/Notice.java index bbaa65c..4d07010 100644 --- a/multification-core/src/com/eternalcode/multification/notice/Notice.java +++ b/multification-core/src/com/eternalcode/multification/notice/Notice.java @@ -1,6 +1,8 @@ package com.eternalcode.multification.notice; import com.eternalcode.multification.notice.resolver.NoticeContent; +import com.eternalcode.multification.notice.resolver.NoticeResolverDefaults; +import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; import com.eternalcode.multification.notice.resolver.actionbar.ActionbarContent; import com.eternalcode.multification.notice.resolver.bossbar.BossBarContent; import com.eternalcode.multification.notice.resolver.chat.ChatContent; @@ -25,6 +27,8 @@ public class Notice { + private static final NoticeResolverRegistry DEFAULT_NOTICE_REGISTRY = NoticeResolverDefaults.createRegistry(); + private final Map, NoticePart> parts = new LinkedHashMap<>(); protected Notice(Map, NoticePart> parts) { @@ -149,6 +153,22 @@ public static Notice empty() { return new Notice(Collections.emptyMap()); } + public String serialize() { + return serialize(DEFAULT_NOTICE_REGISTRY); + } + + public String serialize(NoticeResolverRegistry noticeRegistry) { + return NoticeJsonCodec.serialize(this, noticeRegistry); + } + + public static Notice deserialize(String raw) { + return deserialize(raw, DEFAULT_NOTICE_REGISTRY); + } + + public static Notice deserialize(String raw, NoticeResolverRegistry noticeRegistry) { + return NoticeJsonCodec.deserialize(raw, noticeRegistry); + } + public static Builder builder() { return new Builder(); } diff --git a/multification-core/src/com/eternalcode/multification/notice/NoticeJsonCodec.java b/multification-core/src/com/eternalcode/multification/notice/NoticeJsonCodec.java new file mode 100644 index 0000000..e45e65c --- /dev/null +++ b/multification-core/src/com/eternalcode/multification/notice/NoticeJsonCodec.java @@ -0,0 +1,138 @@ +package com.eternalcode.multification.notice; + +import com.eternalcode.multification.notice.resolver.NoticeDeserializeResult; +import com.eternalcode.multification.notice.resolver.NoticeContent; +import com.eternalcode.multification.notice.resolver.NoticeResolverRegistry; +import com.eternalcode.multification.notice.resolver.NoticeSerdesResult; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +final class NoticeJsonCodec { + + private NoticeJsonCodec() { + } + + static String serialize(Notice notice, NoticeResolverRegistry noticeRegistry) { + JsonObject root = new JsonObject(); + + for (NoticePart part : notice.parts()) { + NoticeSerdesResult serializedPart = noticeRegistry.serialize(part); + root.add(part.noticeKey().key(), toJsonElement(serializedPart)); + } + + return root.toString(); + } + + static Notice deserialize(String raw, NoticeResolverRegistry noticeRegistry) { + JsonElement parsed = JsonParser.parseString(raw); + if (!parsed.isJsonObject()) { + throw new IllegalArgumentException("Notice JSON must be an object"); + } + + Notice.Builder builder = Notice.builder(); + JsonObject root = parsed.getAsJsonObject(); + + for (Map.Entry entry : root.entrySet()) { + String key = entry.getKey(); + NoticeSerdesResult result = toSerdesResult(key, entry.getValue()); + Optional> deserialized = noticeRegistry.deserialize(key, result); + + if (deserialized.isEmpty()) { + throw new IllegalArgumentException("Unsupported notice key: " + key); + } + + withPart(builder, deserialized.get()); + } + + return builder.build(); + } + + private static JsonElement toJsonElement(NoticeSerdesResult result) { + if (result instanceof NoticeSerdesResult.Single single) { + return new JsonPrimitive(single.element()); + } + + if (result instanceof NoticeSerdesResult.Multiple multiple) { + JsonArray array = new JsonArray(); + for (String element : multiple.elements()) { + array.add(element); + } + return array; + } + + if (result instanceof NoticeSerdesResult.Section section) { + JsonObject object = new JsonObject(); + for (Map.Entry sectionEntry : section.elements().entrySet()) { + object.addProperty(sectionEntry.getKey(), sectionEntry.getValue()); + } + return object; + } + + if (result instanceof NoticeSerdesResult.Empty) { + return JsonNull.INSTANCE; + } + + throw new IllegalArgumentException("Unsupported result type: " + result.getClass().getName()); + } + + private static NoticeSerdesResult toSerdesResult(String key, JsonElement element) { + if (element.isJsonNull()) { + return new NoticeSerdesResult.Empty(); + } + + if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + return new NoticeSerdesResult.Single(element.getAsString()); + } + + if (element.isJsonArray()) { + return new NoticeSerdesResult.Multiple(toStringList(key, element.getAsJsonArray())); + } + + if (element.isJsonObject()) { + return new NoticeSerdesResult.Section(toStringMap(key, element.getAsJsonObject())); + } + + throw new JsonParseException("Unsupported JSON value for key '" + key + "'"); + } + + private static List toStringList(String key, JsonArray jsonArray) { + List values = new ArrayList<>(); + for (JsonElement jsonElement : jsonArray) { + if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) { + throw new JsonParseException("All array elements for key '" + key + "' must be strings"); + } + values.add(jsonElement.getAsString()); + } + return values; + } + + private static Map toStringMap(String key, JsonObject jsonObject) { + Map values = new LinkedHashMap<>(); + for (Map.Entry jsonEntry : jsonObject.entrySet()) { + JsonElement jsonElement = jsonEntry.getValue(); + if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) { + throw new JsonParseException("All object values for key '" + key + "' must be strings"); + } + values.put(jsonEntry.getKey(), jsonElement.getAsString()); + } + return values; + } + + private static void withPart( + Notice.Builder builder, + NoticeDeserializeResult noticeResult + ) { + builder.withPart(noticeResult.noticeKey(), noticeResult.content()); + } + +} diff --git a/multification-core/test/com/eternalcode/multification/MultificationTest.java b/multification-core/test/com/eternalcode/multification/MultificationTest.java index ee5bbae..692f0b7 100644 --- a/multification-core/test/com/eternalcode/multification/MultificationTest.java +++ b/multification-core/test/com/eternalcode/multification/MultificationTest.java @@ -5,6 +5,7 @@ import com.eternalcode.multification.viewer.ViewerProvider; import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.notice.Notice; +import java.time.Duration; import com.eternalcode.multification.shared.Replacer; import java.util.ArrayList; import java.util.Collection; @@ -13,6 +14,7 @@ import java.util.Map; import java.util.UUID; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -141,4 +143,25 @@ void test() { .containsExactly(DEFAULT_MESSAGE, OTHER_MESSAGE, "-INNER- -GLOBAL-"); } + @Test + @DisplayName("Should serialize and deserialize notice as json") + void shouldSerializeAndDeserializeNoticeAsJson() { + MyMultification multification = new MyMultification(); + Notice notice = Notice.builder() + .chat("line-1", "line-2") + .actionBar("action") + .title("title", "subtitle") + .times(Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(3)) + .build(); + + String raw = multification.serialize(notice); + Notice restored = multification.deserialize(raw); + + assertThat(raw) + .contains("\"chat\"") + .contains("\"actionbar\"") + .contains("\"times\""); + assertEquals(notice.parts(), restored.parts()); + } + }