Skip to content

Commit 968cb2d

Browse files
committed
feat: Add support for skip-nulls option to Parser.java
Parser will now honor JsonUrlOptions.isSkipNulls() and skip null values during parse so they will not present in the parse result.
1 parent 5dddd1d commit 968cb2d

File tree

7 files changed

+198
-41
lines changed

7 files changed

+198
-41
lines changed

module/jsonurl-core/src/main/java/org/jsonurl/BaseJsonUrlOptions.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,28 @@ public void setSkipNulls(boolean skipNulls) {
129129
* <pre>
130130
* setEmptyUnquotedKeyAllowed(true);
131131
* setEmptyUnquotedValueAllowed(true);
132+
* setSkipNulls(true);
132133
* setImpliedStringLiterals(true);
133134
* </pre>
134135
*
135136
* <p>If {@link #isImpliedStringLiterals()} is true but
136137
* {@link #isEmptyUnquotedValueAllowed()} is false then an empty string
137138
* literal value will trigger an Exception because there wouldn't be any
138139
* way to represent it.
139-
*
140+
*
140141
* <p>If {@link #isImpliedStringLiterals()} is true but
141142
* {@link #isEmptyUnquotedKeyAllowed()} is false then an empty key will
142143
* trigger an Exception because there wouldn't be any way to represent it.
143-
*
144+
*
145+
* <p>If {@link #isImpliedStringLiterals()} is true but
146+
* {@link #isSkipNulls()} is false:<ul>
147+
* <li>when writing to a {@link JsonTextBuilder} - a {@code null} will
148+
* trigger an Exception because there wouldn't be a way to represent it.
149+
* <li>during parse - no exception would occur (because a {@code null}
150+
* value isn't possible) but the parse would happen as though
151+
* {@code null} values were not present in the stream.
152+
* </ul>
153+
*
144154
* <p>If you want the above behavior, because your data doesn't
145155
* support those cases and you want the parser to catch them for you,
146156
* then call {@link #setImpliedStringLiterals(boolean)
@@ -153,6 +163,7 @@ public void setSkipNulls(boolean skipNulls) {
153163
public void enableImpliedStringLiterals() {
154164
setEmptyUnquotedKeyAllowed(true);
155165
setEmptyUnquotedValueAllowed(true);
166+
setSkipNulls(true);
156167
setImpliedStringLiterals(true);
157168
}
158169
}

module/jsonurl-core/src/main/java/org/jsonurl/ValueFactoryParseResultFacade.java

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.jsonurl.JsonUrl.Parse.literal;
2121
import static org.jsonurl.JsonUrl.Parse.literalToJavaString;
2222
import static org.jsonurl.JsonUrl.Parse.newNumberBuilder;
23+
import static org.jsonurl.JsonUrlOptions.isSkipNulls;
2324
import static org.jsonurl.SyntaxException.Message.MSG_EXPECT_OBJECT_VALUE;
2425

2526
import java.util.Deque;
@@ -187,18 +188,29 @@ public void addSingleElementArray(
187188
CharSequence text,
188189
int start,
189190
int stop) {
190-
ABT sea = factory.newArrayBuilder();
191-
192-
factory.add(
193-
sea,
194-
literal(
191+
192+
V val = literal(
195193
buf,
196194
numb,
197195
text,
198196
start,
199197
stop,
200198
factory,
201-
options));
199+
options);
200+
201+
ABT sea = factory.newArrayBuilder();
202+
203+
boolean skip = factory.isNull(val) && isSkipNulls(options);
204+
if (!skip) {
205+
//
206+
// note that this produces an empty array rather than the empty
207+
// composite value. This is really an implementation choice -- I
208+
// think either would be "correct". I chose this because it isn't
209+
// ambiguous; we know it's an array by the syntax, even if it's
210+
// empty.
211+
//
212+
factory.add(sea, val);
213+
}
202214

203215
factoryValueStack.push(factory.newArray(sea));
204216
}
@@ -219,10 +231,14 @@ public void addObjectKey(
219231
public ParseResultFacade<V> addArrayElement() {
220232
V topval = factoryValueStack.pop();
221233

222-
@SuppressWarnings("unchecked")
223-
ABT destArray = (ABT)builderStack.peek();
234+
boolean skip = factory.isNull(topval) && isSkipNulls(options);
224235

225-
factory.add(destArray, topval);
236+
if (!skip) {
237+
@SuppressWarnings("unchecked")
238+
ABT destArray = (ABT)builderStack.peek();
239+
240+
factory.add(destArray, topval);
241+
}
226242
return this;
227243
}
228244

@@ -231,10 +247,14 @@ public ParseResultFacade<V> addObjectElement() {
231247
V topval = factoryValueStack.pop();
232248
String key = keyStack.pop();
233249

234-
@SuppressWarnings("unchecked")
235-
JBT builder = (JBT)builderStack.peek();
250+
boolean skip = factory.isNull(topval) && isSkipNulls(options);
236251

237-
factory.put(builder, key, topval);
252+
if (!skip) {
253+
@SuppressWarnings("unchecked")
254+
JBT builder = (JBT)builderStack.peek();
255+
256+
factory.put(builder, key, topval);
257+
}
238258
return this;
239259
}
240260

@@ -291,11 +311,6 @@ public ParseResultFacade<V> addMissingValue(
291311
int start,
292312
int stop) {
293313

294-
//
295-
// note that isEmptyUnquotedStringOK is always false. This makes
296-
// sense when you think about what's happening. We're supplying a
297-
// missing value for a given key, so the key must be present.
298-
//
299314
final String key = parseKey(
300315
text,
301316
start,

module/jsonurl-core/src/test/java/org/jsonurl/AbstractParseTest.java

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,62 @@ void testAllowEmptyUnquotedArrayValue(String text) {
16081608
factory,
16091609
factory.newArrayBuilder()));
16101610
}
1611-
1611+
1612+
private void assertSkipNullArrayValue(String text, A actual) {
1613+
assertEquals(
1614+
factory.getNumber(new NumberBuilder("1")),
1615+
getNumber(0, actual),
1616+
text);
1617+
1618+
assertEquals(
1619+
factory.getNumber(new NumberBuilder("3")),
1620+
getNumber(1, actual),
1621+
text);
1622+
}
1623+
1624+
@ParameterizedTest
1625+
@Tag(TAG_PARSE)
1626+
@ValueSource(strings = {
1627+
"(1,null,3)",
1628+
"(1,null,null,null,null,3)",
1629+
"(null,1,null,null,null,null,3,null)",
1630+
})
1631+
void testSkipNullArrayValue(String text) {
1632+
Parser p = new Parser();
1633+
p.setSkipNulls(true);
1634+
1635+
assertTrue(p.isSkipNulls(), text);
1636+
1637+
assertSkipNullArrayValue(text, p.parseArray(text, factory));
1638+
1639+
assertSkipNullArrayValue(
1640+
text,
1641+
p.parseArray(
1642+
makeImplied(text),
1643+
factory,
1644+
factory.newArrayBuilder()));
1645+
}
1646+
1647+
@ParameterizedTest
1648+
@Tag(TAG_PARSE)
1649+
@ValueSource(strings = {
1650+
"(null)",
1651+
"(null,null,null,null)",
1652+
})
1653+
void testSkipNullSingleArrayValue(String text) {
1654+
Parser p = new Parser();
1655+
p.setSkipNulls(true);
1656+
1657+
assertTrue(p.isSkipNulls(), text);
1658+
assertEquals(0, getSize(p.parseArray(text, factory)), text);
1659+
assertEquals(0, getSize(
1660+
p.parseArray(
1661+
makeImplied(text),
1662+
factory,
1663+
factory.newArrayBuilder())),
1664+
text);
1665+
}
1666+
16121667
private void assertAllowEmptyUnquotedObjectValue(
16131668
String text,
16141669
J actual) {
@@ -1620,23 +1675,7 @@ private void assertAllowEmptyUnquotedObjectValue(
16201675

16211676
assertTrue(getBoolean("b", actual), text);
16221677
}
1623-
1624-
private void assertAllowEmptyUnquotedObjectValue(
1625-
Parser p,
1626-
String text) {
16271678

1628-
assertAllowEmptyUnquotedObjectValue(
1629-
text,
1630-
p.parseObject(text, factory));
1631-
1632-
assertAllowEmptyUnquotedObjectValue(
1633-
text,
1634-
p.parseObject(
1635-
makeImplied(text),
1636-
factory,
1637-
factory.newObjectBuilder()));
1638-
}
1639-
16401679
@ParameterizedTest
16411680
@Tag(TAG_PARSE)
16421681
@ValueSource(strings = {
@@ -1655,9 +1694,50 @@ void testAllowEmptyUnquotedObjectValue(String text) {
16551694
//
16561695
p.setEmptyUnquotedValueAllowed(true);
16571696
assertTrue(p.isEmptyUnquotedValueAllowed(), text);
1658-
assertAllowEmptyUnquotedObjectValue(p, text);
1697+
1698+
assertAllowEmptyUnquotedObjectValue(
1699+
text,
1700+
p.parseObject(text, factory));
1701+
1702+
assertAllowEmptyUnquotedObjectValue(
1703+
text,
1704+
p.parseObject(
1705+
makeImplied(text),
1706+
factory,
1707+
factory.newObjectBuilder()));
16591708
}
16601709

1710+
private void assertSkipNullObjectValue(String text, J actual) {
1711+
assertTrue(getNull("a", actual), text);
1712+
assertTrue(getBoolean("b", actual), text);
1713+
}
1714+
1715+
@ParameterizedTest
1716+
@Tag(TAG_PARSE)
1717+
@ValueSource(strings = {
1718+
"(a:null,b:true)",
1719+
"(b:true,a:null)"
1720+
})
1721+
void testSkipNullObjectValue(String text) {
1722+
Parser p = new Parser();
1723+
1724+
//
1725+
// setEmptyUnquotedValueAllowed(true)
1726+
//
1727+
p.setSkipNulls(true);
1728+
assertTrue(p.isSkipNulls(), text);
1729+
1730+
assertSkipNullObjectValue(
1731+
text,
1732+
p.parseObject(text, factory));
1733+
1734+
assertSkipNullObjectValue(
1735+
text,
1736+
p.parseObject(
1737+
makeImplied(text),
1738+
factory,
1739+
factory.newObjectBuilder()));
1740+
}
16611741

16621742
private void assertAllowEmptyUnquotedKey(String text, J actual) {
16631743
assertFalse(getBoolean("", actual), text);
@@ -1971,6 +2051,7 @@ private static final String urlEncodeAndEscapeStructChars(String s)
19712051
protected abstract M getNumber(String key, J value);
19722052
protected abstract Number getNumberValue(V value);
19732053
protected abstract String getStringValue(V value);
2054+
protected abstract int getSize(A value);
19742055
protected abstract ValueFactory<V,C,ABT,A,JBT,J,B,M,N,S> newBigMathFactory(
19752056
MathContext mc,
19762057
String boundNeg,

module/jsonurl-core/src/test/java/org/jsonurl/AbstractWriteTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.jsonurl.AbstractParseTest.makeImplied;
2121
import static org.junit.jupiter.api.Assertions.assertEquals;
2222
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
2324
import static org.junit.jupiter.api.Assertions.assertTrue;
2425

2526
import java.io.IOException;
@@ -66,6 +67,41 @@ protected abstract <I,R> boolean write(
6667
*/
6768
protected abstract ValueFactory<V,C,ABT,A,JBT,J,?,?,?,?> getFactory();
6869

70+
@ParameterizedTest
71+
@ValueSource(strings = {
72+
"",
73+
})
74+
void testEmptyString(String text) throws IOException {
75+
String test = "empty string";
76+
String expected = "''";
77+
JsonUrlStringBuilder jup = new JsonUrlStringBuilder();
78+
assertEquals(expected, jup.add(text).build(), test);
79+
assertEquals(expected, jup.clear().addKey(text).build(), test);
80+
81+
jup.setImpliedStringLiterals(true);
82+
assertThrows(
83+
IOException.class,
84+
() -> jup.clear().add(text).build());
85+
assertThrows(
86+
IOException.class,
87+
() -> jup.clear().addKey(text).build());
88+
89+
expected = "";
90+
jup.setEmptyUnquotedKeyAllowed(true);
91+
jup.setEmptyUnquotedValueAllowed(false);
92+
assertEquals(expected, jup.addKey(text).build(), test);
93+
assertThrows(
94+
IOException.class,
95+
() -> jup.clear().add(text).build());
96+
97+
jup.setEmptyUnquotedKeyAllowed(false);
98+
jup.setEmptyUnquotedValueAllowed(true);
99+
assertEquals(expected, jup.add(text).build(), test);
100+
assertThrows(
101+
IOException.class,
102+
() -> jup.clear().addKey(text).build());
103+
}
104+
69105
@ParameterizedTest
70106
@ValueSource(strings = {
71107
"null",

module/jsonurl-core/src/test/java/org/jsonurl/j2se/AbstractJavaValueFactoryParseTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ protected boolean getBoolean(String key, Map<String,Object> value) {
6363
@Override
6464
protected boolean getNull(String key, Map<String,Object> value) {
6565
Object ret = value.get(key);
66-
return factory.getNull() == ret;
66+
return factory.isNull(ret);
6767
}
6868

6969
@Override
@@ -130,6 +130,11 @@ protected String getStringValue(Object value) {
130130
return value instanceof String ? (String)value : null;
131131
}
132132

133+
@Override
134+
protected int getSize(List<Object> value) {
135+
return factory.isNull(value) ? 0 : value.size();
136+
}
137+
133138
@Override
134139
protected JavaValueFactory newBigMathFactory(
135140
MathContext mctxt,

module/jsonurl-jsonorg/src/test/java/org/jsonurl/jsonorg/AbstractJsonOrgParseTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ protected boolean getBoolean(String key, JSONObject value) {
104104

105105
@Override
106106
protected boolean getNull(String key, JSONObject value) {
107-
Object ret = value.get(key);
108-
return factory.getNull() == ret;
107+
return value.isNull(key);
109108
}
110109

111110
@Override
@@ -128,6 +127,11 @@ protected String getStringValue(Object value) {
128127
return value instanceof String ? (String)value : null;
129128
}
130129

130+
@Override
131+
protected int getSize(JSONArray value) {
132+
return factory.isNull(value) ? 0 : value.length();
133+
}
134+
131135
@Override
132136
protected ValueFactory<
133137
Object,

module/jsonurl-jsr374/src/test/java/org/jsonurl/jsonp/AbstractJsonpParseTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ protected boolean getBoolean(String key, JsonObject value) {
115115
@Override
116116
protected boolean getNull(String key, JsonObject value) {
117117
Object ret = value.get(key);
118-
return factory.getNull() == ret;
118+
return factory.isNull(ret);
119119
}
120120

121121
@Override
@@ -138,6 +138,11 @@ protected String getStringValue(JsonValue value) {
138138
return value instanceof JsonString ? ((JsonString)value).getString() : null;
139139
}
140140

141+
@Override
142+
protected int getSize(JsonArray value) {
143+
return factory.isNull(value) ? 0 : value.size();
144+
}
145+
141146
@Override
142147
protected ValueFactory<
143148
JsonValue,

0 commit comments

Comments
 (0)