Skip to content

Commit 212f113

Browse files
authored
[json-node] Add extract() methods to assist filtering and mapping (#300)
1 parent bfa49e1 commit 212f113

File tree

8 files changed

+342
-22
lines changed

8 files changed

+342
-22
lines changed

json-node/.editorconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
end_of_line = lf
8+
indent_size = 2
9+
indent_style = space
10+
insert_final_newline = true
11+
trim_trailing_whitespace = true
12+
spaces_around_operators = true

json-node/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313

1414
<dependencies>
1515

16+
<dependency>
17+
<groupId>org.jspecify</groupId>
18+
<artifactId>jspecify</artifactId>
19+
<version>1.0.0</version>
20+
</dependency>
21+
1622
<dependency>
1723
<groupId>io.avaje</groupId>
1824
<artifactId>avaje-json</artifactId>

json-node/src/main/java/io/avaje/json/node/JsonArray.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.ArrayList;
44
import java.util.Collections;
55
import java.util.List;
6+
import java.util.stream.Stream;
67

78
import static java.util.Objects.requireNonNull;
89

@@ -67,6 +68,13 @@ public List<JsonNode> elements() {
6768
return children;
6869
}
6970

71+
/**
72+
* Return the stream of child elements.
73+
*/
74+
public Stream<JsonNode> stream() {
75+
return children.stream();
76+
}
77+
7078
/**
7179
* Add an element to the json array.
7280
*/
Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,120 @@
11
package io.avaje.json.node;
22

3+
import org.jspecify.annotations.Nullable;
4+
5+
/**
6+
* Represents the code JSON types.
7+
*/
38
public interface JsonNode {
49

10+
/**
11+
* The types for JsonNode.
12+
*/
13+
enum Type {
14+
ARRAY(true, false),
15+
OBJECT(false, true),
16+
NULL(),
17+
BOOLEAN(),
18+
STRING(),
19+
NUMBER(true),
20+
;
21+
// BINARY,
22+
// MISSING,
23+
// POJO
24+
private final boolean value;
25+
private final boolean numberType;
26+
private final boolean arrayType;
27+
private final boolean objectType;
28+
29+
Type() {
30+
this(true, false, false, false);
31+
}
32+
33+
Type(boolean numberType) {
34+
this(true, numberType, false, false);
35+
}
36+
37+
Type(boolean arrayType, boolean objectType) {
38+
this(false, false, arrayType, objectType);
39+
}
40+
41+
Type(boolean value, boolean numberType, boolean arrayType, boolean objectType) {
42+
this.value = value;
43+
this.numberType = numberType;
44+
this.arrayType = arrayType;
45+
this.objectType = objectType;
46+
}
47+
48+
public boolean isValue() {
49+
return value;
50+
}
51+
52+
public boolean isNumber() {
53+
return numberType;
54+
}
55+
56+
public boolean isArray() {
57+
return arrayType;
58+
}
59+
60+
public boolean isObject() {
61+
return objectType;
62+
}
63+
}
64+
565
/**
666
* Return the type of the node.
767
*/
868
Type type();
969

70+
/**
71+
* Return a text representation of the node.
72+
*/
1073
String text();
1174

1275
/**
13-
* The types for JsonNode.
76+
* Find a node given a path using dot notation.
77+
* @param path The path in dot notation
78+
* @return The found node or null
1479
*/
15-
enum Type {
16-
NULL,
17-
ARRAY,
18-
OBJECT,
19-
BOOLEAN,
20-
STRING,
21-
NUMBER,
22-
// BINARY,
23-
// MISSING,
24-
// POJO
80+
@Nullable
81+
default JsonNode find(String path) {
82+
throw new UnsupportedOperationException();
83+
}
84+
85+
/**
86+
* Extract the text from the node at the given path.
87+
*/
88+
@Nullable
89+
default String extract(String path) {
90+
throw new UnsupportedOperationException();
91+
}
92+
93+
/**
94+
* Extract the text from the given path if present or the given default value.
95+
*/
96+
default String extract(String path, String defaultValue) {
97+
throw new UnsupportedOperationException();
98+
}
99+
100+
/**
101+
* Extract the long from the given path if present or the given default value.
102+
*/
103+
default long extract(String path, long defaultValue) {
104+
throw new UnsupportedOperationException();
105+
}
106+
107+
/**
108+
* Extract the int from the given path if present or the given default value.
109+
*/
110+
default Number extract(String path, Number defaultValue) {
111+
throw new UnsupportedOperationException();
112+
}
113+
114+
/**
115+
* Extract the boolean from the given path if present or the given default value.
116+
*/
117+
default boolean extract(String path, boolean defaultValue) {
118+
throw new UnsupportedOperationException();
25119
}
26120
}

json-node/src/main/java/io/avaje/json/node/JsonObject.java

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.avaje.json.node;
22

3+
import org.jspecify.annotations.Nullable;
4+
35
import java.util.Collections;
46
import java.util.LinkedHashMap;
57
import java.util.Map;
6-
import java.util.Optional;
8+
import java.util.regex.Pattern;
79

810
import static java.util.Objects.requireNonNull;
911

@@ -12,6 +14,8 @@
1214
*/
1315
public final class JsonObject implements JsonNode {
1416

17+
private static final Pattern PATH_PATTERN = Pattern.compile("\\.");
18+
1519
private final Map<String, JsonNode> children;
1620

1721
/**
@@ -87,8 +91,98 @@ public JsonObject add(String key, JsonNode value) {
8791
return this;
8892
}
8993

90-
public Optional<JsonNode> get(String key) {
91-
return Optional.ofNullable(children.get(key));
94+
/**
95+
* Add a String value.
96+
*/
97+
public JsonObject add(String key, String value) {
98+
return add(key, JsonString.of(value));
99+
}
100+
101+
/**
102+
* Add a boolean value.
103+
*/
104+
public JsonObject add(String key, boolean value) {
105+
return add(key, JsonBoolean.of(value));
106+
}
107+
108+
/**
109+
* Add a int value.
110+
*/
111+
public JsonObject add(String key, int value) {
112+
return add(key, JsonInteger.of(value));
113+
}
114+
115+
/**
116+
* Add a long value.
117+
*/
118+
public JsonObject add(String key, long value) {
119+
return add(key, JsonLong.of(value));
120+
}
121+
122+
/**
123+
* Return the direct element at the given path throwing IllegalArgumentException
124+
* if it is missing.
125+
*
126+
* @param key The child key that must exist.
127+
* @return The child node.
128+
*/
129+
public JsonNode get(String key) {
130+
final var node = children.get(key);
131+
if (node == null) {
132+
throw new IllegalArgumentException("Node not present for " + key);
133+
}
134+
return node;
135+
}
136+
137+
@Nullable
138+
@Override
139+
public JsonNode find(String path) {
140+
final String[] paths = PATH_PATTERN.split(path, 2);
141+
final JsonNode child = children.get(paths[0]);
142+
if (child == null || paths.length == 1) {
143+
return child;
144+
}
145+
if (child instanceof JsonObject) {
146+
JsonObject co = (JsonObject) child;
147+
return co.find(paths[1]);
148+
}
149+
return null;
150+
}
151+
152+
@Nullable
153+
@Override
154+
public String extract(String path) {
155+
final var node = find(path);
156+
return node == null ? null : node.text();
92157
}
93158

159+
@Override
160+
public String extract(String path, String defaultValue) {
161+
final var name = find(path);
162+
return name == null ? defaultValue : name.text();
163+
}
164+
165+
@Override
166+
public long extract(String path, long defaultValue) {
167+
final var node = find(path);
168+
return !(node instanceof JsonNumber)
169+
? defaultValue
170+
: ((JsonNumber) node).longValue();
171+
}
172+
173+
@Override
174+
public Number extract(String path, Number defaultValue) {
175+
final var node = find(path);
176+
return !(node instanceof JsonNumber)
177+
? defaultValue
178+
: ((JsonNumber) node).numberValue();
179+
}
180+
181+
@Override
182+
public boolean extract(String path, boolean defaultValue) {
183+
final var node = find(path);
184+
return !(node instanceof JsonBoolean)
185+
? defaultValue
186+
: ((JsonBoolean) node).value();
187+
}
94188
}

json-node/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
exports io.avaje.json.node;
44

5+
requires transitive org.jspecify;
56
requires transitive io.avaje.json;
67
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.avaje.json.node;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class JsonArrayTest {
11+
12+
String input =
13+
"{\n" +
14+
" \"firstName\": \"John\",\n" +
15+
" \"lastName\": \"doe\",\n" +
16+
" \"age\": 26,\n" +
17+
" \"address\": {\n" +
18+
" \"streetAddress\": \"naist street\",\n" +
19+
" \"city\": \"Nara\",\n" +
20+
" \"postalCode\": \"630-0192\"\n" +
21+
" },\n" +
22+
" \"phoneNumbers\": [\n" +
23+
" {\n" +
24+
" \"type\": \"iPhone\",\n" +
25+
" \"number\": \"0123-4567-8888\"\n" +
26+
" },\n" +
27+
" {\n" +
28+
" \"type\": \"home\",\n" +
29+
" \"number\": \"0123-4567-8910\"\n" +
30+
" },\n" +
31+
" {\n" +
32+
" \"type\": \"home\",\n" +
33+
" \"number\": \"563-4567-8910\"\n" +
34+
" }\n" +
35+
" ]\n" +
36+
"}";
37+
38+
static final JsonNodeAdapter node = JsonNodeAdapter.builder().build();
39+
40+
static final JsonArray basicArray = JsonArray.create()
41+
.add(JsonInteger.of(42))
42+
.add(JsonString.of("foo"));
43+
44+
@Test
45+
void streamFilter() {
46+
47+
JsonObject top = node.fromJson(JsonObject.class, input);
48+
JsonArray phoneNumbers = (JsonArray)top.get("phoneNumbers");
49+
50+
List<String> result =
51+
phoneNumbers.stream()
52+
.filter(n -> "home".equals(n.extract("type")))
53+
.map(n -> n.extract("number"))
54+
.collect(Collectors.toList());
55+
56+
assertThat(result).hasSize(2);
57+
}
58+
59+
@Test
60+
void type() {
61+
assertThat(basicArray.type()).isEqualTo(JsonNode.Type.ARRAY);
62+
assertThat(basicArray.type().isArray()).isTrue();
63+
assertThat(basicArray.type().isObject()).isFalse();
64+
assertThat(basicArray.type().isValue()).isFalse();
65+
}
66+
67+
@Test
68+
void text() {
69+
assertThat(JsonArray.create().text()).isEqualTo("[]");
70+
assertThat(basicArray.text()).isEqualTo("[42, foo]");
71+
}
72+
}

0 commit comments

Comments
 (0)