From 82184e7cd2649c6edc5c350a0d04175925d08cb8 Mon Sep 17 00:00:00 2001 From: OmniaGM Date: Fri, 21 Apr 2023 12:55:28 +0100 Subject: [PATCH] KAFKA-14737: Move kafka.utils.json to server-common --- build.gradle | 1 + checkstyle/import-control-server-common.xml | 2 + .../org/apache/kafka/server/util/Json.java | 107 ++++++++ .../kafka/server/util/json/DecodeJson.java | 127 ++++++++++ .../kafka/server/util/json/JsonArray.java | 67 +++++ .../kafka/server/util/json/JsonObject.java | 82 ++++++ .../kafka/server/util/json/JsonValue.java | 130 ++++++++++ .../apache/kafka/server/util/JsonTest.java | 233 ++++++++++++++++++ 8 files changed, 749 insertions(+) create mode 100644 server-common/src/main/java/org/apache/kafka/server/util/Json.java create mode 100644 server-common/src/main/java/org/apache/kafka/server/util/json/DecodeJson.java create mode 100644 server-common/src/main/java/org/apache/kafka/server/util/json/JsonArray.java create mode 100644 server-common/src/main/java/org/apache/kafka/server/util/json/JsonObject.java create mode 100644 server-common/src/main/java/org/apache/kafka/server/util/json/JsonValue.java create mode 100644 server-common/src/test/java/org/apache/kafka/server/util/JsonTest.java diff --git a/build.gradle b/build.gradle index 63a4f577370de..7dfea2bf5165e 100644 --- a/build.gradle +++ b/build.gradle @@ -1569,6 +1569,7 @@ project(':server-common') { implementation libs.slf4jApi implementation libs.metrics implementation libs.joptSimple + implementation libs.jacksonDatabind implementation libs.pcollections testImplementation project(':clients') diff --git a/checkstyle/import-control-server-common.xml b/checkstyle/import-control-server-common.xml index 2e65ec601a395..088fce6e7403f 100644 --- a/checkstyle/import-control-server-common.xml +++ b/checkstyle/import-control-server-common.xml @@ -89,6 +89,8 @@ + + diff --git a/server-common/src/main/java/org/apache/kafka/server/util/Json.java b/server-common/src/main/java/org/apache/kafka/server/util/Json.java new file mode 100644 index 0000000000000..3a2922838a34a --- /dev/null +++ b/server-common/src/main/java/org/apache/kafka/server/util/Json.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kafka.server.util; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; +import org.apache.kafka.server.util.json.JsonValue; + +import java.io.IOException; +import java.util.Optional; + +/** + * Provides methods for parsing JSON with Jackson and encoding to JSON with a simple and naive custom implementation. + */ +public final class Json { + private static ObjectMapper mapper = new ObjectMapper(); + + /** + * Parse a JSON string into a JsonValue if possible. `None` is returned if `input` is not valid JSON. + */ + public static Optional parseFull(String input) { + try { + return Optional.ofNullable(tryParseFull(input)); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } + + /** + * Parse a JSON string into a generic type T, or throw JsonProcessingException in the case of + * exception. + */ + public static T parseStringAs(String input, Class clazz) throws JsonProcessingException { + return mapper.readValue(input, clazz); + } + + /** + * Parse a JSON byte array into a JsonValue if possible. `None` is returned if `input` is not valid JSON. + */ + public static Optional parseBytes(byte[] input) throws IOException { + try { + return Optional.ofNullable(mapper.readTree(input)).map(JsonValue::apply); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } + + public static JsonValue tryParseBytes(byte[] input) throws IOException { + return JsonValue.apply(mapper.readTree(input)); + } + + /** + * Parse a JSON byte array into a generic type T, or throws a JsonProcessingException in the case of exception. + */ + public static T parseBytesAs(byte[] input, Class clazz) throws IOException { + return mapper.readValue(input, clazz); + } + + /** + * Parse a JSON string into a JsonValue if possible. + * @param input a JSON string to parse + * @return the actual json value. + * @throws JsonProcessingException if failed to parse + */ + public static JsonValue tryParseFull(String input) throws JsonProcessingException { + if (input == null || input.isEmpty()) { + throw new JsonParseException(MissingNode.getInstance().traverse(), "The input string shouldn't be empty"); + } else { + return JsonValue.apply(mapper.readTree(input)); + } + } + + /** + * Encode an object into a JSON string. This method accepts any type supported by Jackson's ObjectMapper in + * the default configuration. That is, Java collections are supported, but Scala collections are not (to avoid + * a jackson-scala dependency). + */ + public static String encodeAsString(Object obj) throws JsonProcessingException { + return mapper.writeValueAsString(obj); + } + + /** + * Encode an object into a JSON value in bytes. This method accepts any type supported by Jackson's ObjectMapper in + * the default configuration. That is, Java collections are supported, but Scala collections are not (to avoid + * a jackson-scala dependency). + */ + public static byte[] encodeAsBytes(Object obj) throws JsonProcessingException { + return mapper.writeValueAsBytes(obj); + } +} diff --git a/server-common/src/main/java/org/apache/kafka/server/util/json/DecodeJson.java b/server-common/src/main/java/org/apache/kafka/server/util/json/DecodeJson.java new file mode 100644 index 0000000000000..dbc28548a3427 --- /dev/null +++ b/server-common/src/main/java/org/apache/kafka/server/util/json/DecodeJson.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kafka.server.util.json; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface DecodeJson { + /** + * Decode the JSON node provided into an instance of `T`. + * + * @throws JsonMappingException if `node` cannot be decoded into `T`. + */ + T decode(JsonNode node) throws JsonMappingException; + + static JsonMappingException throwJsonMappingException(String expectedType, JsonNode node) { + return new JsonMappingException(null, String.format("Expected `%s` value, received %s", expectedType, node)); + } + + final class DecodeBoolean implements DecodeJson { + @Override + public Boolean decode(JsonNode node) throws JsonMappingException { + if (node.isBoolean()) { + return node.booleanValue(); + } + throw throwJsonMappingException(Boolean.class.getSimpleName(), node); + } + } + + final class DecodeDouble implements DecodeJson { + @Override + public Double decode(JsonNode node) throws JsonMappingException { + if (node.isDouble() || node.isLong() || node.isInt()) { + return node.doubleValue(); + } + throw throwJsonMappingException(Double.class.getSimpleName(), node); + } + } + + final class DecodeInteger implements DecodeJson { + @Override + public Integer decode(JsonNode node) throws JsonMappingException { + if (node.isInt()) { + return node.intValue(); + } + throw throwJsonMappingException(Integer.class.getSimpleName(), node); + } + } + + final class DecodeLong implements DecodeJson { + @Override + public Long decode(JsonNode node) throws JsonMappingException { + if (node.isLong() || node.isInt()) { + return node.longValue(); + } + throw throwJsonMappingException(Long.class.getSimpleName(), node); + } + } + + final class DecodeString implements DecodeJson { + @Override + public String decode(JsonNode node) throws JsonMappingException { + if (node.isTextual()) { + return node.textValue(); + } + throw throwJsonMappingException(String.class.getSimpleName(), node); + } + } + + static DecodeJson> decodeOptional(DecodeJson decodeJson) { + return node -> { + if (node.isNull()) return Optional.empty(); + return Optional.of(decodeJson.decode(node)); + }; + } + + static DecodeJson> decodeList(DecodeJson decodeJson) throws JsonMappingException { + return node -> { + if (node.isArray()) { + List result = new ArrayList<>(); + Iterator elements = node.elements(); + while (elements.hasNext()) { + result.add(decodeJson.decode(elements.next())); + } + return result; + } + throw throwJsonMappingException("JSON array", node); + }; + } + + static DecodeJson> decodeMap(DecodeJson decodeJson) throws JsonMappingException { + return node -> { + if (node.isObject()) { + Map result = new HashMap<>(); + Iterator> elements = node.fields(); + while (elements.hasNext()) { + Map.Entry next = elements.next(); + result.put(next.getKey(), decodeJson.decode(next.getValue())); + } + return result; + } + throw throwJsonMappingException("JSON object", node); + }; + } +} diff --git a/server-common/src/main/java/org/apache/kafka/server/util/json/JsonArray.java b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonArray.java new file mode 100644 index 0000000000000..1f1c0287eecd9 --- /dev/null +++ b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonArray.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kafka.server.util.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class JsonArray implements JsonValue { + protected final ArrayNode node; + + JsonArray(ArrayNode node) { + this.node = node; + } + + @Override + public JsonNode node() { + return node; + } + + public Iterator iterator() { + Stream nodeStream = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(node.elements(), Spliterator.ORDERED), + false); + Stream results = nodeStream.map(node -> JsonValue.apply(node)); + return results.collect(Collectors.toList()).iterator(); + } + + @Override + public int hashCode() { + return node().hashCode(); + } + + @Override + public boolean equals(Object a) { + if (a instanceof JsonArray) { + return node().equals(((JsonArray) a).node()); + } + return false; + } + + @Override + public String toString() { + return node().toString(); + } +} diff --git a/server-common/src/main/java/org/apache/kafka/server/util/json/JsonObject.java b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonObject.java new file mode 100644 index 0000000000000..85bd5583f2a8d --- /dev/null +++ b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonObject.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kafka.server.util.json; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.AbstractMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class JsonObject implements JsonValue { + protected final ObjectNode node; + + JsonObject(ObjectNode node) { + this.node = node; + } + + @Override + public JsonNode node() { + return node; + } + + public JsonValue apply(String name) throws JsonMappingException { + return get(name).orElseThrow(() -> new JsonMappingException(null, "No such field exists: `" + name + "`")); + } + + public Optional get(String name) { + return Optional.ofNullable(node().get(name)).map(JsonValue::apply); + } + + public Iterator> iterator() { + Iterator> iterator = node.fields(); + Stream> stream = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); + + Stream> results = stream.map(entry -> + new AbstractMap.SimpleEntry<>(entry.getKey(), JsonValue.apply(entry.getValue())) + ); + return results.collect(Collectors.toList()).iterator(); + } + + @Override + public int hashCode() { + return node().hashCode(); + } + + @Override + public boolean equals(Object a) { + if (a instanceof JsonObject) { + return node().equals(((JsonObject) a).node()); + } + return false; + } + + @Override + public String toString() { + return node().toString(); + } +} diff --git a/server-common/src/main/java/org/apache/kafka/server/util/json/JsonValue.java b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonValue.java new file mode 100644 index 0000000000000..daf3b4c7d613c --- /dev/null +++ b/server-common/src/main/java/org/apache/kafka/server/util/json/JsonValue.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.kafka.server.util.json; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; + +/** + * A simple wrapper over Jackson's JsonNode that enables type safe parsing via the `DecodeJson` type + * class. + * + * Typical usage would be something like: + * + * {{{ + * val jsonNode: JsonNode = ??? + * val jsonObject = JsonValue(jsonNode).asJsonObject + * val intValue = jsonObject("int_field").to[Int] + * val optionLongValue = jsonObject("option_long_field").to[Option[Long]] + * val mapStringIntField = jsonObject("map_string_int_field").to[Map[String, Int]] + * val seqStringField = jsonObject("seq_string_field").to[Seq[String] + * }}} + * + * The `to` method throws an exception if the value cannot be converted to the requested type. + */ + +public interface JsonValue { + JsonNode node(); + + default T to(DecodeJson decodeJson) throws JsonMappingException { + return decodeJson.decode(node()); + } + + /** + * If this is a JSON object, return an instance of JsonObject. Otherwise, throw a JsonMappingException. + */ + default JsonObject asJsonObject() throws JsonMappingException { + return asJsonObjectOptional() + .orElseThrow(() -> new JsonMappingException(null, String.format("Expected JSON object, received %s", node()))); + } + + /** + * If this is a JSON object, return a JsonObject wrapped by a `Some`. Otherwise, return None. + */ + default Optional asJsonObjectOptional() { + if (this instanceof JsonObject) { + return Optional.of((JsonObject) this); + } else if (node() instanceof ObjectNode) { + return Optional.of(new JsonObject((ObjectNode) node())); + } else { + return Optional.empty(); + } + } + + /** + * If this is a JSON array, return an instance of JsonArray. Otherwise, throw a JsonMappingException. + */ + default JsonArray asJsonArray() throws JsonMappingException { + return asJsonArrayOptional() + .orElseThrow(() -> new JsonMappingException(null, String.format("Expected JSON array, received %s", node()))); + + } + + /** + * If this is a JSON array, return a JsonArray wrapped by a `Some`. Otherwise, return None. + */ + default Optional asJsonArrayOptional() { + if (this instanceof JsonArray) { + return Optional.of((JsonArray) this); + } else if (node() instanceof ArrayNode) { + return Optional.of(new JsonArray((ArrayNode) node())); + } else { + return Optional.empty(); + } + } + + static JsonValue apply(JsonNode node) { + if (node instanceof ObjectNode) { + return new JsonObject((ObjectNode) node); + } else if (node instanceof ArrayNode) { + return new JsonArray((ArrayNode) node); + } else { + return new BasicJsonValue(node); + } + } + + class BasicJsonValue implements JsonValue { + protected JsonNode node; + BasicJsonValue(JsonNode node) { + this.node = node; + } + + @Override + public JsonNode node() { + return node; + } + @Override + public int hashCode() { + return node().hashCode(); + } + @Override + public boolean equals(Object a) { + if (a instanceof BasicJsonValue) { + return node().equals(((BasicJsonValue) a).node()); + } + return false; + } + @Override + public String toString() { + return node().toString(); + } + } +} diff --git a/server-common/src/test/java/org/apache/kafka/server/util/JsonTest.java b/server-common/src/test/java/org/apache/kafka/server/util/JsonTest.java new file mode 100644 index 0000000000000..d4c25070768be --- /dev/null +++ b/server-common/src/test/java/org/apache/kafka/server/util/JsonTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.kafka.server.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.kafka.server.util.json.DecodeJson; +import org.apache.kafka.server.util.json.JsonObject; +import org.apache.kafka.server.util.json.JsonValue; +import org.junit.jupiter.api.Test; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JsonTest { + private static final String JSON = "{\n" + + " \"boolean\": false,\n" + + " \"int\": 1234,\n" + + " \"long\": 3000000000,\n" + + " \"double\": 16.244355,\n" + + " \"string\": \"string\",\n" + + " \"number_as_string\": \"123\",\n" + + " \"array\": [4.0, 11.1, 44.5],\n" + + " \"object\": {\n" + + " \"a\": true,\n" + + " \"b\": false\n" + + " },\n" + + " \"null\": null\n" + + "}"; + + private JsonValue parse(String s) { + return Json.parseFull(s).orElseThrow(() -> new RuntimeException("Failed to parse json: " + s)); + } + + @Test + public void testAsJsonObject() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + JsonValue obj = parsed.apply("object"); + assertEquals(obj, obj.asJsonObject()); + assertThrows(JsonMappingException.class, () -> parsed.apply("array").asJsonObject()); + } + + @Test + public void testAsJsonObjectOption() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + assertTrue(parsed.apply("object").asJsonObjectOptional().isPresent()); + assertEquals(Optional.empty(), parsed.apply("array").asJsonObjectOptional()); + } + + @Test + public void testAsJsonArray() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + JsonValue array = parsed.apply("array"); + assertEquals(array, array.asJsonArray()); + assertThrows(JsonMappingException.class, () -> parsed.apply("object").asJsonArray()); + } + + @Test + public void testAsJsonArrayOption() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + assertTrue(parsed.apply("array").asJsonArrayOptional().isPresent()); + assertEquals(Optional.empty(), parsed.apply("object").asJsonArrayOptional()); + } + + @Test + public void testJsonObjectGet() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + assertEquals(Optional.of(parse("{\"a\":true,\"b\":false}")), parsed.get("object")); + assertEquals(Optional.empty(), parsed.get("aaaaa")); + } + + @Test + public void testJsonObjectApply() throws JsonProcessingException { + JsonObject parsed = parse(JSON).asJsonObject(); + assertEquals(parse("{\"a\":true,\"b\":false}"), parsed.apply("object")); + assertThrows(JsonMappingException.class, () -> parsed.apply("aaaaaaaa")); + } + + @Test + public void testJsonObjectIterator() throws JsonProcessingException { + List> results = new ArrayList<>(); + parse(JSON).asJsonObject().apply("object").asJsonObject().iterator().forEachRemaining(results::add); + + Map.Entry entryA = new AbstractMap.SimpleEntry<>("a", parse("true")); + AbstractMap.SimpleEntry entryB = new AbstractMap.SimpleEntry<>("b", parse("false")); + List> expectedResult = Arrays.asList(entryA, entryB); + + assertEquals(expectedResult, results); + } + + @Test + public void testJsonArrayIterator() throws JsonProcessingException { + List results = new ArrayList<>(); + parse(JSON).asJsonObject().apply("array").asJsonArray().iterator().forEachRemaining(results::add); + + List expected = Arrays.asList("4.0", "11.1", "44.5") + .stream() + .map(this::parse) + .collect(Collectors.toList()); + assertEquals(expected, results); + } + + @Test + public void testJsonValueEquals() { + assertEquals(parse(JSON), parse(JSON)); + assertEquals(parse("{\"blue\": true, \"red\": false}"), parse("{\"red\": false, \"blue\": true}")); + assertNotEquals(parse("{\"blue\": true, \"red\": true}"), parse("{\"red\": false, \"blue\": true}")); + + assertEquals(parse("[1, 2, 3]"), parse("[1, 2, 3]")); + assertNotEquals(parse("[1, 2, 3]"), parse("[2, 1, 3]")); + + assertEquals(parse("1344"), parse("1344")); + assertNotEquals(parse("1344"), parse("144")); + } + + @Test + public void testJsonValueHashCode() throws JsonProcessingException { + assertEquals(new ObjectMapper().readTree(JSON).hashCode(), parse(JSON).hashCode()); + } + + @Test + public void testJsonValueToString() { + String js = "{\"boolean\":false,\"int\":1234,\"array\":[4.0,11.1,44.5],\"object\":{\"a\":true,\"b\":false}}"; + assertEquals(js, parse(js).toString()); + } + + @Test + public void testDecodeBoolean() throws JsonMappingException { + DecodeJson.DecodeBoolean decodeJson = new DecodeJson.DecodeBoolean(); + assertTo(false, decodeJson, jsonObject -> jsonObject.get("boolean").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("int").get()); + } + + @Test + public void testDecodeString() throws JsonMappingException { + DecodeJson.DecodeString decodeJson = new DecodeJson.DecodeString(); + assertTo("string", decodeJson, jsonObject -> jsonObject.get("string").get()); + assertTo("123", decodeJson, jsonObject -> jsonObject.get("number_as_string").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("int").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("array").get()); + } + + @Test + public void testDecodeInt() throws JsonMappingException { + DecodeJson.DecodeInteger decodeJson = new DecodeJson.DecodeInteger(); + assertTo(1234, decodeJson, jsonObject -> jsonObject.get("int").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("long").get()); + } + + @Test + public void testDecodeLong() throws JsonMappingException { + DecodeJson.DecodeLong decodeJson = new DecodeJson.DecodeLong(); + assertTo(3000000000L, decodeJson, jsonObject -> jsonObject.get("long").get()); + assertTo(1234L, decodeJson, jsonObject -> jsonObject.get("int").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("string").get()); + } + + @Test + public void testDecodeDouble() throws JsonMappingException { + DecodeJson.DecodeDouble decodeJson = new DecodeJson.DecodeDouble(); + assertTo(16.244355, decodeJson, jsonObject -> jsonObject.get("double").get()); + assertTo(1234.0, decodeJson, jsonObject -> jsonObject.get("int").get()); + assertTo(3000000000.0, decodeJson, jsonObject -> jsonObject.get("long").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("string").get()); + } + + @Test + public void testDecodeSeq() throws JsonMappingException { + DecodeJson> decodeJson = DecodeJson.decodeList(new DecodeJson.DecodeDouble()); + assertTo(Arrays.asList(4.0, 11.1, 44.5), decodeJson, jsonObject -> jsonObject.get("array").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("string").get()); + assertToFails(decodeJson, jsonObject -> jsonObject.get("object").get()); + assertToFails(DecodeJson.decodeList(new DecodeJson.DecodeString()), jsonObject -> jsonObject.get("array").get()); + } + + @Test + public void testDecodeMap() throws JsonMappingException { + DecodeJson> decodeJson = DecodeJson.decodeMap(new DecodeJson.DecodeBoolean()); + HashMap stringBooleanMap = new HashMap<>(); + stringBooleanMap.put("a", true); + stringBooleanMap.put("b", false); + assertTo(stringBooleanMap, decodeJson, jsonObject -> jsonObject.get("object").get()); + assertToFails(DecodeJson.decodeMap(new DecodeJson.DecodeInteger()), jsonObject -> jsonObject.get("object").get()); + assertToFails(DecodeJson.decodeMap(new DecodeJson.DecodeString()), jsonObject -> jsonObject.get("object").get()); + assertToFails(DecodeJson.decodeMap(new DecodeJson.DecodeDouble()), jsonObject -> jsonObject.get("array").get()); + } + + @Test + public void testDecodeOptional() throws JsonMappingException { + DecodeJson> decodeJson = DecodeJson.decodeOptional(new DecodeJson.DecodeInteger()); + assertTo(Optional.empty(), decodeJson, jsonObject -> jsonObject.get("null").get()); + assertTo(Optional.of(1234), decodeJson, jsonObject -> jsonObject.get("int").get()); + assertToFails(DecodeJson.decodeOptional(new DecodeJson.DecodeString()), jsonObject -> jsonObject.get("int").get()); + } + private void assertTo(T expected, DecodeJson decodeJson, Function jsonValue) throws JsonMappingException { + JsonValue parsed = jsonValue.apply(parse(JSON).asJsonObject()); + assertEquals(expected, parsed.to(decodeJson)); + } + + private void assertToFails(DecodeJson decoder, Function jsonValue) throws JsonMappingException { + JsonObject obj = parse(JSON).asJsonObject(); + JsonValue parsed = jsonValue.apply(obj); + assertThrows(JsonMappingException.class, () -> parsed.to(decoder)); + } +}