diff --git a/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonWriterTest.java b/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonWriterTest.java new file mode 100644 index 0000000..72559e5 --- /dev/null +++ b/test-utils/serde-json/src/main/java/de/siphalor/tweed5/testutils/serde/json/JsonWriterTest.java @@ -0,0 +1,81 @@ +package de.siphalor.tweed5.testutils.serde.json; + +import de.siphalor.tweed5.dataapi.api.TweedDataWriter; +import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.StringWriter; + +import static org.assertj.core.api.Assertions.assertThat; + +public interface JsonWriterTest { + TweedDataWriter createPrettyJsonWriter(StringWriter stringWriter); + + @ParameterizedTest + @CsvSource(ignoreLeadingAndTrailingWhitespace = false, textBlock = """ + 123,"123" + abc def ," abc def " + 'line + breaks + ',"line\\nbreaks\\n" + "quotes","\\"quotes\\"" + """) + @SneakyThrows + default void jsonString(String string, String expected) { + var stringWriter = new StringWriter(); + try (var writer = createPrettyJsonWriter(stringWriter)) { + writer.visitString(string); + } + assertThat(stringWriter.toString()).isEqualTo(expected); + } + + @Test + @SneakyThrows + default void jsonComplex() { + var stringWriter = new StringWriter(); + try (var writer = createPrettyJsonWriter(stringWriter)) { + writer.visitMapStart(); + + writer.visitDecoration((TweedDataCommentDecoration) () -> "The first is the best!"); + writer.visitMapEntryKey("first"); + writer.visitListStart(); + writer.visitInt(123); + writer.visitListStart(); + writer.visitBoolean(false); + writer.visitListEnd(); + writer.visitListEnd(); + + writer.visitDecoration((TweedDataCommentDecoration) () -> "Hello\nWorld"); + writer.visitDecoration((TweedDataCommentDecoration) () -> "!"); + writer.visitMapEntryKey("second"); + writer.visitMapStart(); + writer.visitMapEntryKey("nested"); + writer.visitDouble(12.34); + writer.visitMapEnd(); + + writer.visitMapEnd(); + } + + assertThat(stringWriter.toString()).isEqualToNormalizingNewlines(""" + { + "first__comment": "The first is the best!", + "first": [ + 123, + [ + false + ] + ], + "second__comment": [ + "Hello", + "World", + "!" + ], + "second": { + "nested": 12.34 + } + }"""); + } +} 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 index c9d34c8..3fb0997 100644 --- 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 @@ -1,23 +1,22 @@ 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.TweedDataWriter; 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.ArrayList; import java.util.Deque; +import java.util.List; public class GsonWriter implements TweedDataWriter { private final JsonWriter writer; private final Deque contextStack = new ArrayDeque<>(); - - private @Nullable String deferredFieldComment; + private final List deferredFieldComments = new ArrayList<>(); public GsonWriter(JsonWriter writer) { this.writer = writer; @@ -148,10 +147,18 @@ public class GsonWriter implements TweedDataWriter { @Override public void visitMapEntryKey(String key) { try { - if (deferredFieldComment != null) { + if (!deferredFieldComments.isEmpty()) { writer.name(key + "__comment"); - writer.value(deferredFieldComment); - deferredFieldComment = null; + if (deferredFieldComments.size() == 1) { + writer.value(deferredFieldComments.get(0)); + } else { + writer.beginArray(); + for (String comment : deferredFieldComments) { + writer.value(comment); + } + writer.endArray(); + } + deferredFieldComments.clear(); } writer.name(key); contextStack.push(Context.VALUE); @@ -174,14 +181,25 @@ public class GsonWriter implements TweedDataWriter { @Override public void visitDecoration(TweedDataDecoration decoration) { if (decoration instanceof TweedDataCommentDecoration) { - if (deferredFieldComment == null) { - deferredFieldComment = ((TweedDataCommentDecoration) decoration).comment(); - } else { - deferredFieldComment += "\n" + ((TweedDataCommentDecoration) decoration).comment(); + if (peekContext() == Context.MAP) { + appendDeferredComment(((TweedDataCommentDecoration) decoration).comment()); } } } + private void appendDeferredComment(String comment) { + int index = 0; + while (true) { + int next = comment.indexOf('\n', index); + if (next == -1) { + deferredFieldComments.add(comment.substring(index)); + break; + } + deferredFieldComments.add(comment.substring(index, next)); + index = next + 1; + } + } + @Override public void close() throws Exception { writer.close(); 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 index 1b84aad..c52e24a 100644 --- 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 @@ -1,54 +1,16 @@ package de.siphaolor.tweed5.data.gson; import com.google.gson.GsonBuilder; -import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; +import de.siphalor.tweed5.dataapi.api.TweedDataWriter; +import de.siphalor.tweed5.testutils.serde.json.JsonWriterTest; import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; import java.io.StringWriter; -import static org.assertj.core.api.Assertions.assertThat; - -class GsonWriterTest { - +class GsonWriterTest implements JsonWriterTest { + @Override @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 - } - }"""); + public TweedDataWriter createPrettyJsonWriter(StringWriter stringWriter) { + return new GsonWriter(new GsonBuilder().setPrettyPrinting().create().newJsonWriter(stringWriter)); } } diff --git a/tweed5-serde-jackson/src/main/java/de/siphalor/tweed5/data/jackson/JacksonWriter.java b/tweed5-serde-jackson/src/main/java/de/siphalor/tweed5/data/jackson/JacksonWriter.java index 7acdcaf..dbc9397 100644 --- a/tweed5-serde-jackson/src/main/java/de/siphalor/tweed5/data/jackson/JacksonWriter.java +++ b/tweed5-serde-jackson/src/main/java/de/siphalor/tweed5/data/jackson/JacksonWriter.java @@ -1,23 +1,23 @@ package de.siphalor.tweed5.data.jackson; import com.fasterxml.jackson.core.JsonGenerator; -import de.siphalor.tweed5.dataapi.api.TweedDataVisitor; import de.siphalor.tweed5.dataapi.api.TweedDataWriteException; import de.siphalor.tweed5.dataapi.api.TweedDataWriter; 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.ArrayList; import java.util.Deque; +import java.util.List; public class JacksonWriter implements TweedDataWriter { private final JsonGenerator generator; private final CommentWriteMode commentWriteMode; private final Deque contextStack = new ArrayDeque<>(); - private @Nullable String deferredFieldComment; + private final List deferredFieldComments = new ArrayList<>(); public JacksonWriter(JsonGenerator generator, CommentWriteMode commentWriteMode) { this.generator = generator; @@ -149,10 +149,18 @@ public class JacksonWriter implements TweedDataWriter { @Override public void visitMapEntryKey(String key) { try { - if (deferredFieldComment != null) { + if (!deferredFieldComments.isEmpty()) { generator.writeFieldName(key + "__comment"); - generator.writeString(deferredFieldComment); - deferredFieldComment = null; + if (deferredFieldComments.size() == 1) { + generator.writeString(deferredFieldComments.get(0)); + } else { + generator.writeStartArray(); + for (String deferredFieldComment : deferredFieldComments) { + generator.writeString(deferredFieldComment); + } + generator.writeEndArray(); + } + deferredFieldComments.clear(); } generator.writeFieldName(key); contextStack.push(Context.VALUE); @@ -180,11 +188,7 @@ public class JacksonWriter implements TweedDataWriter { break; case MAP_ENTRIES: if (contextStack.peek() == Context.MAP) { - if (deferredFieldComment == null) { - deferredFieldComment = ((TweedDataCommentDecoration) decoration).comment(); - } else { - deferredFieldComment += "\n" + ((TweedDataCommentDecoration) decoration).comment(); - } + appendDeferredComment(((TweedDataCommentDecoration) decoration).comment()); } break; case DOUBLE_SLASHES: @@ -199,6 +203,19 @@ public class JacksonWriter implements TweedDataWriter { } } + private void appendDeferredComment(String comment) { + int index = 0; + while (true) { + int next = comment.indexOf('\n', index); + if (next == -1) { + deferredFieldComments.add(comment.substring(index)); + break; + } + deferredFieldComments.add(comment.substring(index, next)); + index = next + 1; + } + } + @Override public void close() throws Exception { generator.close(); diff --git a/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonWriterTest.java b/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonWriterTest.java index e5e97cd..c827716 100644 --- a/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonWriterTest.java +++ b/tweed5-serde-jackson/src/test/java/de/siphalor/tweed5/data/jackson/JacksonWriterTest.java @@ -1,77 +1,26 @@ package de.siphalor.tweed5.data.jackson; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.core.util.Separators; -import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; +import de.siphalor.tweed5.dataapi.api.TweedDataWriter; +import de.siphalor.tweed5.testutils.serde.json.JsonWriterTest; import lombok.SneakyThrows; -import org.junit.jupiter.api.Test; import java.io.StringWriter; -import static org.assertj.core.api.Assertions.assertThat; - -class JacksonWriterTest { +class JacksonWriterTest implements JsonWriterTest { + @Override @SneakyThrows - @Test - void object() { - var stringWriter = new StringWriter(); - try (var generator = JsonFactory.builder().build().createGenerator(stringWriter)) { - generator.setPrettyPrinter( - new DefaultPrettyPrinter() - .withSeparators(new Separators().withObjectFieldValueSpacing(Separators.Spacing.AFTER)) - ); - var writer = new JacksonWriter(generator, JacksonWriter.CommentWriteMode.MAP_ENTRIES); - writer.visitMapStart(); - writer.visitDecoration((TweedDataCommentDecoration) () -> "Hello\nWorld"); - writer.visitDecoration((TweedDataCommentDecoration) () -> "!"); - writer.visitMapEntryKey("test"); - writer.visitInt(1234); - writer.visitMapEnd(); - } - - assertThat(stringWriter.toString()).isEqualTo(""" - { - "test__comment": "Hello\\nWorld\\n!", - "test": 1234 - }"""); - } - - @SneakyThrows - @Test - void complex() { - var stringWriter = new StringWriter(); - try (var generator = JsonFactory.builder().build().createGenerator(stringWriter)) { - generator.setPrettyPrinter( - new DefaultPrettyPrinter() - .withSeparators(new Separators().withObjectFieldValueSpacing(Separators.Spacing.AFTER)) - ); - var writer = new JacksonWriter(generator, JacksonWriter.CommentWriteMode.MAP_ENTRIES); - writer.visitMapStart(); - writer.visitMapEntryKey("first"); - writer.visitListStart(); - writer.visitInt(1); - writer.visitDecoration((TweedDataCommentDecoration) () -> "not written"); - writer.visitInt(2); - writer.visitListEnd(); - writer.visitDecoration((TweedDataCommentDecoration) () -> "second object"); - writer.visitMapEntryKey("second"); - writer.visitMapStart(); - writer.visitDecoration((TweedDataCommentDecoration) () -> "inner entry"); - writer.visitMapEntryKey("inner"); - writer.visitBoolean(true); - writer.visitMapEnd(); - writer.visitMapEnd(); - } - - assertThat(stringWriter.toString()).isEqualTo(""" - { - "first": [ 1, 2 ], - "second__comment": "second object", - "second": { - "inner__comment": "inner entry", - "inner": true - } - }"""); + public TweedDataWriter createPrettyJsonWriter(StringWriter stringWriter) { + return new JacksonWriter(JsonFactory.builder() + .build() + .createGenerator(stringWriter) + .setPrettyPrinter(new DefaultPrettyPrinter() + .withSeparators(new Separators().withObjectFieldValueSpacing(Separators.Spacing.AFTER)) + .withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE) + ), JacksonWriter.CommentWriteMode.MAP_ENTRIES + ); } }