Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[json-core] Add JsonExtract helper to ease extracting values and number types from raw Map<String,Object> #325

Merged
merged 2 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions json-core/src/main/java/io/avaje/json/simple/DExtract.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> map;

DExtract(Map<String, Object> map) {
this.map = map;
}

@SuppressWarnings("unchecked")
private Object find(String path, Map<String, Object> 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<String, Object>) 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<String> 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;
}
}
96 changes: 96 additions & 0 deletions json-core/src/main/java/io/avaje/json/simple/JsonExtract.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* The <em>path</em> can be simple like {@code "name"} or a nested path using
* dot notation like {@code "address.city"}.
* <p>
* For extracting numbers there are methods for int, long and double that will
* return the intValue(), longValue() and doubleValue() respectively.
* <p>
* <pre>{@code
*
* String json = "{\"name\":\"Rob\",\"score\":4.5,\"whenActive\":\"2025-10-20\",\"address\":{\"street\":\"Pall Mall\"}}";
* Map<String, Object> 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();
*
* }</pre>
*
*/
public interface JsonExtract {

/**
* Return a JsonExtract for the given Map of values.
*/
static JsonExtract of(Map<String, Object> 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.
*
* <pre>{@code
*
* LocalDate activeDate = jsonExtract.extractOrEmpty("whenActive")
* .map(LocalDate::parse)
* .orElseThrow();
*
* }</pre>
*/
Optional<String> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ static Builder builder() {
*/
<T> Type<T> type(JsonAdapter<T> customAdapter);

default JsonExtract extract(Map<String, Object> map) {
return new DExtract(map);
}

/**
* Build the JsonNodeMapper.
*/
Expand Down
116 changes: 116 additions & 0 deletions json-core/src/test/java/io/avaje/json/simple/SimpleMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -132,4 +134,118 @@ void arrayToJsonFromJson() {
List<Object> 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<String, Object> 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<String, Object> 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<String, Object> mapFromJson = simpleMapper.fromJsonObject(json);

JsonExtract extract = JsonExtract.of(mapFromJson);
assertThat(extract.extract("one", 0)).isEqualTo(1);
}

@Test
void extract_whenMissing() {
String json = "{}";
Map<String, Object> 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<String, Object> 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<String, Object> 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"));
}
}
Loading