diff --git a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/JsonQuery.java b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/JsonQuery.java index 1ad22151..6dc34dad 100644 --- a/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/JsonQuery.java +++ b/selcukes-databind/src/main/java/io/github/selcukes/databind/utils/JsonQuery.java @@ -76,15 +76,20 @@ public T get(String path, Class type) { return (node == null || node.isMissingNode()) ? null : objectMapper.treeToValue(node, type); } + @SneakyThrows + public List getList(String path, Class type) { + return getList(path).stream().map(node -> objectMapper.convertValue(node, type)).toList(); + } + /** - * Retrieves a list of values from the JSON document at the specified - * wildcard path. + * Retrieves a list of JsonNode values from the JSON document at the + * specified wildcard path. * * @param path the wildcard path to retrieve values from - * @return a list of values found at the specified path + * @return a list of JsonNode found at the specified path */ - public List getList(String path) { - List values = new ArrayList<>(); + public List getList(String path) { + List values = new ArrayList<>(); // Updated to List JsonNode currentNode = rootNode; String[] keys = path.split("\\."); @@ -102,6 +107,25 @@ public List getList(String path) { return extractFinalValues(currentNode, values); } + /** + * Retrieves a JsonNode from the specified path. + * + * @param path the path to the node + * @return the JsonNode at the specified path, or null if not found + */ + public JsonNode getNode(String path) { + JsonNode currentNode = rootNode; + String[] keys = path.split("\\."); + + for (String key : keys) { + currentNode = navigateToNode(currentNode, key); + if (currentNode == null || currentNode.isMissingNode()) { + return null; + } + } + return currentNode; + } + /** * Extracts values from arrays using wildcard paths. * @@ -109,9 +133,14 @@ public List getList(String path) { * @param keys the path keys * @param key the current key being processed * @param values the list to store extracted values - * @return a list of extracted values + * @return a list of extracted JsonNode values */ - private List extractValuesFromArray(JsonNode currentNode, String[] keys, String key, List values) { + private List extractValuesFromArray( + JsonNode currentNode, + String[] keys, + String key, + List values + ) { String arrayKey = extractArrayKey(key); var resultNode = currentNode.path(arrayKey); @@ -131,13 +160,13 @@ private List extractValuesFromArray(JsonNode currentNode, String[] keys, * @param node the current JSON node being processed * @param values the list to store extracted values */ - private void processArrayElement(String[] keys, String arrayKey, JsonNode node, List values) { + private void processArrayElement(String[] keys, String arrayKey, JsonNode node, List values) { if (isLastKey(keys, arrayKey)) { - values.add(node.asText()); + values.add(node); } else { JsonNode resultNode = getNodeFromSubPath(node, removeProcessedArrayKey(keys, arrayKey)); - if (resultNode != null && resultNode.isValueNode()) { - values.add(resultNode.asText()); + if (resultNode != null) { + values.add(resultNode); } } } @@ -193,15 +222,15 @@ private boolean isLastKey(String[] keys, String key) { * * @param currentNode the current JSON node * @param values the list to store extracted values - * @return a list of extracted values + * @return a list of extracted JsonNode values */ - private List extractFinalValues(JsonNode currentNode, List values) { + private List extractFinalValues(JsonNode currentNode, List values) { if (currentNode.isArray()) { for (JsonNode node : currentNode) { - values.add(node.asText()); + values.add(node); } } else if (currentNode.isValueNode()) { - values.add(currentNode.asText()); + values.add(currentNode); } return values; } @@ -251,48 +280,37 @@ private boolean isArrayKey(String key) { } /** - * Retrieves a JsonNode for an array based on the specified key and index. + * Retrieves a JsonNode from an array using the provided key and index. * * @param currentNode the current JSON node - * @param key the array key with an index - * @return the resulting JsonNode or null if not found + * @param key the key to access the array + * @return the JsonNode at the specified array index */ private JsonNode getArrayNode(JsonNode currentNode, String key) { String arrayKey = key.substring(0, key.indexOf("[")); - int index = extractArrayIndex(key); - currentNode = currentNode.path(arrayKey); - - return (currentNode.isArray()) ? currentNode.get(index) : null; + int index = Integer.parseInt(key.substring(key.indexOf("[") + 1, key.indexOf("]"))); + var arrayNode = currentNode.path(arrayKey); + return arrayNode.isArray() && arrayNode.size() > index ? arrayNode.get(index) : null; } /** - * Extracts the index value from an array key. + * Converts a JsonNode to a string representation. * - * @param key the array key containing an index - * @return the extracted index - * @throws NumberFormatException if the index is not a valid integer + * @param node the JsonNode to convert + * @return the string representation of the JsonNode */ - private int extractArrayIndex(String key) { - return Integer.parseInt(key.substring(key.indexOf("[") + 1, key.indexOf("]"))); + @SneakyThrows + public String toString(JsonNode node) { + return objectMapper.writeValueAsString(node); } /** - * Retrieves a node based on a specified path. + * Converts the root node of the JSON to a string representation. * - * @param path the path to retrieve - * @return the resulting JsonNode or null if not found + * @return the string representation of the root node */ - private JsonNode getNode(String path) { - String[] keys = path.split("\\."); - JsonNode currentNode = rootNode; - - for (String key : keys) { - currentNode = navigateToNode(currentNode, key); - if (currentNode == null || currentNode.isMissingNode()) { - return null; - } - } - return currentNode; + @SneakyThrows + public String toString() { + return toString(rootNode); } - } diff --git a/selcukes-databind/src/test/java/io/github/selcukes/databind/tests/JsonQueryTest.java b/selcukes-databind/src/test/java/io/github/selcukes/databind/tests/JsonQueryTest.java index a9a089c5..390a2d9d 100644 --- a/selcukes-databind/src/test/java/io/github/selcukes/databind/tests/JsonQueryTest.java +++ b/selcukes-databind/src/test/java/io/github/selcukes/databind/tests/JsonQueryTest.java @@ -18,127 +18,144 @@ import io.github.selcukes.databind.utils.JsonQuery; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.List; import java.util.Map; public class JsonQueryTest { - private final String json = """ - { - "store": { - "book": [ - { - "category": "reference", - "author": "Nigel Rees", - "title": "Sayings of the Century", - "price": 8.95, - "details": { - "publisher": "Publisher A", - "year": 2000 - } - }, - { - "category": "fiction", - "author": "Evelyn Waugh", - "title": "Sword of Honour", - "price": 12.99, - "details": { - "publisher": "Publisher B", - "year": 2005 - } - }, - { - "category": "fiction", - "author": "Herman Melville", - "title": "Moby Dick", - "isbn": "0-553-21311-3", - "price": 8.99, - "details": { - "publisher": "Publisher C", - "year": 2006 - } - }, - { - "category": "fiction", - "author": "J. R. R. Tolkien", - "title": "The Lord of the Rings", - "isbn": "0-395-19395-8", - "price": 22.99, - "details": { - "publisher": "Publisher D", - "year": 2024 + + private JsonQuery jsonQuery; + + @BeforeMethod + public void setUp() { + String json = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "details": { + "publisher": "Publisher A", + "year": 2000 + } + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + "details": { + "publisher": "Publisher B", + "year": 2005 + } + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "details": { + "publisher": "Publisher C", + "year": 2006 + } + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "details": { + "publisher": "Publisher D", + "year": 2024 + } + } + ], + "bicycle": { + "color": "red", + "price": 19.95 } } - ], - "bicycle": { - "color": "red", - "price": 19.95 } - } - } - """; + """; + jsonQuery = new JsonQuery(json); // Initialize the JsonQuery object + // before each test + } @Test - public void testJsonQueryConstructionFromString() { - JsonQuery jsonQuery = new JsonQuery(json); + public void shouldConstructJsonQueryInstanceFromJsonString() { Assert.assertNotNull(jsonQuery, "JsonQuery instance should be created from JSON string."); } @Test - public void testGetStringValueByPath() { - JsonQuery jsonQuery = new JsonQuery(json); + public void shouldGetStringValueForAuthorPath() { String author = jsonQuery.get("store.book[0].author", String.class); - Assert.assertEquals(author, "Nigel Rees", "Author should be Nigel Rees."); + Assert.assertEquals(author, "Nigel Rees", "Expected author is 'Nigel Rees'."); } @Test - public void testGetMapValueByPath() { - JsonQuery jsonParser = new JsonQuery(json); - var actualBookDetails = jsonParser.get("store.book[3].details", Map.class); - Map expectedBookDetails = Map.of("publisher", "Publisher D", - "year", 2024); - Assert.assertEquals(actualBookDetails, expectedBookDetails, "Book details do not match the expected values."); + public void shouldRetrieveMapValueForDetailsPath() { + var actualBookDetails = jsonQuery.get("store.book[3].details", Map.class); + Map expectedBookDetails = Map.of("publisher", "Publisher D", "year", 2024); + Assert.assertEquals(actualBookDetails, expectedBookDetails, + "The book details should match the expected values."); } @Test - public void testGetMissingValue() { - JsonQuery jsonQuery = new JsonQuery(json); + public void shouldReturnNullForMissingValue() { String missingValue = jsonQuery.get("store.book[0].publisher", String.class); Assert.assertNull(missingValue, "Missing value should return null."); } @Test - public void testGetListByPath() { - JsonQuery jsonQuery = new JsonQuery(json); - List titles = jsonQuery.getList("store.book[*].title"); - Assert.assertEquals(titles.size(), 4, "Should return 4 book titles."); - Assert.assertTrue(titles.contains("Moby Dick"), "Titles should contain 'Moby Dick'."); - Assert.assertTrue(titles.contains("The Lord of the Rings"), "Titles should contain 'The Lord of the Rings'."); + public void shouldGetListOfBookTitles() { + List titles = jsonQuery.getList("store.book[*].title", String.class); + Assert.assertEquals(titles.size(), 4, "There should be 4 book titles."); + Assert.assertTrue(titles.contains("Moby Dick"), "The list of titles should contain 'Moby Dick'."); + Assert.assertTrue(titles.contains("The Lord of the Rings"), + "The list of titles should contain 'The Lord of the Rings'."); + } + + @Test + public void shouldRetrieveListOfDetailsForAllBooks() { + var bookDetails = jsonQuery.getList("store.book[*].details", Map.class); + Map expectedDetailsBook1 = Map.of("publisher", "Publisher A", "year", 2000); + Map expectedDetailsBook2 = Map.of("publisher", "Publisher B", "year", 2005); + Map expectedDetailsBook3 = Map.of("publisher", "Publisher C", "year", 2006); + Map expectedDetailsBook4 = Map.of("publisher", "Publisher D", "year", 2024); + Assert.assertEquals(bookDetails.size(), 4, "There should be 4 book details."); + Assert.assertEquals(bookDetails.get(0), expectedDetailsBook1, "Details for the first book should match."); + Assert.assertEquals(bookDetails.get(1), expectedDetailsBook2, "Details for the second book should match."); + Assert.assertEquals(bookDetails.get(2), expectedDetailsBook3, "Details for the third book should match."); + Assert.assertEquals(bookDetails.get(3), expectedDetailsBook4, "Details for the fourth book should match."); } @Test - public void testGetListFromNonArrayPath() { - JsonQuery jsonQuery = new JsonQuery(json); - List color = jsonQuery.getList("store.bicycle.color"); - Assert.assertEquals(color.size(), 1, "Should return 1 color."); - Assert.assertEquals(color.get(0), "red", "Color should be 'red'."); + public void shouldRetrieveListFromNonArrayPath() { + List color = jsonQuery.getList("store.bicycle.color", String.class); + Assert.assertEquals(color.size(), 1, "There should be exactly 1 color."); + Assert.assertEquals(color.get(0), "red", "The color of the bicycle should be 'red'."); } @Test - public void testGetListFromDeeplyNestedStructure() { - JsonQuery jsonQuery = new JsonQuery(json); - List publishers = jsonQuery.getList("store.book[*].details.publisher"); - Assert.assertEquals(publishers.size(), 4, "Should return 2 publishers."); - Assert.assertTrue(publishers.contains("Publisher A"), "Should contain Publisher A."); - Assert.assertTrue(publishers.contains("Publisher B"), "Should contain Publisher B."); + public void shouldRetrievePublishersFromDeeplyNestedStructure() { + List publishers = jsonQuery.getList("store.book[*].details.publisher", String.class); + Assert.assertEquals(publishers.size(), 4, "There should be 4 publishers."); + Assert.assertTrue(publishers.contains("Publisher A"), "Publishers should contain 'Publisher A'."); + Assert.assertTrue(publishers.contains("Publisher B"), "Publishers should contain 'Publisher B'."); } @Test - public void testGetListFromEmptyArray() { - String emptyJson = "{}"; // Empty JSON - JsonQuery jsonQuery = new JsonQuery(emptyJson); - List emptyList = jsonQuery.getList("store.book[*].title"); - Assert.assertTrue(emptyList.isEmpty(), "List should be empty for non-existent array."); + public void shouldReturnEmptyListForNonExistentArray() { + String emptyJson = "{}"; + JsonQuery emptyJsonQuery = new JsonQuery(emptyJson); + List emptyList = emptyJsonQuery.getList("store.book[*].title", String.class); + Assert.assertTrue(emptyList.isEmpty(), "The list should be empty for a non-existent array."); } }