fix(serialize-json): align handling of names in arrays with serialize-xml
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2024-04-14 10:12:23 +02:00
parent 4a1944f792
commit 2bcaf3336e
Signed by: Johannes
GPG Key ID: E76429612C2929F4
3 changed files with 111 additions and 14 deletions

View File

@ -110,11 +110,23 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
private String[] pathNames = new String[32];
private int[] pathIndices = new int[32];
private boolean wroteName = false;
private Heuristics heuristics = Heuristics.DEFAULT;
/** Creates a new instance that reads a JSON-encoded stream from {@code in}. */
public JsonReader(Reader in) {
this.in = Objects.requireNonNull(in, "in == null");
}
public JsonReader setHeuristics(Heuristics heuristics) {
this.heuristics = Objects.requireNonNull(heuristics);
return this;
}
public Heuristics getHeuristics() {
return heuristics;
}
@Override
public JsonReader beginArray() throws IOException {
int p = peeked;
@ -122,6 +134,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_BEGIN_ARRAY) {
wroteName = false;
push(JsonScope.EMPTY_ARRAY);
pathIndices[stackSize - 1] = 0;
peeked = PEEKED_NONE;
@ -138,6 +151,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_END_ARRAY) {
wroteName = false;
stackSize--;
pathIndices[stackSize - 1]++;
peeked = PEEKED_NONE;
@ -154,6 +168,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_BEGIN_OBJECT) {
wroteName = false;
push(JsonScope.EMPTY_OBJECT);
peeked = PEEKED_NONE;
return this;
@ -169,6 +184,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_END_OBJECT) {
wroteName = false;
stackSize--;
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
pathIndices[stackSize - 1]++;
@ -576,8 +592,16 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
result = nextQuotedValue('"');
} else {
// If we are in an array, allow reading an in inferred name once
if (!wroteName) {
if (stack[stackSize - 1] == JsonScope.EMPTY_ARRAY || stack[stackSize - 1] == JsonScope.NONEMPTY_ARRAY) {
wroteName = true;
return heuristics.guessArrayElementName(getPath());
}
}
throw unexpectedTokenError("a name");
}
wroteName = true;
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = result;
return result;
@ -607,6 +631,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
} else {
throw unexpectedTokenError("a string");
}
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -619,10 +644,12 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_TRUE) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return true;
} else if (p == PEEKED_FALSE) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return false;
@ -637,6 +664,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
p = doPeek();
}
if (p == PEEKED_NULL) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
} else {
@ -652,6 +680,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
if (p == PEEKED_LONG) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return (double) peekedLong;
@ -673,6 +702,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
if (!serializeSpecialFloatingPointValues && (Double.isNaN(result) || Double.isInfinite(result))) {
throw syntaxError("JSON forbids NaN and infinities: " + result);
}
wroteName = false;
peekedString = null;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
@ -687,6 +717,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
if (p == PEEKED_LONG) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return peekedLong;
@ -703,6 +734,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
try {
long result = Long.parseLong(peekedString);
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -720,6 +752,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
throw new NumberFormatException("Expected a long but was " + peekedString + locationString());
}
peekedString = null;
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -738,6 +771,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
throw new NumberFormatException("Expected an int but was " + peekedLong + locationString());
}
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -754,6 +788,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
try {
result = Integer.parseInt(peekedString);
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -775,6 +810,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
}
peekedString = null;
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -788,6 +824,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
}
if (p == PEEKED_LONG) {
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return (double) peekedLong;
@ -812,6 +849,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
throw syntaxError("JSON forbids NaN and infinities: " + result);
}
peekedString = null;
wroteName = false;
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return result;
@ -829,13 +867,16 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
switch (p) {
case PEEKED_BEGIN_ARRAY:
push(JsonScope.EMPTY_ARRAY);
wroteName = false;
count++;
break;
case PEEKED_BEGIN_OBJECT:
push(JsonScope.EMPTY_OBJECT);
wroteName = false;
count++;
break;
case PEEKED_END_ARRAY:
wroteName = false;
stackSize--;
count--;
break;
@ -846,20 +887,25 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
// Free the last path name so that it can be garbage collected
pathNames[stackSize - 1] = null;
}
wroteName = false;
stackSize--;
count--;
break;
case PEEKED_UNQUOTED:
wroteName = false;
skipUnquotedValue();
break;
case PEEKED_SINGLE_QUOTED:
wroteName = false;
skipQuotedValue('\'');
break;
case PEEKED_DOUBLE_QUOTED:
wroteName = false;
skipQuotedValue('"');
break;
case PEEKED_UNQUOTED_NAME:
skipUnquotedValue();
wroteName = true;
// Only update when name is explicitly skipped, otherwise stack is not updated anyways
if (count == 0) {
pathNames[stackSize - 1] = "<skipped>";
@ -867,6 +913,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
break;
case PEEKED_SINGLE_QUOTED_NAME:
skipQuotedValue('\'');
wroteName = true;
// Only update when name is explicitly skipped, otherwise stack is not updated anyways
if (count == 0) {
pathNames[stackSize - 1] = "<skipped>";
@ -874,12 +921,14 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
break;
case PEEKED_DOUBLE_QUOTED_NAME:
skipQuotedValue('"');
wroteName = true;
// Only update when name is explicitly skipped, otherwise stack is not updated anyways
if (count == 0) {
pathNames[stackSize - 1] = "<skipped>";
}
break;
case PEEKED_NUMBER:
wroteName = false;
pos += peekedNumberLength;
break;
case PEEKED_EOF:
@ -887,6 +936,7 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
default:
// For all other tokens there is nothing to do; token has already been consumed from
// underlying reader
wroteName = false;
}
peeked = PEEKED_NONE;
} while (count > 0);
@ -1425,6 +1475,11 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
pos += 5;
}
public interface Heuristics {
String guessArrayElementName(String path);
Heuristics DEFAULT = (path) -> "item";
}
@Override
public void close() throws IOException {
peeked = PEEKED_NONE;

View File

@ -29,6 +29,7 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
private boolean omitQuotes = false;
private String deferredName;
private final List<String> deferredComments = new LinkedList<>();
private boolean commentUnexpectedNames = false;
public JsonWriter(Writer out) {
this.out = Objects.requireNonNull(out, "out == null");
@ -83,6 +84,15 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
return omitQuotes;
}
public JsonWriter setCommentUnexpectedNames(boolean commentUnexpectedNames) {
this.commentUnexpectedNames = commentUnexpectedNames;
return this;
}
public boolean isCommentUnexpectedNames() {
return commentUnexpectedNames;
}
@Override
public JsonWriter beginArray() throws IOException {
writeDeferredName();
@ -118,7 +128,8 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
throw new IllegalStateException("Nesting problem.");
}
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
if (lenient) nullValue();
else throw new IllegalStateException("Dangling name: " + deferredName);
}
if (!deferredComments.isEmpty()) {
@ -192,7 +203,11 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
}
int context = peek();
if (context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) {
throw new IllegalStateException("Please begin an object before writing a name.");
if (lenient) {
if (context != EMPTY_ARRAY && context != NONEMPTY_ARRAY) throw new IllegalStateException("Please begin an object or array before writing a name.");
} else {
throw new IllegalStateException("Please begin an object before writing a name.");
}
}
deferredName = name;
return this;
@ -200,12 +215,19 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
private void writeDeferredName() throws IOException {
if (deferredName != null) {
beforeName();
if (omitQuotes && deferredName.matches("[a-zA-Z_$][\\w$]*")) {
out.write(deferredName);
}
else {
string(deferredName);
int context = peek();
if (context == EMPTY_ARRAY || context == NONEMPTY_ARRAY) {
if (commentUnexpectedNames) {
// Write the name as a comment instead of literally
comment(deferredName);
}
} else {
beforeName();
if (omitQuotes && deferredName.matches("[a-zA-Z_$][\\w$]*")) {
out.write(deferredName);
} else {
string(deferredName);
}
}
deferredName = null;
}

View File

@ -153,21 +153,41 @@ public final class JsonWriterTest {
@Test
public void testNameInArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
JsonWriter jsonWriter = new JsonWriter(stringWriter).setLenient(true);
jsonWriter.beginArray();
jsonWriter.name("hello");
IllegalStateException e =
assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
assertThrows(IllegalStateException.class, () -> jsonWriter.name("world"));
assertThat(e).hasMessageThat().isEqualTo("Already wrote a name, expecting a value.");
jsonWriter.value(12);
e = assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
jsonWriter.name("world2");
jsonWriter.endArray();
jsonWriter.close();
assertThat(stringWriter.toString()).isEqualTo("[12]");
assertThat(stringWriter.toString()).isEqualTo("[12,null]");
}
@Test
public void testCommentNameInArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter).setLenient(true).setCommentUnexpectedNames(true);
jsonWriter.beginArray();
jsonWriter.name("hello");
IllegalStateException e =
assertThrows(IllegalStateException.class, () -> jsonWriter.name("world"));
assertThat(e).hasMessageThat().isEqualTo("Already wrote a name, expecting a value.");
jsonWriter.value(12);
jsonWriter.name("world2");
jsonWriter.endArray();
jsonWriter.close();
assertThat(stringWriter.toString()).isEqualTo("[/* hello */12,/* world2 */null]");
}
@Test