[serde-gson,serde-jackson] Unify JSON writer tests

This commit is contained in:
2025-08-03 21:57:13 +02:00
parent a666ac6c33
commit 32831c7c22
5 changed files with 158 additions and 131 deletions

View File

@@ -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
}
}""");
}
}

View File

@@ -1,23 +1,22 @@
package de.siphaolor.tweed5.data.gson; package de.siphaolor.tweed5.data.gson;
import com.google.gson.stream.JsonWriter; 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.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration; import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import org.jspecify.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List;
public class GsonWriter implements TweedDataWriter { public class GsonWriter implements TweedDataWriter {
private final JsonWriter writer; private final JsonWriter writer;
private final Deque<Context> contextStack = new ArrayDeque<>(); private final Deque<Context> contextStack = new ArrayDeque<>();
private final List<String> deferredFieldComments = new ArrayList<>();
private @Nullable String deferredFieldComment;
public GsonWriter(JsonWriter writer) { public GsonWriter(JsonWriter writer) {
this.writer = writer; this.writer = writer;
@@ -148,10 +147,18 @@ public class GsonWriter implements TweedDataWriter {
@Override @Override
public void visitMapEntryKey(String key) { public void visitMapEntryKey(String key) {
try { try {
if (deferredFieldComment != null) { if (!deferredFieldComments.isEmpty()) {
writer.name(key + "__comment"); writer.name(key + "__comment");
writer.value(deferredFieldComment); if (deferredFieldComments.size() == 1) {
deferredFieldComment = null; writer.value(deferredFieldComments.get(0));
} else {
writer.beginArray();
for (String comment : deferredFieldComments) {
writer.value(comment);
}
writer.endArray();
}
deferredFieldComments.clear();
} }
writer.name(key); writer.name(key);
contextStack.push(Context.VALUE); contextStack.push(Context.VALUE);
@@ -174,14 +181,25 @@ public class GsonWriter implements TweedDataWriter {
@Override @Override
public void visitDecoration(TweedDataDecoration decoration) { public void visitDecoration(TweedDataDecoration decoration) {
if (decoration instanceof TweedDataCommentDecoration) { if (decoration instanceof TweedDataCommentDecoration) {
if (deferredFieldComment == null) { if (peekContext() == Context.MAP) {
deferredFieldComment = ((TweedDataCommentDecoration) decoration).comment(); appendDeferredComment(((TweedDataCommentDecoration) decoration).comment());
} else {
deferredFieldComment += "\n" + ((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 @Override
public void close() throws Exception { public void close() throws Exception {
writer.close(); writer.close();

View File

@@ -1,54 +1,16 @@
package de.siphaolor.tweed5.data.gson; package de.siphaolor.tweed5.data.gson;
import com.google.gson.GsonBuilder; 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 lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.io.StringWriter; import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat; class GsonWriterTest implements JsonWriterTest {
@Override
class GsonWriterTest {
@SneakyThrows @SneakyThrows
@Test public TweedDataWriter createPrettyJsonWriter(StringWriter stringWriter) {
void complex() { return new GsonWriter(new GsonBuilder().setPrettyPrinting().create().newJsonWriter(stringWriter));
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
}
}""");
} }
} }

View File

@@ -1,23 +1,23 @@
package de.siphalor.tweed5.data.jackson; package de.siphalor.tweed5.data.jackson;
import com.fasterxml.jackson.core.JsonGenerator; 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.TweedDataWriteException;
import de.siphalor.tweed5.dataapi.api.TweedDataWriter; import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration; import de.siphalor.tweed5.dataapi.api.decoration.TweedDataCommentDecoration;
import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration; import de.siphalor.tweed5.dataapi.api.decoration.TweedDataDecoration;
import org.jspecify.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List;
public class JacksonWriter implements TweedDataWriter { public class JacksonWriter implements TweedDataWriter {
private final JsonGenerator generator; private final JsonGenerator generator;
private final CommentWriteMode commentWriteMode; private final CommentWriteMode commentWriteMode;
private final Deque<Context> contextStack = new ArrayDeque<>(); private final Deque<Context> contextStack = new ArrayDeque<>();
private @Nullable String deferredFieldComment; private final List<String> deferredFieldComments = new ArrayList<>();
public JacksonWriter(JsonGenerator generator, CommentWriteMode commentWriteMode) { public JacksonWriter(JsonGenerator generator, CommentWriteMode commentWriteMode) {
this.generator = generator; this.generator = generator;
@@ -149,10 +149,18 @@ public class JacksonWriter implements TweedDataWriter {
@Override @Override
public void visitMapEntryKey(String key) { public void visitMapEntryKey(String key) {
try { try {
if (deferredFieldComment != null) { if (!deferredFieldComments.isEmpty()) {
generator.writeFieldName(key + "__comment"); generator.writeFieldName(key + "__comment");
generator.writeString(deferredFieldComment); if (deferredFieldComments.size() == 1) {
deferredFieldComment = null; generator.writeString(deferredFieldComments.get(0));
} else {
generator.writeStartArray();
for (String deferredFieldComment : deferredFieldComments) {
generator.writeString(deferredFieldComment);
}
generator.writeEndArray();
}
deferredFieldComments.clear();
} }
generator.writeFieldName(key); generator.writeFieldName(key);
contextStack.push(Context.VALUE); contextStack.push(Context.VALUE);
@@ -180,11 +188,7 @@ public class JacksonWriter implements TweedDataWriter {
break; break;
case MAP_ENTRIES: case MAP_ENTRIES:
if (contextStack.peek() == Context.MAP) { if (contextStack.peek() == Context.MAP) {
if (deferredFieldComment == null) { appendDeferredComment(((TweedDataCommentDecoration) decoration).comment());
deferredFieldComment = ((TweedDataCommentDecoration) decoration).comment();
} else {
deferredFieldComment += "\n" + ((TweedDataCommentDecoration) decoration).comment();
}
} }
break; break;
case DOUBLE_SLASHES: 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 @Override
public void close() throws Exception { public void close() throws Exception {
generator.close(); generator.close();

View File

@@ -1,77 +1,26 @@
package de.siphalor.tweed5.data.jackson; package de.siphalor.tweed5.data.jackson;
import com.fasterxml.jackson.core.JsonFactory; 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.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.Separators; 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 lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import java.io.StringWriter; import java.io.StringWriter;
import static org.assertj.core.api.Assertions.assertThat; class JacksonWriterTest implements JsonWriterTest {
@Override
class JacksonWriterTest {
@SneakyThrows @SneakyThrows
@Test public TweedDataWriter createPrettyJsonWriter(StringWriter stringWriter) {
void object() { return new JacksonWriter(JsonFactory.builder()
var stringWriter = new StringWriter(); .build()
try (var generator = JsonFactory.builder().build().createGenerator(stringWriter)) { .createGenerator(stringWriter)
generator.setPrettyPrinter( .setPrettyPrinter(new DefaultPrettyPrinter()
new DefaultPrettyPrinter() .withSeparators(new Separators().withObjectFieldValueSpacing(Separators.Spacing.AFTER))
.withSeparators(new Separators().withObjectFieldValueSpacing(Separators.Spacing.AFTER)) .withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE)
); ), JacksonWriter.CommentWriteMode.MAP_ENTRIES
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
}
}""");
} }
} }