diff --git a/json-core/src/main/java/io/avaje/json/simple/DExtract.java b/json-core/src/main/java/io/avaje/json/simple/DExtract.java new file mode 100644 index 00000000..af96a9b0 --- /dev/null +++ b/json-core/src/main/java/io/avaje/json/simple/DExtract.java @@ -0,0 +1,82 @@ +package io.avaje.json.simple; + +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +final class DExtract implements JsonExtract { + + private static final Pattern PATH_PATTERN = Pattern.compile("\\."); + + private final Map map; + + DExtract(Map map) { + this.map = map; + } + + @SuppressWarnings("unchecked") + private Object find(String path, Map map) { + final String[] paths = PATH_PATTERN.split(path, 2); + final Object child = map.get(paths[0]); + if (child == null || paths.length == 1) { + return child; + } + if (child instanceof Map) { + return find(paths[1], (Map) child); + } + return null; + } + + @Override + public String extract(String path) { + final var node = find(path, map); + if (node == null) { + throw new IllegalArgumentException("Node not present for " + path); + } + return node.toString(); + } + + @Override + public Optional extractOrEmpty(String path) { + final var name = find(path, map); + return name == null ? Optional.empty() : Optional.of(name.toString()); + } + + @Override + public String extract(String path, String missingValue) { + final var name = find(path, map); + return name == null ? missingValue : name.toString(); + } + + @Override + public int extract(String path, int missingValue) { + final var node = find(path, map); + return !(node instanceof Number) + ? missingValue + : ((Number) node).intValue(); + } + + @Override + public long extract(String path, long missingValue) { + final var node = find(path, map); + return !(node instanceof Number) + ? missingValue + : ((Number) node).longValue(); + } + + @Override + public double extract(String path, double missingValue) { + final var node = find(path, map); + return !(node instanceof Number) + ? missingValue + : ((Number) node).doubleValue(); + } + + @Override + public boolean extract(String path, boolean missingValue) { + final var node = find(path, map); + return !(node instanceof Boolean) + ? missingValue + : (Boolean) node; + } +} diff --git a/json-core/src/main/java/io/avaje/json/simple/JsonExtract.java b/json-core/src/main/java/io/avaje/json/simple/JsonExtract.java new file mode 100644 index 00000000..d55126a2 --- /dev/null +++ b/json-core/src/main/java/io/avaje/json/simple/JsonExtract.java @@ -0,0 +1,96 @@ +package io.avaje.json.simple; + +import java.util.Map; +import java.util.Optional; + +/** + * A helper to extract values from a Map. + *

+ * The path can be simple like {@code "name"} or a nested path using + * dot notation like {@code "address.city"}. + *

+ * For extracting numbers there are methods for int, long and double that will + * return the intValue(), longValue() and doubleValue() respectively. + *

+ *

{@code
+ *
+ *   String json = "{\"name\":\"Rob\",\"score\":4.5,\"whenActive\":\"2025-10-20\",\"address\":{\"street\":\"Pall Mall\"}}";
+ *   Map mapFromJson = simpleMapper.fromJsonObject(json);
+ *
+ *   JsonExtract jsonExtract = simpleMapper.extract(mapFromJson);
+ *
+ *   String name = jsonExtract.extract("name");
+ *   double score = jsonExtract.extract("score", -1D);
+ *   String street = jsonExtract.extract("address.street");
+ *
+ *   LocalDate activeDate = jsonExtract.extractOrEmpty("whenActive")
+ *     .map(LocalDate::parse)
+ *     .orElseThrow();
+ *
+ * }
+ * + */ +public interface JsonExtract { + + /** + * Return a JsonExtract for the given Map of values. + */ + static JsonExtract of(Map map) { + return new DExtract(map); + } + + /** + * Extract the text from the node at the given path. + * + * @throws IllegalArgumentException When the given path is missing. + */ + String extract(String path); + + /** + * Extract the text value from the given path if present else empty. + * + *
{@code
+   *
+   *   LocalDate activeDate = jsonExtract.extractOrEmpty("whenActive")
+   *     .map(LocalDate::parse)
+   *     .orElseThrow();
+   *
+   * }
+ */ + Optional extractOrEmpty(String path); + + /** + * Extract the text value from the given path if present or the given default value. + * + * @param missingValue The value to use when the path is missing. + */ + String extract(String path, String missingValue); + + /** + * Extract the int from the given path if present or the given default value. + * + * @param missingValue The value to use when the path is missing. + */ + int extract(String path, int missingValue); + + /** + * Extract the long from the given path if present or the given default value. + * + * @param missingValue The value to use when the path is missing. + */ + long extract(String path, long missingValue); + + /** + * Extract the double from the given path if present or the given default value. + * + * @param missingValue The value to use when the path is missing. + */ + double extract(String path, double missingValue); + + /** + * Extract the boolean from the given path if present or the given default value. + * + * @param missingValue The value to use when the path is missing. + */ + boolean extract(String path, boolean missingValue); +} diff --git a/json-core/src/main/java/io/avaje/json/simple/SimpleMapper.java b/json-core/src/main/java/io/avaje/json/simple/SimpleMapper.java index ed0246cb..d3a3629f 100644 --- a/json-core/src/main/java/io/avaje/json/simple/SimpleMapper.java +++ b/json-core/src/main/java/io/avaje/json/simple/SimpleMapper.java @@ -136,6 +136,10 @@ static Builder builder() { */ Type type(JsonAdapter customAdapter); + default JsonExtract extract(Map map) { + return new DExtract(map); + } + /** * Build the JsonNodeMapper. */ diff --git a/json-core/src/test/java/io/avaje/json/simple/SimpleMapperTest.java b/json-core/src/test/java/io/avaje/json/simple/SimpleMapperTest.java index cb2ba8eb..606dc543 100644 --- a/json-core/src/test/java/io/avaje/json/simple/SimpleMapperTest.java +++ b/json-core/src/test/java/io/avaje/json/simple/SimpleMapperTest.java @@ -6,11 +6,13 @@ import io.avaje.json.stream.JsonStream; import org.junit.jupiter.api.Test; +import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class SimpleMapperTest { @@ -132,4 +134,118 @@ void arrayToJsonFromJson() { List list2 = simpleMapper.list().fromJson(asJson); assertThat(list2).isEqualTo(listFromJson); } + + @Test + void extract_example() { + String json = "{\"name\":\"Rob\",\"score\":4.5,\"whenActive\":\"2025-10-20\",\"address\":{\"street\":\"Pall Mall\"}}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = simpleMapper.extract(mapFromJson); + + String name = extract.extract("name"); + double score = extract.extract("score", -1D); + String street = extract.extract("address.street"); + LocalDate activeDate = extract.extractOrEmpty("whenActive") + .map(LocalDate::parse) + .orElseThrow(); + + assertThat(name).isEqualTo("Rob"); + assertThat(score).isEqualTo(4.5D); + assertThat(street).isEqualTo("Pall Mall"); + assertThat(activeDate).isEqualTo(LocalDate.parse("2025-10-20")); + } + + @Test + void extract() { + String json = "{\"one\":1,\"two\":4.5,\"three\":3,\"four\":\"2025-10-20\",\"five\":true}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = simpleMapper.extract(mapFromJson); + assertThat(extract.extract("one", 0)).isEqualTo(1); + assertThat(extract.extract("two", 0D)).isEqualTo(4.5D); + assertThat(extract.extract("three", 0L)).isEqualTo(3L); + assertThat(extract.extract("four")).isEqualTo("2025-10-20"); + assertThat(extract.extract("four", "NA")).isEqualTo("2025-10-20"); + assertThat(extract.extract("five", false)).isTrue(); + + LocalDate fourAsLocalDate = extract.extractOrEmpty("four") + .map(LocalDate::parse) + .orElseThrow(); + + assertThat(fourAsLocalDate) + .isEqualTo(LocalDate.parse("2025-10-20")); + + } + + @Test + void JsonExtractOf() { + String json = "{\"one\":1}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = JsonExtract.of(mapFromJson); + assertThat(extract.extract("one", 0)).isEqualTo(1); + } + + @Test + void extract_whenMissing() { + String json = "{}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = simpleMapper.extract(mapFromJson); + assertThat(extract.extract("one", 0)).isEqualTo(0); + assertThat(extract.extract("two", 0D)).isEqualTo(0D); + assertThat(extract.extract("three", 0L)).isEqualTo(0L); + assertThat(extract.extract("four", "NA")).isEqualTo("NA"); + assertThat(extract.extract("five", false)).isFalse(); + + assertThatThrownBy(() -> extract.extract("four")) + .isInstanceOf(IllegalArgumentException.class); + + LocalDate fourAsLocalDate = extract.extractOrEmpty("four") + .map(LocalDate::parse) + .orElse(LocalDate.of(1970, 1, 21)); + + assertThat(fourAsLocalDate).isEqualTo(LocalDate.parse("1970-01-21")); + } + + @Test + void extractNumber_whenNotANumber_expect_missingValue() { + String json = "{\"text\":\"foo\",\"bool\":true,\"isNull\":null}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = simpleMapper.extract(mapFromJson); + assertThat(extract.extract("text", 7)).isEqualTo(7); + assertThat(extract.extract("text", 7L)).isEqualTo(7L); + assertThat(extract.extract("text", 7.4D)).isEqualTo(7.4D); + assertThat(extract.extract("bool", 7)).isEqualTo(7); + assertThat(extract.extract("bool", 7L)).isEqualTo(7L); + assertThat(extract.extract("bool", 7.4D)).isEqualTo(7.4D); + assertThat(extract.extract("isNull", 7)).isEqualTo(7); + assertThat(extract.extract("isNull", 7L)).isEqualTo(7L); + assertThat(extract.extract("isNull", 7.4D)).isEqualTo(7.4D); + } + + @Test + void extract_nestedPath() { + String json = "{\"outer\":{\"a\":\"v0\", \"b\":1, \"c\":true,\"d\":{\"x\":\"x0\",\"y\":42,\"date\":\"2025-10-20\"}}}"; + Map mapFromJson = simpleMapper.fromJsonObject(json); + + JsonExtract extract = simpleMapper.extract(mapFromJson); + assertThat(extract.extract("outer.b", 0)).isEqualTo(1); + assertThat(extract.extract("outer.d.y", 0)).isEqualTo(42); + assertThat(extract.extract("outer.d.y", "junk")).isEqualTo("42"); + assertThat(extract.extract("outer.a", "NA")).isEqualTo("v0"); + + assertThat(extract.extract("outer.d.y", 0L)).isEqualTo(42L); + assertThat(extract.extract("outer.d.y", 0D)).isEqualTo(42D); + assertThat(extract.extract("outer.c", false)).isTrue(); + + assertThat(extract.extract("outer.c")).isEqualTo("true"); + + LocalDate fourAsLocalDate = extract.extractOrEmpty("outer.d.date") + .map(LocalDate::parse) + .orElse(LocalDate.of(1970, 1, 21)); + + assertThat(fourAsLocalDate).isEqualTo(LocalDate.parse("2025-10-20")); + } }