Skip to content

Commit 6cd97dd

Browse files
committed
feat(jsoniter): add collection and map parsing methods
- Add `readList` and `readCollection` for flexible list/collection deserialization. - Introduce `readMap` variants for parsing key-value pairs from arrays and objects. - Enhance test coverage with diverse scenarios, including null/empty cases, nested structures, and duplicate key handling.
1 parent 7c68b70 commit 6cd97dd

4 files changed

Lines changed: 396 additions & 11 deletions

File tree

json-iterator/src/main/java/systems/comodal/jsoniter/JsonIterator.java

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
import java.math.BigDecimal;
77
import java.math.BigInteger;
88
import java.time.Instant;
9+
import java.util.ArrayList;
10+
import java.util.Collection;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.function.BiFunction;
15+
import java.util.function.Function;
916

1017
public interface JsonIterator {
1118

@@ -203,6 +210,80 @@ default boolean supportsMarkReset() {
203210

204211
boolean readArray();
205212

213+
/// Reads each array element with `parser` and adds it to `collection`.
214+
/// A JSON `null` value reads as an empty array, consistent with
215+
/// [#readArray()]; callers that must distinguish `null` should guard with
216+
/// [#notNull()]:
217+
///
218+
/// ```java
219+
/// this.routePlan = ji.notNull() ? ji.readList(JupiterRoute::parse) : null;
220+
/// ```
221+
default <T, C extends Collection<? super T>> C readCollection(final C collection,
222+
final Function<JsonIterator, T> parser) {
223+
while (readArray()) {
224+
collection.add(parser.apply(this));
225+
}
226+
return collection;
227+
}
228+
229+
/// Reads an array into a [List], one element per `parser` application, e.g.
230+
/// `ji.readList(JsonIterator::readString)`. A JSON `null` value reads as an
231+
/// empty list; see [#readCollection(Collection, Function)].
232+
default <T> List<T> readList(final Function<JsonIterator, T> parser) {
233+
return readCollection(new ArrayList<>(), parser);
234+
}
235+
236+
/// Reads each object field as a map entry: the key is parsed from the field
237+
/// name span and passed to `valueParser` along with this iterator, so value
238+
/// factories that carry their key map directly, e.g.
239+
/// `ji.readMap(PARSE_BASE58_PUBLIC_KEY, JupiterPrice::parsePrice)`. For
240+
/// String keys, `String::new` is a [CharBufferFunction].
241+
///
242+
/// A JSON `null` value reads as an empty map, consistent with [#readArray()]
243+
/// and [#testObject(Object, ContextFieldBufferPredicate)]; callers that must
244+
/// distinguish `null` should guard with [#notNull()]. Duplicate field names
245+
/// follow [Map#put] semantics: the last entry wins.
246+
default <K, V, M extends Map<? super K, ? super V>> M readMap(final M map,
247+
final CharBufferFunction<K> keyParser,
248+
final BiFunction<K, JsonIterator, V> valueParser) {
249+
return testObject(map, (m, buf, offset, len, ji) -> {
250+
final var key = keyParser.apply(buf, offset, len);
251+
m.put(key, valueParser.apply(key, ji));
252+
return true;
253+
});
254+
}
255+
256+
/// [HashMap] convenience over
257+
/// [#readMap(Map, CharBufferFunction, BiFunction)].
258+
default <K, V> Map<K, V> readMap(final CharBufferFunction<K> keyParser,
259+
final BiFunction<K, JsonIterator, V> valueParser) {
260+
return readMap(new HashMap<>(), keyParser, valueParser);
261+
}
262+
263+
/// Reads an array of values into a map, each keyed by `keyExtractor`
264+
/// applied to the parsed value, e.g.
265+
/// `ji.readMap(JupiterTokenV2::parseToken, JupiterTokenV2::address)`.
266+
///
267+
/// A JSON `null` value reads as an empty map, consistent with
268+
/// [#readArray()]; callers that must distinguish `null` should guard with
269+
/// [#notNull()]. Duplicate keys follow [Map#put] semantics: the last
270+
/// element wins.
271+
default <K, V, M extends Map<? super K, ? super V>> M readMap(final M map,
272+
final Function<JsonIterator, V> valueParser,
273+
final Function<V, K> keyExtractor) {
274+
while (readArray()) {
275+
final var value = valueParser.apply(this);
276+
map.put(keyExtractor.apply(value), value);
277+
}
278+
return map;
279+
}
280+
281+
/// [HashMap] convenience over [#readMap(Map, Function, Function)].
282+
default <K, V> Map<K, V> readMap(final Function<JsonIterator, V> valueParser,
283+
final Function<V, K> keyExtractor) {
284+
return readMap(new HashMap<>(), valueParser, keyExtractor);
285+
}
286+
206287
JsonIterator openArray();
207288

208289
JsonIterator continueArray();
@@ -356,7 +437,6 @@ default BigDecimal readBigDecimalStripTrailingZeroes() {
356437

357438
<C> void consumeChars(final C context, final ContextCharBufferConsumer<C> testChars);
358439

359-
360440
// IOC Field Methods
361441

362442
/// @deprecated single-field probes are covered by

json-iterator/src/test/java/systems/comodal/jsoniter/TestArray.java

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
import org.junit.jupiter.params.provider.FieldSource;
66
import systems.comodal.jsoniter.factories.JsonIteratorFactory;
77

8+
import java.util.HashSet;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.TreeMap;
12+
813
import static org.junit.jupiter.api.Assertions.*;
914

1015
@ParameterizedClass
@@ -117,4 +122,144 @@ void test_five_elements() {
117122
assertEquals(5, ji.readInt());
118123
assertFalse(ji.readArray());
119124
}
125+
126+
private record Point(int x, int y) {
127+
128+
static Point parse(final JsonIterator ji) {
129+
final int[] xy = new int[2];
130+
ji.testObject(xy, (context, buf, offset, len, jsonIterator) -> {
131+
if (JsonIterator.fieldEquals("x", buf, offset, len)) {
132+
context[0] = jsonIterator.readInt();
133+
} else if (JsonIterator.fieldEquals("y", buf, offset, len)) {
134+
context[1] = jsonIterator.readInt();
135+
} else {
136+
jsonIterator.skip();
137+
}
138+
return true;
139+
});
140+
return new Point(xy[0], xy[1]);
141+
}
142+
}
143+
144+
@Test
145+
void test_read_list() {
146+
var ji = factory.create(" [ 1 , 2, 3 ] ");
147+
assertEquals(List.of(1, 2, 3), ji.readList(JsonIterator::readInt));
148+
149+
ji = factory.create("""
150+
["a","b","c"]""");
151+
assertEquals(List.of("a", "b", "c"), ji.readList(JsonIterator::readString));
152+
}
153+
154+
@Test
155+
void test_read_list_empty() {
156+
final var ji = factory.create("[]");
157+
final var list = ji.readList(JsonIterator::readInt);
158+
assertTrue(list.isEmpty());
159+
}
160+
161+
@Test
162+
void test_read_list_null() {
163+
final var ji = factory.create("null");
164+
final var list = ji.readList(JsonIterator::readInt);
165+
assertTrue(list.isEmpty());
166+
}
167+
168+
@Test
169+
void test_read_list_null_guard() {
170+
var ji = factory.create("null");
171+
assertNull(ji.notNull() ? ji.readList(JsonIterator::readInt) : null);
172+
173+
ji = factory.create("[7]");
174+
assertEquals(List.of(7), ji.notNull() ? ji.readList(JsonIterator::readInt) : null);
175+
}
176+
177+
@Test
178+
void test_read_list_of_objects() {
179+
final var ji = factory.create("""
180+
[{"x":1,"y":2},{"y":4,"x":3},{}]""");
181+
assertEquals(
182+
List.of(new Point(1, 2), new Point(3, 4), new Point(0, 0)),
183+
ji.readList(Point::parse)
184+
);
185+
}
186+
187+
@Test
188+
void test_read_list_nested() {
189+
final var ji = factory.create("[[1,2],[],[3]]");
190+
assertEquals(
191+
List.of(List.of(1, 2), List.of(), List.of(3)),
192+
ji.readList(elementJi -> elementJi.readList(JsonIterator::readInt))
193+
);
194+
}
195+
196+
@Test
197+
void test_read_collection() {
198+
final var ji = factory.create("""
199+
["a","b","a"]""");
200+
final var set = ji.readCollection(new HashSet<String>(), JsonIterator::readString);
201+
assertEquals(new HashSet<>(List.of("a", "b")), set);
202+
}
203+
204+
@Test
205+
void test_read_list_as_field_value() {
206+
final var ji = factory.create("""
207+
{"ints":[1,2],"after":3}""");
208+
assertEquals(List.of(1, 2), ji.skipUntil("ints").readList(JsonIterator::readInt));
209+
assertEquals(3, ji.skipUntil("after").readInt());
210+
}
211+
212+
@Test
213+
void test_read_map_indexed_by_value_field() {
214+
final var ji = factory.create("""
215+
[{"x":1,"y":2},{"x":3,"y":4}]""");
216+
assertEquals(
217+
Map.of(1, new Point(1, 2), 3, new Point(3, 4)),
218+
ji.readMap(Point::parse, Point::x)
219+
);
220+
}
221+
222+
@Test
223+
void test_read_map_indexed_empty() {
224+
final var ji = factory.create("[]");
225+
assertTrue(ji.readMap(Point::parse, Point::x).isEmpty());
226+
}
227+
228+
@Test
229+
void test_read_map_indexed_null() {
230+
var ji = factory.create("null");
231+
assertTrue(ji.readMap(Point::parse, Point::x).isEmpty());
232+
233+
ji = factory.create("null");
234+
assertNull(ji.notNull() ? ji.readMap(Point::parse, Point::x) : null);
235+
}
236+
237+
@Test
238+
void test_read_map_indexed_duplicate_key_last_wins() {
239+
final var ji = factory.create("""
240+
[{"x":1,"y":2},{"x":1,"y":9}]""");
241+
assertEquals(
242+
Map.of(1, new Point(1, 9)),
243+
ji.readMap(Point::parse, Point::x)
244+
);
245+
}
246+
247+
@Test
248+
void test_read_map_indexed_supplied_map() {
249+
final var ji = factory.create("""
250+
[{"x":3,"y":4},{"x":1,"y":2}]""");
251+
final var map = ji.readMap(new TreeMap<Integer, Point>(), Point::parse, Point::x);
252+
assertEquals(List.of(1, 3), List.copyOf(map.keySet()));
253+
}
254+
255+
@Test
256+
void test_read_map_indexed_as_field_value() {
257+
final var ji = factory.create("""
258+
{"points":[{"x":1,"y":2}],"after":3}""");
259+
assertEquals(
260+
Map.of(1, new Point(1, 2)),
261+
ji.skipUntil("points").readMap(Point::parse, Point::x)
262+
);
263+
assertEquals(3, ji.skipUntil("after").readInt());
264+
}
120265
}

json-iterator/src/test/java/systems/comodal/jsoniter/TestInteger.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import static java.util.concurrent.TimeUnit.MICROSECONDS;
1212
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertFalse;
1314
import static org.junit.jupiter.api.Assertions.assertThrows;
1415

1516
@ParameterizedClass
@@ -73,6 +74,65 @@ void test_positive_negative_long() {
7374
assertEquals(-9223372036854775808L, factory.create("\"-9223372036854775808\"").readLong());
7475
}
7576

77+
@Test
78+
void test_positive_negative_short() {
79+
assertEquals((short) 0, factory.create("0").readShort());
80+
assertEquals((short) 1, factory.create("1").readShort());
81+
assertEquals((short) 321, factory.create("321").readShort());
82+
assertEquals((short) 4321, factory.create("4321").readShort());
83+
assertEquals((short) -4321, factory.create("-4321").readShort());
84+
85+
assertEquals((short) 0, factory.create("\"0\"").readShort());
86+
assertEquals((short) 1, factory.create("\"1\"").readShort());
87+
assertEquals((short) 321, factory.create("\"321\"").readShort());
88+
assertEquals((short) 4321, factory.create("\"4321\"").readShort());
89+
assertEquals((short) -4321, factory.create("\"-4321\"").readShort());
90+
}
91+
92+
@Test
93+
void test_max_min_short() {
94+
assertEquals(Short.MAX_VALUE, factory.create(Short.toString(Short.MAX_VALUE)).readShort());
95+
assertEquals((short) (Short.MAX_VALUE - 1), factory.create(Short.toString((short) (Short.MAX_VALUE - 1))).readShort());
96+
assertEquals((short) (Short.MIN_VALUE + 1), factory.create(Short.toString((short) (Short.MIN_VALUE + 1))).readShort());
97+
assertEquals(Short.MIN_VALUE, factory.create(Short.toString(Short.MIN_VALUE)).readShort());
98+
99+
assertEquals(Short.MAX_VALUE, factory.create(String.format("\"%d\"", Short.MAX_VALUE)).readShort());
100+
assertEquals((short) (Short.MAX_VALUE - 1), factory.create(String.format("\"%d\"", Short.MAX_VALUE - 1)).readShort());
101+
assertEquals((short) (Short.MIN_VALUE + 1), factory.create(String.format("\"%d\"", Short.MIN_VALUE + 1)).readShort());
102+
assertEquals(Short.MIN_VALUE, factory.create(String.format("\"%d\"", Short.MIN_VALUE)).readShort());
103+
}
104+
105+
@Test
106+
void test_short_overflow() {
107+
var ji = factory.create("32768");
108+
assertThrows(JsonException.class, ji::readShort);
109+
110+
ji = factory.create("-32769");
111+
assertThrows(JsonException.class, ji::readShort);
112+
113+
ji = factory.create("2147483647");
114+
assertThrows(JsonException.class, ji::readShort);
115+
116+
ji = factory.create("-2147483648");
117+
assertThrows(JsonException.class, ji::readShort);
118+
119+
ji = factory.create("\"32768\"");
120+
assertThrows(JsonException.class, ji::readShort);
121+
122+
ji = factory.create("\"-32769\"");
123+
assertThrows(JsonException.class, ji::readShort);
124+
}
125+
126+
@Test
127+
void test_max_min_short_array() {
128+
final var ji = factory.create("[32767,-32768]");
129+
ji.readArray();
130+
assertEquals(Short.MAX_VALUE, ji.readShort());
131+
ji.readArray();
132+
assertEquals(Short.MIN_VALUE, ji.readShort());
133+
assertFalse(ji.readArray());
134+
}
135+
76136
@Test
77137
void test_max_min_int() {
78138
assertEquals(Integer.MAX_VALUE, factory.create(Integer.toString(Integer.MAX_VALUE)).readInt());

0 commit comments

Comments
 (0)