diff --git a/src/main/java/org/hisp/dhis/jsontree/Collapsed.java b/src/main/java/org/hisp/dhis/jsontree/Collapsed.java
new file mode 100644
index 0000000..5f163b7
--- /dev/null
+++ b/src/main/java/org/hisp/dhis/jsontree/Collapsed.java
@@ -0,0 +1,32 @@
+package org.hisp.dhis.jsontree;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation for {@link java.lang.reflect.RecordComponent}s that are themselves {@link
+ * Record}s whose components should be treated as if they were directly defined in the parent record
+ * type when mapping JSON to Java using {@link JsonValue#to(Class)}.
+ *
+ *
Collapsing can e.g. be used to access a "flat" JSON object with many properties as a Java
+ * record which uses inner records to group those properties without the need to mirror that
+ * grouping in JSON.
+ *
+ *
Since records do not allow for inheritance collapsing inner records can be used to compose
+ * structures that certain groups of properties without the need to duplicate the components. This
+ * is important as it still allows to write code that operates on one of such groups of properties
+ * in a way that inheritance would without the need to duplicate the code that can handle data
+ * having those properties. In contrast to inheritance collapsed composition supports the equivalent
+ * of multiple-inheritance simply by having multiple inner collapsed record properties.
+ *
+ *
Please note that ATM records with generics are not supported simply to keep the implementation
+ * simple.
+ *
+ * @author Jan Bernitt
+ * @since 1.9
+ */
+@Target(ElementType.RECORD_COMPONENT)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Collapsed {}
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java b/src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
index eac74ae..c59f12f 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
@@ -125,19 +125,9 @@ default boolean exists(CharSequence name) {
* @since 0.11 (as Stream)
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
- default Stream keys() {
- if (isUndefined() || isEmpty()) return Stream.empty();
- return node().keys().stream();
- }
-
- /**
- * @return a stream of the map/object values in order of their declaration
- * @throws JsonTreeException in case this node does exist but is not an object node
- * @since 0.11
- */
- @TerminalOp(canBeUndefined = true, mustBeObject = true)
- default Stream values() {
- return entries();
+ default Streamable.Sized keys() {
+ if (isUndefined() || isEmpty()) return Streamable.empty();
+ return node().keys();
}
/**
@@ -146,9 +136,9 @@ default Stream values() {
* @since 0.11
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
- default Stream entries() {
- if (isUndefined() || isEmpty()) return Stream.empty();
- return node().keys().stream().map(this::get);
+ default Streamable.Sized entries() {
+ if (isUndefined() || isEmpty()) return Streamable.empty();
+ return node().keys().map(this::get);
}
/**
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonAccess.java b/src/main/java/org/hisp/dhis/jsontree/JsonAccess.java
index 7104098..60cc0b7 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonAccess.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonAccess.java
@@ -267,8 +267,8 @@ public static Stream> accessAsStream(JsonMixed stream, Type as, JsonAccessors
JsonAccessor> elements = accessors.accessor(getRawType(elementType));
// auto-box simple values in a 1 element sequence
if (!stream.isArray()) return Stream.of(elements.access(stream, as, accessors));
- return stream.stream(Index.AUTO_SKIP)
- .map(e -> elements.access(e.as(JsonMixed.class), elementType, accessors));
+ return stream.values(Index.AUTO_SKIP)
+ .map(e -> elements.access(e.as(JsonMixed.class), elementType, accessors)).stream();
}
@SuppressWarnings({"java:S1168", "java:S1452"})
@@ -299,6 +299,7 @@ public static Stream> accessAsStream(JsonMixed stream, Type as, JsonAccessors
* @param components the components in that constructor to pass as args
* @param defaults default arguments if the component is undefined in JSON, null of no defaults
* are available (taken from a constant with name {@code DEFAULT}).
+ * @param collapsed true, if the component was annotated with {@link Collapsed}
* @param of a static method to construct a record from a single argument (typical {@code
* of}-method)
* @param ofArgType the generic argument type of the {@link #of()}-method
@@ -307,6 +308,7 @@ private record NewRecord(
MethodHandle constructor,
RecordComponent[] components,
Object[] defaults,
+ boolean[] collapsed,
MethodHandle of,
Type ofArgType) {}
@@ -340,11 +342,12 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access
Object[] args = new Object[components.length];
Object[] defaults = newRecord.defaults;
+ boolean[] collapsed = newRecord.collapsed;
boolean hasDefaults = defaults != null;
if (obj.isObject()) {
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
- JsonMixed cValue = obj.get(c.getName());
+ JsonMixed cValue = collapsed[i] ? obj : obj.get(c.getName());
args[i] =
hasDefaults && cValue.isUndefined()
? defaults[i]
@@ -353,7 +356,7 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access
} else if (obj.isArray()) {
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
- JsonMixed cValue = obj.get(i);
+ JsonMixed cValue = collapsed[i] ? obj : obj.get(i);
args[i] =
hasDefaults && cValue.isUndefined()
? defaults[i]
@@ -399,6 +402,9 @@ private static NewRecord createRecordFactory(
throw new JsonAccessException(
"JSON cannot be mapped to Java record %s, canonical constructor is not accessible"
.formatted(type.getSimpleName()));
+ boolean[] collapsed = new boolean[components.length];
+ for (int i = 0; i < components.length; i++)
+ collapsed[i] = components[i].isAnnotationPresent(Collapsed.class);
MethodHandle ofMethod = null;
Type ofMethodArgType = null;
if (components.length > 1) {
@@ -411,7 +417,7 @@ private static NewRecord createRecordFactory(
}
}
}
- return new NewRecord(canonical, components, defaults(type), ofMethod, ofMethodArgType);
+ return new NewRecord(canonical, components, defaults(type), collapsed, ofMethod, ofMethodArgType);
}
private static MethodHandle constructor(
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonArray.java b/src/main/java/org/hisp/dhis/jsontree/JsonArray.java
index f809662..887bfb8 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonArray.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonArray.java
@@ -30,6 +30,7 @@
import static org.hisp.dhis.jsontree.JsonNode.Index.AUTO;
import static org.hisp.dhis.jsontree.JsonNode.Index.SKIP;
+import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.DoubleStream;
@@ -37,6 +38,7 @@
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hisp.dhis.jsontree.JsonNode.Index;
+import org.hisp.dhis.jsontree.internal.NotNull;
import org.hisp.dhis.jsontree.internal.TerminalOp;
/**
@@ -56,38 +58,59 @@ default JsonArray getValue() {
return this; // return type override
}
+ /**
+ * Index access to the array.
+ *
+ * Note that this will neither check index nor element type.
+ *
+ * @param index index to access (0 and above)
+ * @param as assumed type of the element
+ * @param type of the returned element
+ * @return element at the given index
+ */
+ E get(int index, Class as);
+
+ @Override
+ @TerminalOp(canBeUndefined = true, mustBeArray = true)
+ default @NotNull Iterator iterator() {
+ return values(AUTO).iterator();
+ }
+
@Override
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default Stream stream() {
- return stream(AUTO);
+ return values(AUTO).stream();
}
/**
- * @implNote This utilizes {@link JsonNode#elements(Index)} avoiding map lookups for each element.
- * On {@link JsonAbstractArray} level this cannot be done as the node cannot be {@link
- * JsonNode#lift(JsonAccessors)} ed to the unknown generic target type.
- * @param index the strategy to apply when it comes to node lookup and indexing
+ * @see #values(Index)
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeArray = true)
- default Stream stream(JsonNode.Index index) {
- JsonNode node = nodeIfExists();
- if (node == null || node.isNull()) return Stream.empty();
- JsonAccessors accessors = getAccessors();
- return node.elements(index).stream().map(n -> n.lift(accessors));
+ default Streamable.Sized values() {
+ return values(SKIP);
}
/**
- * Index access to the array.
- *
- * Note that this will neither check index nor element type.
- *
- * @param index index to access (0 and above)
- * @param as assumed type of the element
- * @param type of the returned element
- * @return element at the given index
+ * @see #values()
+ * @since 1.9
*/
- E get(int index, Class as);
+ default Streamable.Sized values(Class to) {
+ return values().map(e -> e.to(to));
+ }
+
+ /**
+ * @return this arrays values as {@link org.hisp.dhis.jsontree.Streamable.Sized}
+ * @throws JsonTreeException if this node exist but is not an array or null node
+ * @since 1.9
+ */
+ @TerminalOp(canBeUndefined = true, mustBeArray = true)
+ default Streamable.Sized values(JsonNode.Index index) {
+ JsonNode node = nodeIfExists();
+ if (node == null || node.isNull()) return Streamable.empty();
+ JsonAccessors accessors = getAccessors();
+ return node.elements(index).map(n -> n.lift(accessors));
+ }
/**
* @return the array elements as a uniform list of {@link String}
@@ -97,7 +120,7 @@ default Stream stream(JsonNode.Index index) {
default List stringValues() {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return List.of();
- return node.elements(JsonNode.Index.SKIP).stream()
+ return node.elements(SKIP)
.map(JsonNode::textValue)
.map(Text::toString)
.toList();
@@ -111,18 +134,7 @@ default List stringValues() {
default List booleanValues() {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return List.of();
- return node.elements(JsonNode.Index.SKIP).stream().map(JsonNode::booleanValue).toList();
- }
-
- @TerminalOp(canBeUndefined = true, mustBeArray = true)
- default List values(Function f) {
- JsonNode node = nodeIfExists();
- if (node == null || node.isNull()) return List.of();
- return node.elements(JsonNode.Index.SKIP).stream()
- .map(JsonNode::textValue)
- .map(Text::toString)
- .map(f)
- .toList();
+ return node.elements(SKIP).map(JsonNode::booleanValue).toList();
}
/**
@@ -158,30 +170,6 @@ default DoubleStream doubleValues() {
return node.elements(SKIP).stream().mapToDouble(JsonNode::doubleValue);
}
- /**
- * Uses {@link #getAccessors()} for type conversion.
- *
- * @param to element target type
- * @return a stream of all array values accessed as the given type in the order defined in JSON
- * @since 1.9
- */
- @TerminalOp(canBeUndefined = true, mustBeArray = true)
- default Stream streamValues(Class to) {
- return stream(SKIP).map(e -> e.to(to));
- }
-
- /**
- * Uses {@link #getAccessors()} for type conversion.
- *
- * @param to element target type
- * @return a list of all array values accessed as the given type in the order defined in JSON
- * @since 1.9
- */
- @TerminalOp(canBeUndefined = true, mustBeArray = true)
- default List listValues(Class to) {
- return streamValues(to).toList();
- }
-
default JsonMixed get(int index) {
return get(index, JsonMixed.class);
}
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonDiff.java b/src/main/java/org/hisp/dhis/jsontree/JsonDiff.java
index c76cc79..1a9a6ad 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonDiff.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonDiff.java
@@ -200,7 +200,7 @@ private static void diffObject(
JsonObject e, JsonObject a, Mode mode, Consumer add, PropertyInfo p) {
if (!p.anyAdditional(mode.objects.anyAdditional)) {
// list all extra members
- a.keys()
+ a.keys().stream()
.filter(not(e::has))
.forEach(key -> add.accept(new Difference(Type.MORE, e.get(key), a.get(key))));
}
@@ -211,7 +211,7 @@ private static void diffObject(
} else {
// exact order
Iterator eKeys = e.keys().iterator();
- Iterator aKeys = a.keys().filter(e::has).iterator();
+ Iterator aKeys = a.keys().stream().filter(e::has).iterator();
while (eKeys.hasNext() && aKeys.hasNext()) {
Text eKey = eKeys.next();
Text aKey = aKeys.next();
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonMap.java b/src/main/java/org/hisp/dhis/jsontree/JsonMap.java
index 7f55c25..d53627b 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonMap.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonMap.java
@@ -51,7 +51,7 @@ public interface JsonMap extends JsonAbstractObject {
* @since 0.11
*/
default Map toMap(Function toValue) {
- return entries().collect(Collectors.toMap(e -> e.getKey().toString(), toValue));
+ return entries().stream().collect(Collectors.toMap(e -> e.getKey().toString(), toValue));
}
/**
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonMixed.java b/src/main/java/org/hisp/dhis/jsontree/JsonMixed.java
index 2774fb9..e053f53 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonMixed.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonMixed.java
@@ -27,8 +27,7 @@
*/
@Validation(type = {OBJECT, ARRAY, STRING, NUMBER, INTEGER, BOOLEAN})
@Validation.Ignore
-public interface JsonMixed
- extends JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonInteger {
+public interface JsonMixed extends JsonObject, JsonArray, JsonPrimitive {
/**
* Lift an actual {@link JsonNode} tree to a virtual {@link JsonValue}.
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonNode.java b/src/main/java/org/hisp/dhis/jsontree/JsonNode.java
index 1f6075f..67c6876 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonNode.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonNode.java
@@ -576,7 +576,7 @@ default JsonNode memberIfExists(Text name) throws JsonPathException {
* @return this {@link #value()} as a sequence of {@link Entry}s
* @throws JsonTreeException if this node is not an object node that could have members
*/
- default Streamable members() {
+ default Streamable.Sized members() {
throw notA(OBJECT, this, "members()");
}
@@ -593,7 +593,7 @@ default Streamable members() {
* @throws JsonTreeException if this node is not an object node that could have members
* @since 0.11
*/
- default Streamable keys() {
+ default Streamable.Sized keys() {
throw notA(OBJECT, this, "keys()");
}
@@ -604,7 +604,7 @@ default Streamable keys() {
* @throws JsonTreeException if this node is not an object node that could have members
* @since 1.2
*/
- default Streamable paths() {
+ default Streamable.Sized paths() {
throw notA(OBJECT, this, "paths()");
}
@@ -619,7 +619,7 @@ default Streamable paths() {
* internally will be reused and returned by the iterator.
* @throws JsonTreeException if this node is not an object node that could have members
*/
- default Streamable members(Index index) {
+ default Streamable.Sized members(Index index) {
throw notA(OBJECT, this, "members(Index)");
}
@@ -674,7 +674,7 @@ default JsonNode elementIfExists(int index) throws JsonPathException, JsonTreeEx
* @return this {@link #value()} as as {@link Stream}
* @throws JsonTreeException if this node is not an array node that could have elements
*/
- default Streamable elements() {
+ default Streamable.Sized elements() {
throw notA(ARRAY, this, "elements()");
}
@@ -689,7 +689,7 @@ default Streamable elements() {
* already exist internally will be reused and returned by the iterator.
* @throws JsonTreeException if this node is not an array node that could have elements
*/
- default Streamable elements(Index index) {
+ default Streamable.Sized elements(Index index) {
throw notA(ARRAY, this, "elements(Index)");
}
@@ -702,7 +702,7 @@ default Streamable elements(Index index) {
* @throws JsonTreeException if this node is not an array node that could have elements
* @since 1.9
*/
- default Streamable values() {
+ default Streamable.Sized values() {
throw notA(ARRAY, this, "values()");
}
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonObject.java b/src/main/java/org/hisp/dhis/jsontree/JsonObject.java
index d6cee84..ae49a8d 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonObject.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonObject.java
@@ -35,7 +35,6 @@
import java.util.List;
import java.util.Set;
import java.util.function.Function;
-import java.util.stream.Stream;
import org.hisp.dhis.jsontree.JsonNode.Index;
import org.hisp.dhis.jsontree.Validation.Rule;
import org.hisp.dhis.jsontree.internal.TerminalOp;
@@ -96,6 +95,16 @@ static List properties(Class> of) {
return JsonVirtualTree.properties(of);
}
+ /**
+ * @param of an object type
+ * @return a list of the properties in the given object type including those collapsed down from
+ * {@link Collapsed} inner {@link Record}s
+ * @since 1.9
+ */
+ static List collapsedProperties(Class extends Record> of) {
+ return JsonVirtualTree.collapsedProperties(of);
+ }
+
/**
* Access to object fields by name.
*
@@ -160,9 +169,13 @@ default JsonMultiMap getMultiMap(CharSequence name, Cla
return JsonAbstractCollection.asMultiMap(getObject(name), as);
}
+ /**
+ * @see #entries(Index)
+ * @since 1.9
+ */
@Override
@TerminalOp(canBeUndefined = true, mustBeObject = true)
- default Stream entries() {
+ default Streamable.Sized entries() {
return entries(AUTO);
}
@@ -179,10 +192,10 @@ default Stream entries() {
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
- default Stream entries(JsonNode.Index index) {
- if (isUndefined() || isEmpty()) return Stream.empty();
+ default Streamable.Sized entries(JsonNode.Index index) {
+ if (isUndefined() || isEmpty()) return Streamable.empty();
JsonAccessors accessors = getAccessors();
- return node().members(index).stream().map(e -> e.lift(accessors));
+ return node().members(index).map(e -> e.lift(accessors));
}
/**
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonPrimitive.java b/src/main/java/org/hisp/dhis/jsontree/JsonPrimitive.java
index 1889cb9..f53c10d 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonPrimitive.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonPrimitive.java
@@ -38,7 +38,7 @@
*/
@Validation(type = {BOOLEAN, NUMBER, STRING})
@Validation.Ignore
-public interface JsonPrimitive extends JsonBoolean, JsonNumber, JsonString {
+public interface JsonPrimitive extends JsonBoolean, JsonString, JsonInteger {
@Override
default JsonPrimitive getValue() {
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonTree.java b/src/main/java/org/hisp/dhis/jsontree/JsonTree.java
index a10e673..d5676de 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonTree.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonTree.java
@@ -322,12 +322,12 @@ public Iterator iterator() {
}
@Override
- public Streamable members() {
+ public Streamable.Sized members() {
return members(Index.AUTO);
}
@Override
- public Streamable members(Index op) {
+ public Streamable.Sized members(Index op) {
if (isEmpty()) return Streamable.empty();
return new Streamable.Sized<>() {
private final char[] json = tree.json;
@@ -383,12 +383,12 @@ public boolean isMember(@NotNull Text name) {
}
@Override
- public Streamable paths() {
+ public Streamable.Sized paths() {
return keysSized(path::chain);
}
@Override
- public Streamable keys() {
+ public Streamable.Sized keys() {
return keysSized(name -> name);
}
@@ -562,12 +562,12 @@ private JsonNode element(int index, Text segment, boolean orNull) throws JsonPat
}
@Override
- public Streamable elements() {
+ public Streamable.Sized elements() {
return elements(Index.AUTO);
}
@Override
- public Streamable elements(Index op) {
+ public Streamable.Sized elements(Index op) {
if (isEmpty()) return Streamable.empty();
return new Streamable.Sized<>() {
private final char[] json = tree.json;
@@ -600,7 +600,7 @@ public JsonNode next() {
}
@Override
- public Streamable values() {
+ public Streamable.Sized values() {
if (isEmpty()) return Streamable.empty();
return new Streamable.Sized<>() {
private final char[] json = tree.json;
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonValue.java b/src/main/java/org/hisp/dhis/jsontree/JsonValue.java
index f178f14..5997f17 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonValue.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonValue.java
@@ -394,7 +394,7 @@ private static boolean equivalentTo(
if (a.isNumber()) return a.doubleValue() == b.doubleValue();
if (a.isArray())
return a.size() == b.size() && a.indexes().allMatch(i -> eqItems.test(a.get(i), b.get(i)));
- return a.size() == b.size() && a.keys().allMatch(key -> eqItems.test(a.get(key), b.get(key)));
+ return a.size() == b.size() && a.keys().stream().allMatch(key -> eqItems.test(a.get(key), b.get(key)));
}
/**
diff --git a/src/main/java/org/hisp/dhis/jsontree/JsonVirtualTree.java b/src/main/java/org/hisp/dhis/jsontree/JsonVirtualTree.java
index bcc16c9..c89eb74 100644
--- a/src/main/java/org/hisp/dhis/jsontree/JsonVirtualTree.java
+++ b/src/main/java/org/hisp/dhis/jsontree/JsonVirtualTree.java
@@ -79,6 +79,7 @@ final class JsonVirtualTree implements JsonMixed, Serializable {
new JsonVirtualTree(JsonNode.NULL, JsonPath.SELF, JsonAccess.GLOBAL);
private static final Map, List> PROPERTIES = new ConcurrentHashMap<>();
+ private static final Map, List> COLLAPSED_PROPERTIES = new ConcurrentHashMap<>();
static List properties(Class> of) {
if (JsonObject.class.isAssignableFrom(of)) {
@@ -95,6 +96,10 @@ static List properties(Class> of) {
"Must be a subtype of JsonObject or Record but was: " + of);
}
+ static List collapsedProperties(Class extends Record> of) {
+ return COLLAPSED_PROPERTIES.computeIfAbsent(of, type -> collapsedComponentProperties(of));
+ }
+
static JsonMixed lift(JsonNode node, JsonAccessors accessors) {
return new JsonVirtualTree(node.getRoot(), node.path(), node, accessors);
}
@@ -475,6 +480,26 @@ private void toSignature(Type type, StringBuilder str) {
}
}
+ private static List collapsedComponentProperties(Class extends Record> of) {
+ List res = new ArrayList<>();
+ List properties = componentProperties(of);
+ for (Property p : properties) {
+ if (p.source().isAnnotationPresent(Collapsed.class)) {
+ Type type = p.javaType().getType();
+ if (type instanceof Class> c && c.isRecord()) {
+ @SuppressWarnings("unchecked")
+ Class extends Record> rType = (Class extends Record>) type;
+ res.addAll(collapsedComponentProperties(rType));
+ } else {
+ res.add(p);
+ }
+ } else {
+ res.add(p);
+ }
+ }
+ return List.copyOf(res);
+ }
+
private static List componentProperties(Class extends Record> of) {
return Stream.of(of.getRecordComponents())
.map(
diff --git a/src/main/java/org/hisp/dhis/jsontree/Streamable.java b/src/main/java/org/hisp/dhis/jsontree/Streamable.java
index c2eff69..b0f60b5 100644
--- a/src/main/java/org/hisp/dhis/jsontree/Streamable.java
+++ b/src/main/java/org/hisp/dhis/jsontree/Streamable.java
@@ -2,10 +2,13 @@
import static java.util.Collections.emptyIterator;
+import java.util.ArrayList;
import java.util.Iterator;
+import java.util.List;
import java.util.NoSuchElementException;
import java.util.Spliterator;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.hisp.dhis.jsontree.internal.NotNull;
@@ -91,6 +94,44 @@ default long estimateSize() {
default int characteristics() {
return ORDERED | SIZED | NONNULL | IMMUTABLE;
}
+
+ default Sized map(Function f) {
+ Sized self = this;
+ return new Sized() {
+ @Override
+ public long getExactSizeIfKnown() {
+ return self.getExactSizeIfKnown();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return self.hasNext();
+ }
+
+ @Override
+ public E next() {
+ return f.apply(self.next());
+ }
+ };
+ }
+
+ /**
+ * @see Stream#toList()
+ */
+ default List toList() {
+ int size = (int) getExactSizeIfKnown();
+ if (size == 0) return List.of();
+ List res = new ArrayList<>(size);
+ while (hasNext()) res.add(next());
+ return res;
+ }
+
+ /**
+ * @see Stream#count()
+ */
+ default long count() {
+ return getExactSizeIfKnown();
+ }
}
Sized> EMPTY = new Sized<>() {
diff --git a/src/main/java/org/hisp/dhis/jsontree/TextualNumber.java b/src/main/java/org/hisp/dhis/jsontree/TextualNumber.java
index 18ccd73..e0615fd 100644
--- a/src/main/java/org/hisp/dhis/jsontree/TextualNumber.java
+++ b/src/main/java/org/hisp/dhis/jsontree/TextualNumber.java
@@ -31,6 +31,13 @@ public static Number of(char @NotNull [] buffer) {
/**
* Does not allow whitespace at start/end of the number.
*
+ * The returned number type is chosen to reflect the input text as close as possible. An
+ * integer input will result in {@link Integer} or {@link Long} based on numeric range as long as
+ * the range allows for it based on digit count (not exact numeric range). An input with decimals
+ * will result in {@link Double} for zero or a {@link TextualNumber} for anything else. Negative
+ * zero will always return a {@link Double} to preserve the distinction between positive and
+ * negative zero.
+ *
* @param buffer with content of an integer or decimal number
* @param offset start of the number in the buffer
* @param length number of characters from offset that represent the number
@@ -59,6 +66,8 @@ public static Number of(char @NotNull [] buffer, int offset, int length) {
return new TextualNumber(buffer, offset, length);
}
}
+ if (isNumericZero(buffer, offset, length))
+ return buffer[offset] == '-' ? -0d : 0d;
if (!isTextualDecimal(buffer, offset, length) && !Text.of(buffer, offset, length).isSpecialDecimal())
throw nanError(buffer, offset, length, "Is neither an integer nor a decimal number: ");
return new TextualNumber(buffer, offset, length);
@@ -169,10 +178,12 @@ static boolean isNumericZero(char[] buffer, int offset, int length) {
int end = offset + length;
int d0 = i;
while (i < end && buffer[i] == '0') i++;
- if (i >= end) return i > d0;
+ boolean integerDigits = i > d0;
+ if (i >= end) return integerDigits;
if (buffer[i++] != '.') return false;
+ d0 = i;
while (i < end && buffer[i] == '0') i++;
- return i == end;
+ return i == end && (integerDigits || i > d0);
}
static boolean isNumericInteger(char[] buffer) {
diff --git a/src/main/java/org/hisp/dhis/jsontree/Validation.java b/src/main/java/org/hisp/dhis/jsontree/Validation.java
index 34e6e30..4efc9b7 100644
--- a/src/main/java/org/hisp/dhis/jsontree/Validation.java
+++ b/src/main/java/org/hisp/dhis/jsontree/Validation.java
@@ -177,6 +177,7 @@ public static Set of(@NotNull Class> type) {
static Class extends JsonValue> toJsonType(Class> type) {
if (type == String.class || type.isEnum()) return JsonString.class;
if (type == Character.class || type == char.class) return JsonString.class;
+ if (type == Class.class) return JsonString.class;
if (type == Date.class) return JsonDate.class;
if (type == LocalDate.class || type == LocalTime.class || type == LocalDateTime.class) return JsonMixed.class;
if (type == Boolean.class || type == boolean.class) return JsonBoolean.class;
diff --git a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java
index acb0ba8..83517fb 100644
--- a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java
+++ b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidation.java
@@ -222,12 +222,12 @@ private static PropertyValidations fromItems(AnnotatedElement src) {
@NotNull
private static PropertyValidations toPropertyValidation(Class> type) {
- RequiredValidation values =
+ RequiredValidation requiredness =
!type.isPrimitive() ? null : RequiredValidation.PRIMITIVES;
StringValidation strings =
!type.isEnum() ? null : new StringValidation(anyOfStrings(type), YesNo.AUTO, -1, -1, null);
return new PropertyValidations(
- NodeType.of(type), PropertyValidations.Strict.DEFAULT, List.of(), values, strings, null, null, null, null);
+ NodeType.of(type), PropertyValidations.Strict.DEFAULT, List.of(), requiredness, strings, null, null, null, null);
}
@NotNull
diff --git a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidator.java b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidator.java
index 1c84cdf..d7f5659 100644
--- a/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidator.java
+++ b/src/main/java/org/hisp/dhis/jsontree/validation/ObjectValidator.java
@@ -196,7 +196,8 @@ private static Validator ofRequired(Text property, PropertyValidations validatio
* possible and if required is AUTO
*/
private static boolean isRequiredImplicitly(PropertyValidations validations) {
- return validations.accepted().stream().allMatch(type -> isRequiredImplicitly(validations, type));
+ Set accepted = validations.accepted();
+ return !accepted.isEmpty() && accepted.stream().allMatch(type -> isRequiredImplicitly(validations, type));
}
private static boolean isRequiredImplicitly(PropertyValidations validations, NodeType type) {
diff --git a/src/test/java/org/hisp/dhis/jsontree/JsonApiPerfTest.java b/src/test/java/org/hisp/dhis/jsontree/JsonApiPerfTest.java
index b600bf6..fd6c319 100644
--- a/src/test/java/org/hisp/dhis/jsontree/JsonApiPerfTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/JsonApiPerfTest.java
@@ -101,12 +101,12 @@ void testJsonValue_doubleValues() {
}
@Test
- void testJsonValue_streamValues() {
+ void testJsonValue_values() {
List accessed = new ArrayList<>();
JsonObject root = JsonMixed.of(JsonNode.of("{\"values\":[1,2,3,4,5]", accessed::add));
assertEquals(List.of(), accessed);
- assertEquals(5L, root.getArray("values").streamValues(double.class).count());
+ assertEquals(5L, root.getArray("values").values(double.class).count());
assertEquals(List.of(JsonPath.of("values")), accessed, "there should be one access of values");
}
@@ -141,12 +141,12 @@ void testJsonNode_elements() {
@Test
void testJsonArray_stream() {
JsonArray arr = JsonMixed.of("[ 1,2 , true , false, \"hello\",{},[]]");
- List nodes = arr.stream(Index.SKIP).map(JsonValue::node).toList();
- List nodes2 = arr.stream(Index.SKIP).map(JsonValue::node).toList();
+ List nodes = arr.values(Index.SKIP).map(JsonValue::node).toList();
+ List nodes2 = arr.values(Index.SKIP).map(JsonValue::node).toList();
for (int i = 0; i < nodes.size(); i++)
assertNotSame(nodes.get(i), nodes2.get(i));
- nodes = arr.stream(Index.ADD).map(JsonValue::node).toList();
- nodes2 = arr.stream(Index.CHECK).map(JsonValue::node).toList();
+ nodes = arr.values(Index.ADD).map(JsonValue::node).toList();
+ nodes2 = arr.values(Index.CHECK).map(JsonValue::node).toList();
for (int i = 0; i < nodes.size(); i++)
assertSame(nodes.get(i), nodes2.get(i));
}
diff --git a/src/test/java/org/hisp/dhis/jsontree/JsonArrayTest.java b/src/test/java/org/hisp/dhis/jsontree/JsonArrayTest.java
index e6f7ed0..507c9ff 100644
--- a/src/test/java/org/hisp/dhis/jsontree/JsonArrayTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/JsonArrayTest.java
@@ -75,16 +75,6 @@ void testForEach_NoArray() {
assertThrowsExactly(JsonTreeException.class, () -> value.forEach(e -> fail()));
}
- @Test
- void testValues_Mapped() {
- // language=json
- String json =
- """
- ["a","b","c"]""";
- JsonMixed arr = JsonMixed.of(json);
- assertEquals(List.of('a', 'b', 'c'), arr.values(str -> str.charAt(0)));
- }
-
@Test
void testGetList_IndexAs() {
JsonMixed arr = JsonMixed.of("[[1,2], [3,4]]");
@@ -111,13 +101,9 @@ void testDoubleValues() {
}
@Test
- void testListValues() {
- assertEquals(List.of(1d, NaN, 3.1d), Json5.of("[1, NaN, 3.1]").listValues(double.class));
- }
-
- @Test
- void streamValues() {
+ void testValues_ToClass() {
+ assertEquals(List.of(1d, NaN, 3.1d), Json5.of("[1, NaN, 3.1]").values(double.class).toList());
assertEquals(
- Stream.of(1, 2, 3).toList(), Json5.of("[1, '2', 3.0]").streamValues(int.class).toList());
+ List.of(1, 2, 3), Json5.of("[1, '2', 3.0]").values(int.class).toList());
}
}
diff --git a/src/test/java/org/hisp/dhis/jsontree/JsonMapTest.java b/src/test/java/org/hisp/dhis/jsontree/JsonMapTest.java
index 723c7b2..ea122a6 100644
--- a/src/test/java/org/hisp/dhis/jsontree/JsonMapTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/JsonMapTest.java
@@ -135,10 +135,10 @@ void testViewAsMap_Keys() {
}
@Test
- void testViewAsMap_Values() {
+ void testViewAsMap_entries() {
JsonMap obj = JsonMixed.of("{\"b\":[1],\"c\":[2]}").asMap(JsonArray.class);
JsonMap view = obj.project(arr -> arr.getNumber(0));
- assertEquals(List.of(1, 2), view.values().map(JsonNumber::intValue).toList());
+ assertEquals(List.of(1, 2), view.entries().map(JsonNumber::intValue).toList());
}
@Test
@@ -151,16 +151,16 @@ void testKeys_Special() {
}
@Test
- void testValues_Special() {
+ void testEntries_Special1() {
String json =
"""
{".":1, "{uid}":2, "[6]":3, "x{y}z": 4}""";
JsonMap map = JsonMixed.of(json).asMap(JsonNumber.class);
- assertEquals(List.of(1, 2, 3, 4), map.values().map(JsonNumber::intValue).toList());
+ assertEquals(List.of(1, 2, 3, 4), map.entries().map(JsonNumber::intValue).toList());
}
@Test
- void testEntries_Special() {
+ void testEntries_Special2() {
String json =
"""
{".":1, "{uid}":2, "[6]":3, "x{y}z": 4}""";
diff --git a/src/test/java/org/hisp/dhis/jsontree/JsonNumberTest.java b/src/test/java/org/hisp/dhis/jsontree/JsonNumberTest.java
index 7b60afd..dfa0c0c 100644
--- a/src/test/java/org/hisp/dhis/jsontree/JsonNumberTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/JsonNumberTest.java
@@ -1,6 +1,7 @@
package org.hisp.dhis.jsontree;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
@@ -166,6 +167,16 @@ void testFloatValue() {
JsonPathException.class, () -> JsonMixed.of("[]").getNumber(0).floatValue());
}
+ @Test
+ void testNumber() {
+ assertEquals(3, JsonMixed.of("3").number());
+ assertEquals(-0d, JsonMixed.of("-0").number());
+ assertTextualNumberEquals(3.0d, JsonMixed.of("3.0").number());
+ assertTextualNumberEquals(3.14d, JsonMixed.of("3.14").number());
+ assertEquals(0d, JsonMixed.of("0.0").number());
+ assertEquals(-0d, JsonMixed.of("-0.0").number());
+ }
+
@Test
void testIrregularNumberPersistence() {
JsonMixed big =
@@ -186,4 +197,9 @@ void testIrregularNumberPersistence() {
}""",
root.getDeclaration().toString());
}
+
+ private static void assertTextualNumberEquals(double expected, Number actual) {
+ assertEquals(expected, actual.doubleValue());
+ assertInstanceOf(TextualNumber.class, actual);
+ }
}
diff --git a/src/test/java/org/hisp/dhis/jsontree/JsonValueToTest.java b/src/test/java/org/hisp/dhis/jsontree/JsonValueToTest.java
index be8b8e0..f7a9492 100644
--- a/src/test/java/org/hisp/dhis/jsontree/JsonValueToTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/JsonValueToTest.java
@@ -19,15 +19,23 @@
*/
class JsonValueToTest {
- public record ExampleParams(
+ public record OuterParams(
int number,
Integer optionalNumber,
String string,
Set types,
List items,
- ExampleItem fallback
+ ExampleItem fallback,
+ @Collapsed InnerParams inner
) {}
+ public record InnerParams(
+ String foo,
+ @Collapsed InnerInnerParams inner
+ ) {}
+
+ public record InnerInnerParams(String bar) {}
+
@Validation(type = NodeType.STRING)
public record ExampleItem(String key, double value) {
public static ExampleItem of(String value) {
@@ -39,14 +47,17 @@ public static ExampleItem of(String value) {
@Test
void testMinimal() {
+ InnerParams inner = new InnerParams(null, new InnerInnerParams(null));
assertParamsEquals(
- new ExampleParams(10, null, null, null, null, null), Map.of("number", List.of("10")));
+ new OuterParams(10, null, null, null, null, null, inner),
+ Map.of("number", List.of("10")));
}
@Test
void testEmpty() {
+ InnerParams inner = new InnerParams(null, new InnerInnerParams(null));
assertParamsEquals(
- new ExampleParams(10, null, null, Set.of(), List.of(), null),
+ new OuterParams(10, null, null, Set.of(), List.of(), null, inner),
Map.ofEntries(
entry("number", List.of("10")),
entry("optionalNumber", List.of()),
@@ -59,43 +70,48 @@ void testEmpty() {
@Test
void testMaximal() {
assertParamsEquals(
- new ExampleParams(
+ new OuterParams(
10,
20,
"hello",
Set.of(JsonNodeType.STRING, JsonNodeType.NULL),
List.of(new ExampleItem("a", 1.5), new ExampleItem("b", 2.0)),
- new ExampleItem("c", 5)),
+ new ExampleItem("c", 5),
+ new InnerParams("foo", new InnerInnerParams("bar"))),
Map.ofEntries(
entry("number", List.of("10")),
entry("optionalNumber", List.of("20")),
entry("string", List.of("hello")),
entry("types", List.of("STRING", "NULL")),
entry("items", List.of("a:1.5", "b:2.0")),
- entry("fallback", List.of("c:5"))));
+ entry("fallback", List.of("c:5")),
+ entry("foo", List.of("foo")),
+ entry("bar", List.of("bar"))));
}
@Test
void testSingle() {
assertParamsEquals(
- new ExampleParams(
+ new OuterParams(
10,
20,
"hello",
Set.of(JsonNodeType.STRING),
List.of(new ExampleItem("a", 1.5)),
- new ExampleItem("c", 5)),
+ new ExampleItem("c", 5),
+ new InnerParams("foo2", new InnerInnerParams(null))),
Map.ofEntries(
entry("number", List.of("10")),
entry("optionalNumber", List.of("20")),
entry("string", List.of("hello")),
entry("types", List.of("STRING")),
entry("items", List.of("a:1.5")),
- entry("fallback", List.of("c:5"))));
+ entry("fallback", List.of("c:5")),
+ entry("foo", List.of("foo2"))));
}
- private static void assertParamsEquals(ExampleParams expected, Map> actual) {
- List properties = JsonObject.properties(ExampleParams.class);
+ private static void assertParamsEquals(OuterParams expected, Map> actual) {
+ List properties = JsonObject.collapsedProperties(OuterParams.class);
JsonNode object =
JsonBuilder.createObject(
obj -> {
@@ -132,7 +148,7 @@ private static void assertParamsEquals(ExampleParams expected, Map e.rule() == Validation.Rule.REQUIRED));
+ }
+
+ public record NotImplicitlyRequired(Class> type) {}
+
+ @Test
+ void testNotImplicitlyRequired() {
+ Validation.Result result = JsonMixed.of("{}").validate(NotImplicitlyRequired.class, PROBE_ALL);
+ assertEquals(List.of(), result.errors());
+ }
}
diff --git a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscTypeUseTest.java b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscTypeUseTest.java
index 0387764..b9f9572 100644
--- a/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscTypeUseTest.java
+++ b/src/test/java/org/hisp/dhis/jsontree/validation/JsonValidationMiscTypeUseTest.java
@@ -19,7 +19,7 @@ public interface JsonTypeUseExampleA extends JsonObject {
@Validator(value = Validation.RegEx.class, params = @Validation(pattern = ".es.*"))
String>>
getData() {
- return getArray("data").values(e -> List.of());
+ return getArray("data").values().map(e -> List.of(e.text().toString())).toList();
}
}