From 1fc418970f46d6223c19d502fd920a4f9df20bfa Mon Sep 17 00:00:00 2001 From: Siphalor Date: Sun, 3 Aug 2025 20:21:33 +0200 Subject: [PATCH] [serde-gson] Support for Gson readers and writers --- .../de.siphalor.tweed5.base-module.gradle.kts | 2 +- .../de.siphalor.tweed5.test-utils.gradle.kts | 28 ++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 4 + test-utils/build.gradle.kts | 8 - test-utils/generic/build.gradle.kts | 3 + .../testutils/generic}/MapTestUtils.java | 2 +- test-utils/serde-json/build.gradle.kts | 3 + .../testutils/serde/json/JsonReaderTest.java | 73 +++++ ...butesReadWriteFilterExtensionImplTest.java | 3 +- .../impl/CommentExtensionImplTest.java | 2 +- .../patch/impl/PatchExtensionImplTest.java | 2 +- .../impl/ValidationExtensionImplTest.java | 2 +- .../impl/ReadWriteExtensionImplTest.java | 2 +- tweed5-serde-gson/build.gradle.kts | 13 + tweed5-serde-gson/gradle.properties | 2 + .../tweed5/data/gson/GsonReader.java | 275 ++++++++++++++++++ .../tweed5/data/gson/GsonWriter.java | 214 ++++++++++++++ .../tweed5/data/gson/package-info.java | 4 + .../tweed5/data/gson/GsonReaderTest.java | 16 + .../tweed5/data/gson/GsonWriterTest.java | 54 ++++ tweed5-serde-jackson/build.gradle.kts | 2 + .../data/jackson/JacksonReaderTest.java | 76 +---- 23 files changed, 710 insertions(+), 82 deletions(-) create mode 100644 buildSrc/src/main/kotlin/de.siphalor.tweed5.test-utils.gradle.kts delete mode 100644 test-utils/build.gradle.kts create mode 100644 test-utils/generic/build.gradle.kts rename test-utils/{src/main/java/de/siphalor/tweed5/testutils => generic/src/main/java/de/siphalor/tweed5/testutils/generic}/MapTestUtils.java (87%) create mode 100644 test-utils/serde-json/build.gradle.kts create mode 100644 test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonReaderTest.java create mode 100644 tweed5-serde-gson/build.gradle.kts create mode 100644 tweed5-serde-gson/gradle.properties create mode 100644 tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonReader.java create mode 100644 tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonWriter.java create mode 100644 tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/package-info.java create mode 100644 tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonReaderTest.java create mode 100644 tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonWriterTest.java diff --git a/buildSrc/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts b/buildSrc/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts index 126711e..d254976 100644 --- a/buildSrc/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts +++ b/buildSrc/src/main/kotlin/de.siphalor.tweed5.base-module.gradle.kts @@ -51,7 +51,7 @@ dependencies { testImplementation(libs.mockito) testAgent(libs.mockito) testImplementation(libs.assertj) - testImplementation(project(":test-utils")) + testImplementation(project(":generic-test-utils")) } tasks.compileTestJava { diff --git a/buildSrc/src/main/kotlin/de.siphalor.tweed5.test-utils.gradle.kts b/buildSrc/src/main/kotlin/de.siphalor.tweed5.test-utils.gradle.kts new file mode 100644 index 0000000..e010c47 --- /dev/null +++ b/buildSrc/src/main/kotlin/de.siphalor.tweed5.test-utils.gradle.kts @@ -0,0 +1,28 @@ +import gradle.kotlin.dsl.accessors._182d53d78a136df48d95cf7411f9259f.lombok +import org.gradle.kotlin.dsl.assign + +plugins { + java + alias(libs.plugins.lombok) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":tweed5-serde-api")) + implementation(platform(libs.junit.platform)) + implementation(libs.junit.core) + implementation(libs.mockito) + implementation(libs.assertj) +} + +lombok { + version = libs.versions.lombok.get() +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.test.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.test.get()) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab1bd54..537c3ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ assertj = "3.26.3" asm = "9.7" autoservice = "1.1.1" acl = "1.3.5" +gson = "2.13.1" jackson = "2.19.0" java-main = "8" java-test = "21" @@ -26,6 +27,7 @@ asm-commons = { group = "org.ow2.asm", name = "asm-commons", version.ref = "asm" asm-core = { group = "org.ow2.asm", name = "asm", version.ref = "asm" } autoservice-annotations = { group = "com.google.auto.service", name = "auto-service-annotations", version.ref = "autoservice" } autoservice-processor = { group = "com.google.auto.service", name = "auto-service", version.ref = "autoservice" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" } jspecify-annotations = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 32f166d..21b420b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ include("tweed5-naming-format") include("tweed5-patchwork") include("tweed5-serde-api") include("tweed5-serde-extension") +include("tweed5-serde-gson") include("tweed5-serde-hjson") include("tweed5-serde-jackson") include("tweed5-type-utils") @@ -21,6 +22,9 @@ include("tweed5-weaver-pojo-validation-extension") includeAs("minecraft:tweed5-bundle", "minecraft/tweed5-bundle") +includeAs("generic-test-utils", "test-utils/generic") +includeAs("serde-json-test-utils", "test-utils/serde-json") + fun includeAs(name: String, path: String) { include(name) project(":$name").projectDir = file(path) diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts deleted file mode 100644 index f3ba020..0000000 --- a/test-utils/build.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - java -} - -java { - sourceCompatibility = JavaVersion.toVersion(libs.versions.java.test.get()) - targetCompatibility = JavaVersion.toVersion(libs.versions.java.test.get()) -} diff --git a/test-utils/generic/build.gradle.kts b/test-utils/generic/build.gradle.kts new file mode 100644 index 0000000..25c9399 --- /dev/null +++ b/test-utils/generic/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("de.siphalor.tweed5.test-utils") +} diff --git a/test-utils/src/main/java/de/siphalor/tweed5/testutils/MapTestUtils.java b/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/MapTestUtils.java similarity index 87% rename from test-utils/src/main/java/de/siphalor/tweed5/testutils/MapTestUtils.java rename to test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/MapTestUtils.java index 281b317..5f095a8 100644 --- a/test-utils/src/main/java/de/siphalor/tweed5/testutils/MapTestUtils.java +++ b/test-utils/generic/src/main/java/de/siphalor/tweed5/testutils/generic/MapTestUtils.java @@ -1,4 +1,4 @@ -package de.siphalor.tweed5.testutils; +package de.siphalor.tweed5.testutils.generic; import java.util.*; diff --git a/test-utils/serde-json/build.gradle.kts b/test-utils/serde-json/build.gradle.kts new file mode 100644 index 0000000..25c9399 --- /dev/null +++ b/test-utils/serde-json/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("de.siphalor.tweed5.test-utils") +} diff --git a/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonReaderTest.java b/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonReaderTest.java new file mode 100644 index 0000000..925f2af --- /dev/null +++ b/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonReaderTest.java @@ -0,0 +1,73 @@ +package de.siphalor.tweed5.testutils.serde.json; + +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public interface JsonReaderTest { + TweedDataReader createJsonReader(String text); + + @Test + @SneakyThrows + default void complexJsonReadTest() { + var reader = createJsonReader(""" + { + "first": [ + [ 1 ] + ], + "second": { + "test": "Hello World!" + } + } + """); + + var token = reader.peekToken(); + assertThat(token.isMapStart()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapStart()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapEntryKey()).isTrue(); + assertThat(token.canReadAsString()).isTrue(); + assertThat(token.readAsString()).isEqualTo("first"); + token = reader.readToken(); + assertThat(token.isMapEntryValue()).isTrue(); + assertThat(token.isListStart()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapEntryValue()).isFalse(); + assertThat(token.isListValue()).isTrue(); + assertThat(token.isListStart()).isTrue(); + token = reader.readToken(); + assertThat(token.isListValue()).isTrue(); + assertThat(token.canReadAsInt()).isTrue(); + assertThat(token.readAsInt()).isEqualTo(1); + token = reader.readToken(); + assertThat(token.isListValue()).isTrue(); + assertThat(token.isListEnd()).isTrue(); + token = reader.readToken(); + assertThat(token.isListValue()).isFalse(); + assertThat(token.isMapEntryValue()).isTrue(); + assertThat(token.isListEnd()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapEntryKey()).isTrue(); + assertThat(token.canReadAsString()).isTrue(); + assertThat(token.readAsString()).isEqualTo("second"); + token = reader.readToken(); + assertThat(token.isMapEntryValue()).isTrue(); + assertThat(token.isMapStart()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapEntryValue()).isFalse(); + assertThat(token.isMapEntryKey()).isTrue(); + assertThat(token.canReadAsString()).isTrue(); + assertThat(token.readAsString()).isEqualTo("test"); + token = reader.readToken(); + assertThat(token.isMapEntryValue()).isTrue(); + assertThat(token.canReadAsString()).isTrue(); + assertThat(token.readAsString()).isEqualTo("Hello World!"); + token = reader.readToken(); + assertThat(token.isMapEnd()).isTrue(); + token = reader.readToken(); + assertThat(token.isMapEnd()).isTrue(); + } +} diff --git a/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java b/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java index a0ea8e9..ee4ebb2 100644 --- a/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java +++ b/tweed5-attributes-extension/src/test/java/de/siphalor/tweed5/attributesextension/impl/serde/filter/AttributesReadWriteFilterExtensionImplTest.java @@ -23,14 +23,13 @@ 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 de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.map; diff --git a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/comment/impl/CommentExtensionImplTest.java b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/comment/impl/CommentExtensionImplTest.java index 8705e76..1a470a7 100644 --- a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/comment/impl/CommentExtensionImplTest.java +++ b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/comment/impl/CommentExtensionImplTest.java @@ -27,7 +27,7 @@ import java.util.Map; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter; import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*; import static de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension.baseComment; -import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; +import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap; import static java.util.Map.entry; import static org.junit.jupiter.api.Assertions.*; diff --git a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java index b411954..f75f4cb 100644 --- a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java +++ b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/patch/impl/PatchExtensionImplTest.java @@ -28,7 +28,7 @@ import java.util.stream.Stream; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.read; import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*; -import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; +import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.argumentSet; diff --git a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java index b9fa5be..6a373ec 100644 --- a/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java +++ b/tweed5-default-extensions/src/test/java/de/siphalor/tweed5/defaultextensions/validation/impl/ValidationExtensionImplTest.java @@ -33,7 +33,7 @@ import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.read; import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*; import static de.siphalor.tweed5.defaultextensions.comment.api.CommentExtension.baseComment; import static de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension.validators; -import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; +import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; diff --git a/tweed5-serde-extension/src/test/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImplTest.java b/tweed5-serde-extension/src/test/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImplTest.java index 7ac6519..72b2aec 100644 --- a/tweed5-serde-extension/src/test/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImplTest.java +++ b/tweed5-serde-extension/src/test/java/de/siphalor/tweed5/data/extension/impl/ReadWriteExtensionImplTest.java @@ -24,7 +24,7 @@ import java.util.function.Function; import static de.siphalor.tweed5.data.extension.api.ReadWriteExtension.entryReaderWriter; import static de.siphalor.tweed5.data.extension.api.readwrite.TweedEntryReaderWriters.*; -import static de.siphalor.tweed5.testutils.MapTestUtils.sequencedMap; +import static de.siphalor.tweed5.testutils.generic.MapTestUtils.sequencedMap; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; diff --git a/tweed5-serde-gson/build.gradle.kts b/tweed5-serde-gson/build.gradle.kts new file mode 100644 index 0000000..6c12db2 --- /dev/null +++ b/tweed5-serde-gson/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("de.siphalor.tweed5.base-module") + id("de.siphalor.tweed5.minecraft.mod.dummy") + id("de.siphalor.tweed5.shadow.explicit") +} + +dependencies { + implementation(project(":tweed5-serde-api")) + implementation(libs.gson) + + testImplementation(project(":serde-json-test-utils")) +} + diff --git a/tweed5-serde-gson/gradle.properties b/tweed5-serde-gson/gradle.properties new file mode 100644 index 0000000..0413e12 --- /dev/null +++ b/tweed5-serde-gson/gradle.properties @@ -0,0 +1,2 @@ +minecraft.mod.name = Tweed 5 Gson +minecraft.mod.description = Tweed 5 module that adds support for reading and writing JSON using the Gson library. diff --git a/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonReader.java b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonReader.java new file mode 100644 index 0000000..4ab7093 --- /dev/null +++ b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonReader.java @@ -0,0 +1,275 @@ +package de.siphaolor.tweed5.data.gson; + +import com.google.gson.stream.JsonReader; +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 org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +public class GsonReader implements TweedDataReader { + private final JsonReader reader; + + private final Deque contextStack = new ArrayDeque<>(); + + private @Nullable TweedDataToken peekedToken; + + public GsonReader(JsonReader reader) { + this.reader = reader; + contextStack.push(Context.VALUE); + } + + @Override + public TweedDataToken peekToken() throws TweedDataReadException { + if (peekedToken == null) { + peekedToken = nextToken(); + } + return peekedToken; + } + + @Override + public TweedDataToken readToken() throws TweedDataReadException { + if (peekedToken != null) { + TweedDataToken token = peekedToken; + peekedToken = null; + return token; + } + return nextToken(); + } + + private TweedDataToken nextToken() throws TweedDataReadException { + try { + switch (reader.peek()) { + case BEGIN_ARRAY: { + reader.beginArray(); + TweedDataToken token = wrapToken(TweedDataTokens.getListStart()); + contextStack.push(Context.LIST); + return token; + } + case END_ARRAY: { + reader.endArray(); + popContext(Context.LIST); + TweedDataToken token = wrapToken(TweedDataTokens.getListEnd()); + afterValueRead(); + return token; + } + case BEGIN_OBJECT: { + reader.beginObject(); + TweedDataToken token = wrapToken(TweedDataTokens.getMapStart()); + contextStack.push(Context.MAP); + return token; + } + case END_OBJECT: { + reader.endObject(); + popContext(Context.MAP); + TweedDataToken token = wrapToken(TweedDataTokens.getMapEnd()); + afterValueRead(); + return token; + } + case NAME: + contextStack.push(Context.MAP_ENTRY_VALUE); + return TweedDataTokens.asMapEntryKey(createStringToken(reader.nextName())); + case NULL: { + reader.nextNull(); + TweedDataToken token = wrapToken(TweedDataTokens.getNull()); + afterValueRead(); + return token; + } + case BOOLEAN: { + boolean value = reader.nextBoolean(); + TweedDataToken token = wrapToken(new TweedDataToken() { + @Override + public boolean canReadAsBoolean() { + return true; + } + + @Override + public boolean readAsBoolean() throws TweedDataReadException { + return value; + } + }); + afterValueRead(); + return token; + } + case NUMBER: { + Long longValue = tryReadLong(); + TweedDataToken token; + if (longValue != null) { + token = wrapToken(new TweedDataToken() { + @Override + public boolean canReadAsByte() { + return longValue >= Byte.MIN_VALUE && longValue <= Byte.MAX_VALUE; + } + + @Override + public byte readAsByte() { + return longValue.byteValue(); + } + + @Override + public boolean canReadAsShort() { + return longValue >= Short.MIN_VALUE && longValue <= Short.MAX_VALUE; + } + + @Override + public short readAsShort() { + return longValue.shortValue(); + } + + @Override + public boolean canReadAsInt() { + return longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE; + } + + @Override + public int readAsInt() { + return longValue.intValue(); + } + + @Override + public boolean canReadAsLong() { + return true; + } + + @Override + public long readAsLong() { + return longValue; + } + + @Override + public boolean canReadAsFloat() { + return true; + } + + @Override + public float readAsFloat() { + return longValue.floatValue(); + } + + @Override + public boolean canReadAsDouble() { + return true; + } + + @Override + public double readAsDouble() { + return longValue.doubleValue(); + } + }); + } else { + double doubleValue = reader.nextDouble(); + token = wrapToken(new TweedDataToken() { + @Override + public boolean canReadAsFloat() { + return true; + } + + @Override + public float readAsFloat() { + return (float) doubleValue; + } + + @Override + public boolean canReadAsDouble() { + return true; + } + + @Override + public double readAsDouble() { + return doubleValue; + } + }); + } + afterValueRead(); + return token; + } + case STRING: { + TweedDataToken token = wrapToken(createStringToken(reader.nextString())); + afterValueRead(); + return token; + } + default: + throw TweedDataReadException.builder() + .message("Encountered unexpected " + peekedToken + " token at " + reader.getPath()) + .build(); + } + } catch (IOException e) { + throw TweedDataReadException.builder() + .message("Error reading data using gson at " + reader.getPath()) + .cause(e) + .build(); + } + } + + private @Nullable Long tryReadLong() throws IOException { + try { + return reader.nextLong(); + } catch (NumberFormatException e) { + return null; + } + } + + private TweedDataToken wrapToken(TweedDataToken token) throws TweedDataReadException { + switch (peekContext()) { + case MAP_ENTRY_VALUE: + return TweedDataTokens.asMapEntryValue(token); + case LIST: + return TweedDataTokens.asListValue(token); + default: + return token; + } + } + + private void afterValueRead() throws TweedDataReadException { + Context context = peekContext(); + switch (context) { + case MAP_ENTRY_VALUE: + case VALUE: + popContext(context); + } + } + + private Context peekContext() throws TweedDataReadException { + Context context = contextStack.peek(); + if (context == null) { + throw TweedDataReadException.builder() + .message("Tried to read context but currently not in any context") + .build(); + } + return context; + } + + private void popContext(Context expectedContext) throws TweedDataReadException { + Context context = contextStack.pop(); + if (context != expectedContext) { + throw TweedDataReadException.builder() + .message("Unexpected context " + context + " when popping " + expectedContext) + .build(); + } + } + + private TweedDataToken createStringToken(String value) { + return new TweedDataToken() { + @Override + public boolean canReadAsString() { + return true; + } + + @Override + public String readAsString() { + return value; + } + }; + } + + private enum Context { + VALUE, + LIST, + MAP, + MAP_ENTRY_VALUE, + } +} diff --git a/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonWriter.java b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonWriter.java new file mode 100644 index 0000000..b31dc73 --- /dev/null +++ b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/GsonWriter.java @@ -0,0 +1,214 @@ +package de.siphaolor.tweed5.data.gson; + +import com.google.gson.stream.JsonWriter; +import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; +import de.siphalor.tweed5.dataapi.api.TweedDataWriteException; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +public class GsonWriter implements TweedDataVisitor { + private final JsonWriter writer; + + private final Deque contextStack = new ArrayDeque<>(); + + private @Nullable String deferredFieldComment; + + public GsonWriter(JsonWriter writer) { + this.writer = writer; + this.contextStack.push(Context.VALUE); + } + + @Override + public void visitNull() { + try { + writer.nullValue(); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitBoolean(boolean value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitByte(byte value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitShort(short value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitInt(int value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitLong(long value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitFloat(float value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitDouble(double value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitString(String value) { + try { + writer.value(value); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitListStart() { + try { + writer.beginArray(); + contextStack.push(Context.LIST); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitListEnd() { + try { + writer.endArray(); + popContext(Context.LIST); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitMapStart() { + try { + writer.beginObject(); + contextStack.push(Context.MAP); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitMapEntryKey(String key) { + try { + if (deferredFieldComment != null) { + writer.name(key + "__comment"); + writer.value(deferredFieldComment); + deferredFieldComment = null; + } + writer.name(key); + contextStack.push(Context.VALUE); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitMapEnd() { + try { + writer.endObject(); + popContext(Context.MAP); + afterValueWritten(); + } catch (IOException e) { + throw createWriteExceptionFromIoException(e); + } + } + + @Override + public void visitDecoration(TweedDataDecoration decoration) { + if (decoration instanceof TweedDataCommentDecoration) { + if (deferredFieldComment == null) { + deferredFieldComment = ((TweedDataCommentDecoration) decoration).comment(); + } else { + deferredFieldComment += "\n" + ((TweedDataCommentDecoration) decoration).comment(); + } + } + } + + private void afterValueWritten() { + if (peekContext() == Context.VALUE) { + contextStack.pop(); + } + } + + private void popContext(Context expectedContext) { + Context context = contextStack.pop(); + if (context != expectedContext) { + throw new TweedDataWriteException("Unexpected context " + context + " when popping " + expectedContext); + } + } + + private Context peekContext() { + Context context = contextStack.peek(); + if (context == null) { + throw new TweedDataWriteException("Tried to read context but currently not in any context"); + } + return context; + } + + private TweedDataWriteException createWriteExceptionFromIoException(IOException e) { + return new TweedDataWriteException("Error writing data using Gson", e); + } + + private enum Context { + VALUE, + LIST, + MAP, + } +} diff --git a/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/package-info.java b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/package-info.java new file mode 100644 index 0000000..2e0c5ba --- /dev/null +++ b/tweed5-serde-gson/src/main/java/de/siphaolor/tweed5/data/gson/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.siphaolor.tweed5.data.gson; + +import org.jspecify.annotations.NullMarked; diff --git a/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonReaderTest.java b/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonReaderTest.java new file mode 100644 index 0000000..be8f1f0 --- /dev/null +++ b/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonReaderTest.java @@ -0,0 +1,16 @@ +package de.siphaolor.tweed5.data.gson; + +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonReader; +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import de.siphalor.tweed5.testutils.serde.json.JsonReaderTest; + +import java.io.StringReader; + +class GsonReaderTest implements JsonReaderTest { + @Override + public TweedDataReader createJsonReader(String text) { + JsonReader jsonReader = new GsonBuilder().create().newJsonReader(new StringReader(text)); + return new GsonReader(jsonReader); + } +} diff --git a/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonWriterTest.java b/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonWriterTest.java new file mode 100644 index 0000000..1b84aad --- /dev/null +++ b/tweed5-serde-gson/src/test/java/de/siphaolor/tweed5/data/gson/GsonWriterTest.java @@ -0,0 +1,54 @@ +package de.siphaolor.tweed5.data.gson; + +import com.google.gson.GsonBuilder; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +import static org.assertj.core.api.Assertions.assertThat; + +class GsonWriterTest { + + @SneakyThrows + @Test + void complex() { + var stringWriter = new StringWriter(); + var writer = new GsonWriter(new GsonBuilder().setPrettyPrinting().create().newJsonWriter(stringWriter)); + + writer.visitMapStart(); + + writer.visitMapEntryKey("first"); + writer.visitListStart(); + writer.visitInt(123); + writer.visitListStart(); + writer.visitBoolean(false); + writer.visitListEnd(); + writer.visitListEnd(); + + writer.visitDecoration((TweedDataCommentDecoration) () -> "Hello"); + writer.visitDecoration((TweedDataCommentDecoration) () -> "World"); + writer.visitMapEntryKey("second"); + writer.visitMapStart(); + writer.visitMapEntryKey("nested"); + writer.visitDouble(12.34); + writer.visitMapEnd(); + + writer.visitMapEnd(); + + assertThat(stringWriter.toString()).isEqualTo(""" + { + "first": [ + 123, + [ + false + ] + ], + "second__comment": "Hello\\nWorld", + "second": { + "nested": 12.34 + } + }"""); + } +} diff --git a/tweed5-serde-jackson/build.gradle.kts b/tweed5-serde-jackson/build.gradle.kts index 3d104fa..eb352c6 100644 --- a/tweed5-serde-jackson/build.gradle.kts +++ b/tweed5-serde-jackson/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { implementation(project(":tweed5-serde-api")) implementation(libs.jackson.core) shadowOnly(libs.jackson.core) + + testImplementation(project(":serde-json-test-utils")) } tasks.shadowJar { diff --git a/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonReaderTest.java b/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonReaderTest.java index bb3588f..0e96c99 100644 --- a/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonReaderTest.java +++ b/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonReaderTest.java @@ -2,77 +2,21 @@ package de.siphalor.tweed5.data.jackson; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.StreamReadFeature; +import de.siphalor.tweed5.dataapi.api.TweedDataReader; +import de.siphalor.tweed5.testutils.serde.json.JsonReaderTest; import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; -import static org.assertj.core.api.Assertions.assertThat; - -class JacksonReaderTest { +class JacksonReaderTest implements JsonReaderTest { @SneakyThrows - @Test - void complex() { - var inputStream = new ByteArrayInputStream(""" - { - "first": [ - [ 1 ] - ], - "second": { - "test": "Hello World!" - } - } - """.getBytes(StandardCharsets.UTF_8)); - try (var parser = JsonFactory.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build().createParser(inputStream)) { - var reader = new JacksonReader(parser); - - var token = reader.peekToken(); - assertThat(token.isMapStart()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapStart()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapEntryKey()).isTrue(); - assertThat(token.canReadAsString()).isTrue(); - assertThat(token.readAsString()).isEqualTo("first"); - token = reader.readToken(); - assertThat(token.isMapEntryValue()).isTrue(); - assertThat(token.isListStart()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapEntryValue()).isFalse(); - assertThat(token.isListValue()).isTrue(); - assertThat(token.isListStart()).isTrue(); - token = reader.readToken(); - assertThat(token.isListValue()).isTrue(); - assertThat(token.canReadAsInt()).isTrue(); - assertThat(token.readAsInt()).isEqualTo(1); - token = reader.readToken(); - assertThat(token.isListValue()).isTrue(); - assertThat(token.isListEnd()).isTrue(); - token = reader.readToken(); - assertThat(token.isListValue()).isFalse(); - assertThat(token.isMapEntryValue()).isTrue(); - assertThat(token.isListEnd()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapEntryKey()).isTrue(); - assertThat(token.canReadAsString()).isTrue(); - assertThat(token.readAsString()).isEqualTo("second"); - token = reader.readToken(); - assertThat(token.isMapEntryValue()).isTrue(); - assertThat(token.isMapStart()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapEntryValue()).isFalse(); - assertThat(token.isMapEntryKey()).isTrue(); - assertThat(token.canReadAsString()).isTrue(); - assertThat(token.readAsString()).isEqualTo("test"); - token = reader.readToken(); - assertThat(token.isMapEntryValue()).isTrue(); - assertThat(token.canReadAsString()).isTrue(); - assertThat(token.readAsString()).isEqualTo("Hello World!"); - token = reader.readToken(); - assertThat(token.isMapEnd()).isTrue(); - token = reader.readToken(); - assertThat(token.isMapEnd()).isTrue(); - } + @Override + public TweedDataReader createJsonReader(String text) { + var parser = JsonFactory.builder() + .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + .build() + .createParser(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); + return new JacksonReader(parser); } }