From 27b8099d126b735a6b8137e29a984af74cd73f78 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 11 Dec 2024 11:51:07 +1300 Subject: [PATCH 1/3] Initial add of avaje-json-node --- json-node/pom.xml | 31 ++++ .../java/io/avaje/json/node/JsonArray.java | 66 ++++++++ .../java/io/avaje/json/node/JsonBoolean.java | 29 ++++ .../java/io/avaje/json/node/JsonDecimal.java | 58 +++++++ .../java/io/avaje/json/node/JsonDouble.java | 58 +++++++ .../java/io/avaje/json/node/JsonInteger.java | 58 +++++++ .../java/io/avaje/json/node/JsonLong.java | 58 +++++++ .../java/io/avaje/json/node/JsonNode.java | 26 ++++ .../io/avaje/json/node/JsonNodeAdapter.java | 118 ++++++++++++++ .../java/io/avaje/json/node/JsonNumber.java | 41 +++++ .../java/io/avaje/json/node/JsonObject.java | 66 ++++++++ .../java/io/avaje/json/node/JsonString.java | 24 +++ .../avaje/json/node/adapter/ArrayAdapter.java | 44 ++++++ .../json/node/adapter/BooleanAdapter.java | 19 +++ .../json/node/adapter/DJsonNodeAdapter.java | 82 ++++++++++ .../json/node/adapter/DecimalAdapter.java | 19 +++ .../json/node/adapter/DoubleAdapter.java | 19 +++ .../json/node/adapter/IntegerAdapter.java | 19 +++ .../avaje/json/node/adapter/LongAdapter.java | 19 +++ .../avaje/json/node/adapter/NodeAdapter.java | 87 +++++++++++ .../json/node/adapter/NodeAdapterBuilder.java | 38 +++++ .../json/node/adapter/NumberAdapter.java | 26 ++++ .../json/node/adapter/ObjectAdapter.java | 57 +++++++ .../json/node/adapter/StringAdapter.java | 19 +++ .../java/io/avaje/json/node/package-info.java | 32 ++++ json-node/src/main/java/module-info.java | 6 + .../node/adapter/JsonNodeAdaptersTest.java | 145 ++++++++++++++++++ pom.xml | 1 + 28 files changed, 1265 insertions(+) create mode 100644 json-node/pom.xml create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonArray.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonBoolean.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonDecimal.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonDouble.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonInteger.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonLong.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonNode.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonNodeAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonNumber.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonObject.java create mode 100644 json-node/src/main/java/io/avaje/json/node/JsonString.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/ArrayAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/BooleanAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/DJsonNodeAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/DoubleAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/IntegerAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/LongAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapterBuilder.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/NumberAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/ObjectAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/adapter/StringAdapter.java create mode 100644 json-node/src/main/java/io/avaje/json/node/package-info.java create mode 100644 json-node/src/main/java/module-info.java create mode 100644 json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java diff --git a/json-node/pom.xml b/json-node/pom.xml new file mode 100644 index 00000000..a674fdb1 --- /dev/null +++ b/json-node/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + io.avaje + avaje-jsonb-parent + 3.0-RC1 + + + avaje-json-node + + + + + io.avaje + avaje-json + 3.0-RC1 + + + + io.avaje + junit + 1.5 + test + + + + + diff --git a/json-node/src/main/java/io/avaje/json/node/JsonArray.java b/json-node/src/main/java/io/avaje/json/node/JsonArray.java new file mode 100644 index 00000000..95e05194 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonArray.java @@ -0,0 +1,66 @@ +package io.avaje.json.node; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * JSON Array type. + */ +public final class JsonArray implements JsonNode { + + private final List children; + + /** + * Create a new JsonArray that can be added to. + */ + public static JsonArray create() { + return new JsonArray(new ArrayList<>()); + } + + /** + * Create an unmodifiable JsonArray with the given elements. + */ + public static JsonArray of(List children) { + return new JsonArray(Collections.unmodifiableList(children)); + } + + JsonArray(List children) { + this.children = requireNonNull(children); + } + + @Override + public Type type() { + return Type.ARRAY; + } + + @Override + public String text() { + return children.toString(); + } + + /** + * Return the child elements. + */ + public List elements() { + return children; + } + + /** + * Return true if the json array is empty. + */ + public boolean isEmpty() { + return children.isEmpty(); + } + + /** + * Add an element to the json array. + */ + public JsonArray add(JsonNode element) { + children.add(element); + return this; + } + +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java b/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java new file mode 100644 index 00000000..0bad10b5 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java @@ -0,0 +1,29 @@ +package io.avaje.json.node; + +public final class JsonBoolean implements JsonNode { + + private final boolean value; + + public static JsonBoolean of(boolean value) { + return new JsonBoolean(value); + } + + private JsonBoolean(boolean value) { + this.value = value; + } + + @Override + public Type type() { + return Type.BOOLEAN; + } + + @Override + public String text() { + return Boolean.toString(value); + } + + public boolean value() { + return value; + } + +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java b/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java new file mode 100644 index 00000000..f2601a8f --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java @@ -0,0 +1,58 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonWriter; + +import java.math.BigDecimal; + +public final class JsonDecimal implements JsonNumber { + + private final BigDecimal value; + + public static JsonDecimal of(BigDecimal value) { + return new JsonDecimal(value); + } + + private JsonDecimal(BigDecimal value) { + this.value = value; + } + + @Override + public Type type() { + return Type.NUMBER; + } + + @Override + public String text() { + return value.toString(); + } + + @Override + public int intValue() { + return value.intValueExact(); + } + + @Override + public long longValue() { + return value.longValueExact(); + } + + @Override + public double doubleValue() { + return value.doubleValue(); + } + + @Override + public BigDecimal decimalValue() { + return value; + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public void toJson(JsonWriter writer) { + writer.value(value); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonDouble.java b/json-node/src/main/java/io/avaje/json/node/JsonDouble.java new file mode 100644 index 00000000..c46cc855 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonDouble.java @@ -0,0 +1,58 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonWriter; + +import java.math.BigDecimal; + +public final class JsonDouble implements JsonNumber { + + private final double value; + + public static JsonDouble of(double value) { + return new JsonDouble(value); + } + + private JsonDouble(double value) { + this.value = value; + } + + @Override + public Type type() { + return Type.NUMBER; + } + + @Override + public String text() { + return Double.toString(value); + } + + @Override + public int intValue() { + return (int)value; + } + + @Override + public long longValue() { + return (long)value; + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public BigDecimal decimalValue() { + return BigDecimal.valueOf(this.value); + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public void toJson(JsonWriter writer) { + writer.value(value); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonInteger.java b/json-node/src/main/java/io/avaje/json/node/JsonInteger.java new file mode 100644 index 00000000..3c5ebafe --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonInteger.java @@ -0,0 +1,58 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonWriter; + +import java.math.BigDecimal; + +public final class JsonInteger implements JsonNumber { + + private final int value; + + public static JsonInteger of(int value) { + return new JsonInteger(value); + } + + private JsonInteger(int value) { + this.value = value; + } + + @Override + public Type type() { + return Type.NUMBER; + } + + @Override + public String text() { + return Long.toString(value); + } + + @Override + public int intValue() { + return value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public BigDecimal decimalValue() { + return BigDecimal.valueOf(value); + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public void toJson(JsonWriter writer) { + writer.value(value); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonLong.java b/json-node/src/main/java/io/avaje/json/node/JsonLong.java new file mode 100644 index 00000000..e98b86f3 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonLong.java @@ -0,0 +1,58 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonWriter; + +import java.math.BigDecimal; + +public final class JsonLong implements JsonNumber { + + private final long value; + + public static JsonLong of(long value) { + return new JsonLong(value); + } + + private JsonLong(long value) { + this.value = value; + } + + @Override + public Type type() { + return Type.NUMBER; + } + + @Override + public String text() { + return Long.toString(value); + } + + @Override + public int intValue() { + return (int)value; + } + + @Override + public long longValue() { + return value; + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public BigDecimal decimalValue() { + return BigDecimal.valueOf(value); + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public void toJson(JsonWriter writer) { + writer.value(value); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonNode.java b/json-node/src/main/java/io/avaje/json/node/JsonNode.java new file mode 100644 index 00000000..4ecb803e --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonNode.java @@ -0,0 +1,26 @@ +package io.avaje.json.node; + +public interface JsonNode { + + /** + * Return the type of the node. + */ + Type type(); + + String text(); + + /** + * The types for JsonNode. + */ + enum Type { + NULL, + ARRAY, + OBJECT, + BOOLEAN, + STRING, + NUMBER, + // BINARY, + // MISSING, + // POJO + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonNodeAdapter.java b/json-node/src/main/java/io/avaje/json/node/JsonNodeAdapter.java new file mode 100644 index 00000000..c242cb0a --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonNodeAdapter.java @@ -0,0 +1,118 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.node.adapter.NodeAdapterBuilder; +import io.avaje.json.stream.JsonStream; + +import java.lang.reflect.Type; + +/** + * Provide JsonAdapters for the JsonNode types. + * + *
{@code
+ *
+ * static final JsonNodeAdapter node = JsonNodeAdapter.builder().build();
+ *
+ * JsonArray jsonArray = JsonArray.create()
+ * .add(JsonInteger.of(42))
+ * .add(JsonString.of("foo"));
+ *
+ * var asJson = node.toJson(jsonArray);
+ *
+ * JsonNode jsonNodeFromJson = node.fromJson(asJson);
+ * assertThat(jsonNodeFromJson).isInstanceOf(JsonArray.class);
+ *
+ * JsonArray arrayFromJson = node.fromJson(JsonArray.class, asJson);
+ * assertThat(arrayFromJson.elements()).hasSize(2);
+ *
+ * }
+ */ +public interface JsonNodeAdapter { + + /** + * Create a Builder for the JsonNodeAdapter. + */ + static Builder builder() { + return new NodeAdapterBuilder(); + } + + /** + * Return the JsonAdapter for the given JsonNode type. + * + * @param type The JsonNode type + * @return The adapter for the given type + */ + JsonAdapter of(Class type); + + /** + * Helper method to write the node to JSON. + * + *
{@code
+   * static final JsonNodeAdapter node = JsonNodeAdapter.builder().build();
+   *
+   * JsonArray jsonArray = JsonArray.create()
+   * .add(JsonInteger.of(42))
+   * .add(JsonString.of("foo"));
+   *
+   * var asJson = node.toJson(jsonArray);
+   * }
+ */ + String toJson(JsonNode node); + + /** + * Helper method to read JSON returning a JsonNode. + * + *
{@code
+   * static final JsonNodeAdapter node = JsonNodeAdapter.builder().build();
+   *
+   * JsonNode nodeFromJson = node.fromJson(jsonContent);
+   * }
+ */ + JsonNode fromJson(String json); + + /** + * Helper method to read JSON with an expected JsonNode type. + * + *
{@code
+   * static final JsonNodeAdapter node = JsonNodeAdapter.builder().build();
+   *
+   * JsonArray arrayFromJson = node.fromJson(JsonArray.class, jsonContent);
+   * }
+ */ + T fromJson(Class type, String json); + + /** + * Create a JsonAdapter for the given generic type or null if the + * type is not actually a JsonNode type. + */ + JsonAdapter create(Type type); + + /** + * Build the JsonNodeAdapter. + */ + interface Builder { + + /** + * Set the default JsonStream to use when using {@link JsonNodeAdapter#toJson(JsonNode)} + * {@link JsonNodeAdapter#fromJson(String)}. + *

+ * When not set this defaults to {@code JsonStream.builder().build()}. + * + * @see JsonStream#builder() + */ + Builder jsonStream(JsonStream jsonStream); + + /** + * Set the adapter to use when reading {@link JsonNode.Type#NUMBER}. + *

+ * The default will read as a double and test for the value being an + * integral returning a long if is. + */ + Builder numberAdapter(JsonAdapter numberAdapter); + + /** + * Build and return the JsonNodeAdapter. + */ + JsonNodeAdapter build(); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonNumber.java b/json-node/src/main/java/io/avaje/json/node/JsonNumber.java new file mode 100644 index 00000000..48178ed6 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonNumber.java @@ -0,0 +1,41 @@ +package io.avaje.json.node; + +import io.avaje.json.JsonWriter; + +import java.math.BigDecimal; + +/** + * JsonNode Number type. + */ +public interface JsonNumber extends JsonNode { + + /** + * Return the int value for the number. + */ + int intValue(); + + /** + * Return the long value for the number. + */ + long longValue(); + + /** + * Return the double value for the number. + */ + double doubleValue(); + + /** + * Return the decimal value for the number. + */ + BigDecimal decimalValue(); + + /** + * Return the number. + */ + Number numberValue(); + + /** + * Write the value of this node to the writer. + */ + void toJson(JsonWriter writer); +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonObject.java b/json-node/src/main/java/io/avaje/json/node/JsonObject.java new file mode 100644 index 00000000..56bf3f07 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonObject.java @@ -0,0 +1,66 @@ +package io.avaje.json.node; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +/** + * JSON Object type. + */ +public final class JsonObject implements JsonNode { + + private final Map children; + + /** + * Create a new mutable JsonObject to add elements to. + */ + public static JsonObject create() { + return new JsonObject(new LinkedHashMap<>()); + } + + /** + * Create a unmodifiable JsonObject with the given elements. + */ + public static JsonObject of(Map elements) { + return new JsonObject(Collections.unmodifiableMap(elements)); + } + + private JsonObject(Map children) { + this.children = requireNonNull(children); + } + + @Override + public Type type() { + return Type.OBJECT; + } + + @Override + public String text() { + return children.toString(); + } + + /** + * Return the elements of the object. + */ + public Map elements() { + return children; + } + + /** + * Add an element to the json object. + * + * @param key The key for the element + * @param value The value for the element. + * @return This JsonObject for fluid use. + */ + public JsonObject add(String key, JsonNode value) { + children.put(key, value); + return this; + } + +// public JsonNode put(String key, JsonNode value) { +// return children.put(key, value); +// } +} diff --git a/json-node/src/main/java/io/avaje/json/node/JsonString.java b/json-node/src/main/java/io/avaje/json/node/JsonString.java new file mode 100644 index 00000000..5584b1ec --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/JsonString.java @@ -0,0 +1,24 @@ +package io.avaje.json.node; + +public final class JsonString implements JsonNode { + + private final String value; + + public static JsonString of(String value) { + return new JsonString(value); + } + + private JsonString(String value) { + this.value = value; + } + + @Override + public Type type() { + return Type.STRING; + } + + @Override + public String text() { + return value; + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/ArrayAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/ArrayAdapter.java new file mode 100644 index 00000000..0d9fe367 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/ArrayAdapter.java @@ -0,0 +1,44 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonArray; +import io.avaje.json.node.JsonNode; + +import java.util.ArrayList; +import java.util.List; + +final class ArrayAdapter implements JsonAdapter { + + private final JsonAdapter elementAdapter; + + ArrayAdapter(JsonAdapter elementAdapter) { + this.elementAdapter = elementAdapter; + } + + @Override + public JsonArray fromJson(JsonReader reader) { + List result = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNextElement()) { + result.add(elementAdapter.fromJson(reader)); + } + reader.endArray(); + return JsonArray.of(result); + } + + @Override + public void toJson(JsonWriter writer, JsonArray value) { + if (value.isEmpty()) { + writer.emptyArray(); + return; + } + writer.beginArray(); + for (JsonNode element : value.elements()) { + elementAdapter.toJson(writer, element); + } + writer.endArray(); + } + +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/BooleanAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/BooleanAdapter.java new file mode 100644 index 00000000..ece62fb7 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/BooleanAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonBoolean; + +final class BooleanAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonBoolean node) { + writer.value(node.value()); + } + + @Override + public JsonBoolean fromJson(JsonReader reader) { + return JsonBoolean.of(reader.readBoolean()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/DJsonNodeAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/DJsonNodeAdapter.java new file mode 100644 index 00000000..0d5350c1 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/DJsonNodeAdapter.java @@ -0,0 +1,82 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.node.*; +import io.avaje.json.stream.JsonStream; + +import java.lang.reflect.Type; + +final class DJsonNodeAdapter implements JsonNodeAdapter { + + static final BooleanAdapter BOOLEAN_ADAPTER = new BooleanAdapter(); + static final StringAdapter STRING_ADAPTER = new StringAdapter(); + static final IntegerAdapter INTEGER_ADAPTER = new IntegerAdapter(); + static final LongAdapter LONG_ADAPTER = new LongAdapter(); + static final DoubleAdapter DOUBLE_ADAPTER = new DoubleAdapter(); + static final DecimalAdapter DECIMAL_ADAPTER = new DecimalAdapter(); + static final NumberAdapter NUMBER_ADAPTER = new NumberAdapter(); + + private final JsonStream jsonStream; + private final NodeAdapter nodeAdapter; + private final ObjectAdapter objectAdapter; + private final ArrayAdapter arrayAdapter; + + DJsonNodeAdapter(JsonStream jsonStream, NodeAdapter nodeAdapter, ObjectAdapter objectAdapter, ArrayAdapter arrayAdapter) { + this.jsonStream = jsonStream; + this.nodeAdapter = nodeAdapter; + this.objectAdapter = objectAdapter; + this.arrayAdapter = arrayAdapter; + } + + @Override + public String toJson(JsonNode node) { + final var writer = jsonStream.bufferedWriter(); + nodeAdapter.toJson(writer, node); + return writer.result(); + } + + @Override + public JsonNode fromJson(String json) { + try (JsonReader reader = jsonStream.reader(json)) { + return nodeAdapter.fromJson(reader); + } + } + + @Override + public T fromJson(Class type, String json) { + JsonAdapter adapter = of(type); + try (JsonReader reader = jsonStream.reader(json)) { + return adapter.fromJson(reader); + } + } + + @SuppressWarnings("unchecked") + @Override + public JsonAdapter create(Type type) { + if (type instanceof Class) { + Class cls = (Class) type; + if (JsonNode.class.isAssignableFrom(cls)) { + return of((Class)cls); + } + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public JsonAdapter of(Class type) { + if (type == JsonNode.class) return (JsonAdapter) nodeAdapter; + if (type == JsonObject.class) return (JsonAdapter) objectAdapter; + if (type == JsonArray.class) return (JsonAdapter) arrayAdapter; + if (type == JsonBoolean.class) return (JsonAdapter) BOOLEAN_ADAPTER; + if (type == JsonString.class) return (JsonAdapter) STRING_ADAPTER; + if (type == JsonInteger.class) return (JsonAdapter) INTEGER_ADAPTER; + if (type == JsonLong.class) return (JsonAdapter) LONG_ADAPTER; + if (type == JsonDouble.class) return (JsonAdapter) DOUBLE_ADAPTER; + if (type == JsonDecimal.class) return (JsonAdapter) DECIMAL_ADAPTER; + if (type == JsonNumber.class) return (JsonAdapter) NUMBER_ADAPTER; + + throw new IllegalArgumentException("Unexpected type " + type + " is not a JsonNode?"); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java new file mode 100644 index 00000000..d7025160 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/DecimalAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonLong; + +final class DecimalAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonLong value) { + writer.value(value.longValue()); + } + + @Override + public JsonLong fromJson(JsonReader reader) { + return JsonLong.of(reader.readLong()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/DoubleAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/DoubleAdapter.java new file mode 100644 index 00000000..ad7420e0 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/DoubleAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonDouble; + +final class DoubleAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonDouble value) { + writer.value(value.doubleValue()); + } + + @Override + public JsonDouble fromJson(JsonReader reader) { + return JsonDouble.of(reader.readDouble()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/IntegerAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/IntegerAdapter.java new file mode 100644 index 00000000..6554868f --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/IntegerAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonInteger; + +final class IntegerAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonInteger value) { + writer.value(value.intValue()); + } + + @Override + public JsonInteger fromJson(JsonReader reader) { + return JsonInteger.of(reader.readInt()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/LongAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/LongAdapter.java new file mode 100644 index 00000000..d922c6e2 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/LongAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonLong; + +final class LongAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonLong value) { + writer.value(value.longValue()); + } + + @Override + public JsonLong fromJson(JsonReader reader) { + return JsonLong.of(reader.readLong()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java new file mode 100644 index 00000000..66e8e10b --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapter.java @@ -0,0 +1,87 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.*; + +final class NodeAdapter implements JsonAdapter { + + private final BooleanAdapter booleanAdapter; + private final StringAdapter stringAdapter; + private final ArrayAdapter arrayAdapter; + private final ObjectAdapter objectAdapter; + private final JsonAdapter numberAdapter; + + NodeAdapter(JsonAdapter numberAdapter) { + this.booleanAdapter = DJsonNodeAdapter.BOOLEAN_ADAPTER; + this.stringAdapter = DJsonNodeAdapter.STRING_ADAPTER; + this.numberAdapter = numberAdapter; + this.arrayAdapter = new ArrayAdapter(this); + this.objectAdapter = new ObjectAdapter(this); + } + + ArrayAdapter arrayAdapter() { + return arrayAdapter; + } + + ObjectAdapter objectAdapter() { + return objectAdapter; + } + + @Override + public JsonNode fromJson(JsonReader reader) { + switch (reader.currentToken()) { + case NULL: + return null; + case BEGIN_ARRAY: + return arrayAdapter.fromJson(reader); + case BEGIN_OBJECT: + return objectAdapter.fromJson(reader); + case STRING: + return stringAdapter.fromJson(reader); + case BOOLEAN: + return booleanAdapter.fromJson(reader); + case NUMBER: + return numberAdapter.fromJson(reader); + default: + throw new IllegalStateException("Expected a value but was " + reader.currentToken() + " at path " + reader.location()); + } + } + + @Override + public void toJson(JsonWriter writer, JsonNode value) { + if (value == null) { + writer.nullValue(); + return; + } + JsonNode.Type type = value.type(); + switch (type) { + case NULL: + writer.nullValue(); + break; + case ARRAY: + arrayAdapter.toJson(writer, (JsonArray)value); + break; + case OBJECT: + objectAdapter.toJson(writer, (JsonObject) value); + break; + case BOOLEAN: + booleanAdapter.toJson(writer, (JsonBoolean) value); + break; + case STRING: + stringAdapter.toJson(writer, (JsonString)value); + break; + case NUMBER: + numberAdapter.toJson(writer, (JsonNumber)value); + break; + default: + throw new UnsupportedOperationException("Type not supported " + value.getClass()); + } + } + + @Override + public String toString() { + return "JsonNodeAdapter"; + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapterBuilder.java b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapterBuilder.java new file mode 100644 index 00000000..f1919a48 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/NodeAdapterBuilder.java @@ -0,0 +1,38 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.node.JsonNodeAdapter; +import io.avaje.json.node.JsonNumber; +import io.avaje.json.stream.JsonStream; + +/** + * Builder for JsonNodeAdapter. + */ +public final class NodeAdapterBuilder implements JsonNodeAdapter.Builder { + + private JsonStream jsonStream; + private JsonAdapter numberAdapter; + + @Override + public JsonNodeAdapter.Builder jsonStream(JsonStream jsonStream) { + this.jsonStream = jsonStream; + return this; + } + + @Override + public JsonNodeAdapter.Builder numberAdapter(JsonAdapter numberAdapter) { + this.numberAdapter = numberAdapter; + return this; + } + + @Override + public JsonNodeAdapter build() { + final var stream = jsonStream != null ? jsonStream : JsonStream.builder().build(); + final var number = numberAdapter != null ? numberAdapter : DJsonNodeAdapter.NUMBER_ADAPTER; + final var nodeAdapter = new NodeAdapter(number); + final var objectAdapter = nodeAdapter.objectAdapter(); + final var arrayAdapter = nodeAdapter.arrayAdapter(); + + return new DJsonNodeAdapter(stream, nodeAdapter, objectAdapter, arrayAdapter); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/NumberAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/NumberAdapter.java new file mode 100644 index 00000000..11ef4e6e --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/NumberAdapter.java @@ -0,0 +1,26 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonDouble; +import io.avaje.json.node.JsonLong; +import io.avaje.json.node.JsonNumber; + +final class NumberAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonNumber value) { + value.toJson(writer); + } + + @Override + public JsonNumber fromJson(JsonReader reader) { + // read unknown number type + double d = reader.readDouble(); + if (d % 1 == 0) { + return JsonLong.of((long) d); + } + return JsonDouble.of(d); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/ObjectAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/ObjectAdapter.java new file mode 100644 index 00000000..65c59563 --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/ObjectAdapter.java @@ -0,0 +1,57 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonDataException; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonNode; +import io.avaje.json.node.JsonObject; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Converts maps with string keys to JSON objects. + */ +final class ObjectAdapter implements JsonAdapter { + + private final JsonAdapter valueAdapter; + + ObjectAdapter(JsonAdapter valueAdapter) { + this.valueAdapter = valueAdapter; + } + + @Override + public void toJson(JsonWriter writer, JsonObject value) { + writer.beginObject(); + for (var entry : value.elements().entrySet()) { + if (entry.getKey() == null) { + throw new JsonDataException("Map key is null at " + writer.path()); + } + writer.name(entry.getKey()); + valueAdapter.toJson(writer, entry.getValue()); + } + writer.endObject(); + } + + @Override + public JsonObject fromJson(JsonReader reader) { + Map result = new LinkedHashMap<>(); + reader.beginObject(); + while (reader.hasNextField()) { + String name = reader.nextField(); + JsonNode value = valueAdapter.fromJson(reader); + JsonNode replaced = result.put(name, value); + if (replaced != null) { + throw new JsonDataException(String.format("Map key '%s' has multiple values at path %s : %s and %s", name, reader.location(), replaced, value)); + } + } + reader.endObject(); + return JsonObject.of(result); + } + + @Override + public String toString() { + return "JsonObject()"; + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/adapter/StringAdapter.java b/json-node/src/main/java/io/avaje/json/node/adapter/StringAdapter.java new file mode 100644 index 00000000..99d4e1bb --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/adapter/StringAdapter.java @@ -0,0 +1,19 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.JsonWriter; +import io.avaje.json.node.JsonString; + +final class StringAdapter implements JsonAdapter { + + @Override + public void toJson(JsonWriter writer, JsonString value) { + writer.value(value.text()); + } + + @Override + public JsonString fromJson(JsonReader reader) { + return JsonString.of(reader.readString()); + } +} diff --git a/json-node/src/main/java/io/avaje/json/node/package-info.java b/json-node/src/main/java/io/avaje/json/node/package-info.java new file mode 100644 index 00000000..a3ef81ef --- /dev/null +++ b/json-node/src/main/java/io/avaje/json/node/package-info.java @@ -0,0 +1,32 @@ +/** + * JsonNode types and adapters. + * + *

JsonNodeAdapter

+ *

+ * Create a JsonNodeAdapter using default settings. This instance is thread safe, + * ideally one instance is created and used for all JsonNode use. + * + *

{@code
+ *
+ * static final JsonNodeAdapter jsonNodeAdapter =
+ *     JsonNodeAdapter
+ *       .builder()
+ *       .build();
+ *
+ * }
+ * + *

toJson fromJson

+ *

+ * JsonNodeAdapter provides helper method for {@code toJson()} and {@code fromJson()}. + * + *

{@code
+ *
+ * var jsonObject = JsonObject.create()
+ *       .add("name", JsonString.of("foo"))
+ *       .add("other", JsonInteger.of(42));
+ *
+ * String asJson = jsonNodeAdapter.toJson(jsonObject);
+ *
+ * }
+ */ +package io.avaje.json.node; diff --git a/json-node/src/main/java/module-info.java b/json-node/src/main/java/module-info.java new file mode 100644 index 00000000..0fb973d8 --- /dev/null +++ b/json-node/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module io.avaje.json.node { + + exports io.avaje.json.node; + + requires transitive io.avaje.json; +} diff --git a/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java new file mode 100644 index 00000000..701004de --- /dev/null +++ b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java @@ -0,0 +1,145 @@ +package io.avaje.json.node.adapter; + +import io.avaje.json.JsonAdapter; +import io.avaje.json.JsonReader; +import io.avaje.json.node.*; +import io.avaje.json.stream.JsonStream; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonNodeAdaptersTest { + + static final JsonNodeAdapter node = JsonNodeAdapter.builder().build(); + + static final JsonStream stream = JsonStream.builder().build(); + static final JsonAdapter nodeAdapter = node.of(JsonNode.class); + + @Test + void create_expect_null() { + assertThat(node.create(LocalDate.class)).isNull(); + } + + @Test + void create_JsonNode_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonNode.class); + JsonAdapter adapter = node.of(JsonNode.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonObject_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonObject.class); + JsonAdapter adapter = node.of(JsonObject.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonArray_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonArray.class); + JsonAdapter adapter = node.of(JsonArray.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonInteger_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonInteger.class); + JsonAdapter adapter = node.of(JsonInteger.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonLong_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonLong.class); + JsonAdapter adapter = node.of(JsonLong.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonDouble_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonDouble.class); + JsonAdapter adapter = node.of(JsonDouble.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonDecimal_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonDecimal.class); + JsonAdapter adapter = node.of(JsonDecimal.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonNumber_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonNumber.class); + JsonAdapter adapter = node.of(JsonNumber.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + @Test + void create_JsonBoolean_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonBoolean.class); + JsonAdapter adapter = node.of(JsonBoolean.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + + @Test + void create_JsonString_expect_sameInstance() { + JsonAdapter jsonAdapter = node.create(JsonString.class); + JsonAdapter adapter = node.of(JsonString.class); + assertThat(jsonAdapter).isSameAs(adapter); + } + + + @Test + void arrayCreateOfMixed_defaultStream() { + JsonArray jsonArray = JsonArray.create() + .add(JsonInteger.of(42)) + .add(JsonString.of("foo")); + + var asJson = node.toJson(jsonArray); + assertThat(asJson).isEqualTo("[42,\"foo\"]"); + + JsonArray arrayFromJson = node.fromJson(JsonArray.class, asJson); + assertThat(arrayFromJson.elements()).hasSize(2); + + JsonNode jsonNodeFromJson = node.fromJson(asJson); + assertThat(jsonNodeFromJson).isInstanceOf(JsonArray.class); + } + + @Test + void arrayOfMixed_explicitUseOfStream() { + JsonArray jsonArray = JsonArray.of(List.of(JsonInteger.of(42), JsonString.of("foo"))); + + var writer = stream.bufferedWriter(); + nodeAdapter.toJson(writer, jsonArray); + var asJson = writer.result(); + assertThat(asJson).isEqualTo("[42,\"foo\"]"); + + JsonReader reader = stream.reader(asJson); + JsonNode fromJsonNode = nodeAdapter.fromJson(reader); + assertThat(fromJsonNode).isInstanceOf(JsonArray.class); + } + + @Test + void object() { + var obj = JsonObject.create() + .add("name", JsonString.of("foo")) + .add("other", JsonInteger.of(42)); + + String asJson0 = node.toJson(obj); + assertThat(asJson0).isEqualTo("{\"name\":\"foo\",\"other\":42}"); + + JsonObject jsonObjectFromJson = node.fromJson(JsonObject.class, asJson0); + assertThat(jsonObjectFromJson.elements()).containsKeys("name", "other"); + + var writer = stream.bufferedWriter(); + nodeAdapter.toJson(writer, obj); + var asJson = writer.result(); + assertThat(asJson).isEqualTo("{\"name\":\"foo\",\"other\":42}"); + } +} diff --git a/pom.xml b/pom.xml index afdda7d2..f76bcdab 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ json-core + json-node jsonb jsonb-generator jsonb-jackson From c4ad74bc00f31f7586cd7ddf2b4ff92e15a1e3a5 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 12 Dec 2024 11:38:24 +1300 Subject: [PATCH 2/3] [json-code] size() and isEmpty() for JsonArray and JsonObject --- .../java/io/avaje/json/node/JsonArray.java | 21 +++++++++----- .../java/io/avaje/json/node/JsonObject.java | 29 +++++++++++++++++-- .../java/io/avaje/json/node/JsonString.java | 4 +++ .../node/adapter/JsonNodeAdaptersTest.java | 8 +++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/json-node/src/main/java/io/avaje/json/node/JsonArray.java b/json-node/src/main/java/io/avaje/json/node/JsonArray.java index 95e05194..27f60a28 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonArray.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonArray.java @@ -27,7 +27,7 @@ public static JsonArray of(List children) { return new JsonArray(Collections.unmodifiableList(children)); } - JsonArray(List children) { + private JsonArray(List children) { this.children = requireNonNull(children); } @@ -42,17 +42,24 @@ public String text() { } /** - * Return the child elements. + * Return true if the json array is empty. */ - public List elements() { - return children; + public boolean isEmpty() { + return children.isEmpty(); } /** - * Return true if the json array is empty. + * Return the number of elements. */ - public boolean isEmpty() { - return children.isEmpty(); + public int size() { + return children.size(); + } + + /** + * Return the child elements. + */ + public List elements() { + return children; } /** diff --git a/json-node/src/main/java/io/avaje/json/node/JsonObject.java b/json-node/src/main/java/io/avaje/json/node/JsonObject.java index 56bf3f07..974a00e4 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonObject.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonObject.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import static java.util.Objects.requireNonNull; @@ -41,6 +42,27 @@ public String text() { return children.toString(); } + /** + * Return true if the json object contains no elements. + */ + public boolean isEmpty() { + return children.isEmpty(); + } + + /** + * Return the number of elements. + */ + public int size() { + return children.size(); + } + + /** + * Return true if the object contains the given key. + */ + public boolean containsKey(String key) { + return children.containsKey(key); + } + /** * Return the elements of the object. */ @@ -60,7 +82,8 @@ public JsonObject add(String key, JsonNode value) { return this; } -// public JsonNode put(String key, JsonNode value) { -// return children.put(key, value); -// } + public Optional get(String key) { + return Optional.ofNullable(children.get(key)); + } + } diff --git a/json-node/src/main/java/io/avaje/json/node/JsonString.java b/json-node/src/main/java/io/avaje/json/node/JsonString.java index 5584b1ec..eee9d67c 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonString.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonString.java @@ -21,4 +21,8 @@ public Type type() { public String text() { return value; } + + public String value() { + return value; + } } diff --git a/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java index 701004de..97b1878c 100644 --- a/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java +++ b/json-node/src/test/java/io/avaje/json/node/adapter/JsonNodeAdaptersTest.java @@ -115,6 +115,9 @@ void arrayCreateOfMixed_defaultStream() { void arrayOfMixed_explicitUseOfStream() { JsonArray jsonArray = JsonArray.of(List.of(JsonInteger.of(42), JsonString.of("foo"))); + assertThat(jsonArray.isEmpty()).isFalse(); + assertThat(jsonArray.size()).isEqualTo(2); + var writer = stream.bufferedWriter(); nodeAdapter.toJson(writer, jsonArray); var asJson = writer.result(); @@ -131,6 +134,11 @@ void object() { .add("name", JsonString.of("foo")) .add("other", JsonInteger.of(42)); + assertThat(obj.isEmpty()).isFalse(); + assertThat(obj.size()).isEqualTo(2); + assertThat(obj.containsKey("name")).isTrue(); + assertThat(obj.containsKey("DoesNotExist")).isFalse(); + String asJson0 = node.toJson(obj); assertThat(asJson0).isEqualTo("{\"name\":\"foo\",\"other\":42}"); From c3fa861822b378f0caa119a1008c23e5bb43e1f2 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 12 Dec 2024 12:06:38 +1300 Subject: [PATCH 3/3] Add toString() methods --- .../java/io/avaje/json/node/JsonArray.java | 5 ++ .../java/io/avaje/json/node/JsonBoolean.java | 5 ++ .../java/io/avaje/json/node/JsonDecimal.java | 5 ++ .../java/io/avaje/json/node/JsonDouble.java | 5 ++ .../java/io/avaje/json/node/JsonInteger.java | 5 ++ .../java/io/avaje/json/node/JsonLong.java | 5 ++ .../java/io/avaje/json/node/JsonObject.java | 5 ++ .../java/io/avaje/json/node/JsonString.java | 5 ++ .../io/avaje/json/node/JsonObjectTest.java | 86 +++++++++++++++++++ 9 files changed, 126 insertions(+) create mode 100644 json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java diff --git a/json-node/src/main/java/io/avaje/json/node/JsonArray.java b/json-node/src/main/java/io/avaje/json/node/JsonArray.java index 27f60a28..77b497bd 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonArray.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonArray.java @@ -31,6 +31,11 @@ private JsonArray(List children) { this.children = requireNonNull(children); } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.ARRAY; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java b/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java index 0bad10b5..cc7bce5c 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonBoolean.java @@ -12,6 +12,11 @@ private JsonBoolean(boolean value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.BOOLEAN; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java b/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java index f2601a8f..587aeae4 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonDecimal.java @@ -16,6 +16,11 @@ private JsonDecimal(BigDecimal value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.NUMBER; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonDouble.java b/json-node/src/main/java/io/avaje/json/node/JsonDouble.java index c46cc855..1760e92b 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonDouble.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonDouble.java @@ -16,6 +16,11 @@ private JsonDouble(double value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.NUMBER; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonInteger.java b/json-node/src/main/java/io/avaje/json/node/JsonInteger.java index 3c5ebafe..9f8994bf 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonInteger.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonInteger.java @@ -16,6 +16,11 @@ private JsonInteger(int value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.NUMBER; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonLong.java b/json-node/src/main/java/io/avaje/json/node/JsonLong.java index e98b86f3..c5bbd60e 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonLong.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonLong.java @@ -16,6 +16,11 @@ private JsonLong(long value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.NUMBER; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonObject.java b/json-node/src/main/java/io/avaje/json/node/JsonObject.java index 974a00e4..3a4163e9 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonObject.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonObject.java @@ -32,6 +32,11 @@ private JsonObject(Map children) { this.children = requireNonNull(children); } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.OBJECT; diff --git a/json-node/src/main/java/io/avaje/json/node/JsonString.java b/json-node/src/main/java/io/avaje/json/node/JsonString.java index eee9d67c..5cb4a88c 100644 --- a/json-node/src/main/java/io/avaje/json/node/JsonString.java +++ b/json-node/src/main/java/io/avaje/json/node/JsonString.java @@ -12,6 +12,11 @@ private JsonString(String value) { this.value = value; } + @Override + public String toString() { + return text(); + } + @Override public Type type() { return Type.STRING; diff --git a/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java new file mode 100644 index 00000000..2fd62e92 --- /dev/null +++ b/json-node/src/test/java/io/avaje/json/node/JsonObjectTest.java @@ -0,0 +1,86 @@ +package io.avaje.json.node; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonObjectTest { + + final JsonObject basicObject = JsonObject.create() + .add("name", JsonString.of("foo")) + .add("other", JsonInteger.of(42)); + + final JsonObject emptyObject = JsonObject.create(); + + @Test + void of() { + Map map = new LinkedHashMap<>(); + map.put("name", JsonString.of("foo")); + map.put("other", JsonInteger.of(42)); + + JsonObject immutableJsonObject = JsonObject.of(map); + assertThat(immutableJsonObject.elements()).containsOnlyKeys("name", "other"); + } + + @Test + void type() { + assertThat(emptyObject.type()).isEqualTo(JsonNode.Type.OBJECT); + } + + @Test + void text() { + assertThat(emptyObject.text()).isEqualTo("{}"); + assertThat(basicObject.text()).isEqualTo("{name=foo, other=42}"); + } + + @Test + void isEmpty() { + assertThat(basicObject.isEmpty()).isFalse(); + assertThat(emptyObject.isEmpty()).isTrue(); + } + + @Test + void size() { + assertThat(basicObject.size()).isEqualTo(2); + assertThat(emptyObject.size()).isEqualTo(0); + } + + @Test + void containsKey() { + assertThat(basicObject.containsKey("name")).isTrue(); + assertThat(basicObject.containsKey("DoesNotExist")).isFalse(); + assertThat(emptyObject.containsKey("DoesNotExist")).isFalse(); + } + + @Test + void elements() { + assertThat(emptyObject.elements()).isInstanceOf(Map.class); + assertThat(emptyObject.elements()).isEmpty(); + assertThat(basicObject.elements()).isInstanceOf(Map.class); + assertThat(basicObject.elements()).hasSize(2); + } + + @Test + void add() { + var obj = JsonObject.create() + .add("name", JsonString.of("foo")); + assertThat(obj.containsKey("name")).isTrue(); + assertThat(obj.size()).isEqualTo(1); + } + + @Test + void get() { + Optional name = basicObject.get("name"); + assertThat(name).isNotEmpty(); + String nameVal = name.stream() + .map(JsonNode::text) + .findFirst() + .orElse("NotPresent"); + + assertThat(nameVal).isEqualTo("foo"); + } +}