diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index 5dc585b9..e1f2bee5 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -5,7 +5,6 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; -import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -41,8 +40,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.apache.commons.io.FileUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; @@ -52,14 +49,15 @@ import picocli.CommandLine.UnmatchedArgumentException; /******************************************************************************* + ** QQQ PicoCLI implementation. Given a QInstance, produces an entire CLI + ** for working with all tables in that instance. + ** ** Note: Please do not use System.out or .err here -- rather, use the CommandLine ** object's out & err members - so the unit test can see the output! - * + ** *******************************************************************************/ public class QPicoCliImplementation { - private static final Logger LOG = LogManager.getLogger(QPicoCliImplementation.class); - public static final int DEFAULT_LIMIT = 20; private static QInstance qInstance; @@ -274,7 +272,6 @@ public class QPicoCliImplementation { CommandSpec deleteCommand = CommandSpec.create(); - QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); deleteCommand.addOption(OptionSpec.builder("--primaryKey") .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type? .build()); @@ -287,19 +284,19 @@ public class QPicoCliImplementation /******************************************************************************* ** *******************************************************************************/ - @SuppressWarnings("checkstyle:Indentation") private Class getClassForField(QFieldMetaData field) { + // @formatter:off // IJ can't do new-style switch correctly yet... return switch(field.getType()) - { - case STRING, TEXT, HTML, PASSWORD -> String.class; - case INTEGER -> Integer.class; - case DECIMAL -> BigDecimal.class; - case DATE -> LocalDate.class; - // case TIME -> LocalTime.class; - case DATE_TIME -> LocalDateTime.class; - default -> throw new IllegalStateException("Unsupported field type: " + field.getType()); - }; + { + case STRING, TEXT, HTML, PASSWORD -> String.class; + case INTEGER -> Integer.class; + case DECIMAL -> BigDecimal.class; + case DATE -> LocalDate.class; + // case TIME -> LocalTime.class; + case DATE_TIME -> LocalDateTime.class; + }; + // @formatter:on } @@ -436,7 +433,7 @@ public class QPicoCliImplementation ///////////////////////////////////////////// // get the records that the user specified // ///////////////////////////////////////////// - List recordList = null; + List recordList; if(subParseResult.hasMatchedOption("--jsonBody")) { String json = subParseResult.matchedOptionValue("--jsonBody", ""); @@ -502,6 +499,7 @@ public class QPicoCliImplementation } + /******************************************************************************* ** *******************************************************************************/ @@ -509,13 +507,10 @@ public class QPicoCliImplementation { DeleteRequest deleteRequest = new DeleteRequest(qInstance); deleteRequest.setTableName(tableName); - QTableMetaData table = qInstance.getTable(tableName); ///////////////////////////////////////////// // get the pKeys that the user specified // ///////////////////////////////////////////// - List primaryKeyList = null; - String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); String[] primaryKeyValues = primaryKeyOption.split(","); deleteRequest.setPrimaryKeys(Arrays.asList(primaryKeyValues)); diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java index 3a8a2689..f86977fb 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -2,12 +2,17 @@ package com.kingsrook.qqq.frontend.picocli; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.UUID; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.commons.io.FileUtils; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* + ** Unit test for the QPicoCliImplementation. ** *******************************************************************************/ class QPicoCliImplementationTest @@ -27,6 +33,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** Fully rebuild the test-database before each test runs, for completely known state. ** *******************************************************************************/ @BeforeEach @@ -38,6 +45,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test that w/ no arguments you just get usage. ** *******************************************************************************/ @Test @@ -50,6 +58,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test that --help gives you usage. ** *******************************************************************************/ @Test @@ -63,6 +72,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test the --verion argument ** *******************************************************************************/ @Test @@ -75,6 +85,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** Test that an unrecognized opttion gives an error ** *******************************************************************************/ @Test @@ -89,6 +100,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test the top-level --meta-data option ** *******************************************************************************/ @Test @@ -108,6 +120,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test giving a table-name, gives usage for that table ** *******************************************************************************/ @Test @@ -121,6 +134,22 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test unknown command under table, prints error and usage. + ** + *******************************************************************************/ + @Test + public void test_tableUnknownCommand() + { + String badCommand = "qwuijibo"; + TestOutput testOutput = testCli("person", badCommand); + assertTrue(testOutput.getError().contains("Unmatched argument at index 1: '" + badCommand + "'")); + assertTrue(testOutput.getError().contains("Usage: " + CLI_NAME + " person [COMMAND]")); + } + + + + /******************************************************************************* + ** test requesting table meta-data ** *******************************************************************************/ @Test @@ -144,6 +173,7 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test running a query on a table ** *******************************************************************************/ @Test @@ -161,6 +191,134 @@ class QPicoCliImplementationTest /******************************************************************************* + ** test running an insert w/o specifying any fields, prints usage + ** + *******************************************************************************/ + @Test + public void test_tableInsertNoFieldsPrintsUsage() + { + TestOutput testOutput = testCli("person", "insert"); + assertTrue(testOutput.getOutput().contains("Usage: " + CLI_NAME + " person insert")); + } + + + + /******************************************************************************* + ** test running an insert w/ fields as arguments + ** + *******************************************************************************/ + @Test + public void test_tableInsertFieldArguments() + { + TestOutput testOutput = testCli("person", "insert", + "--field-firstName=Lucy", + "--field-lastName=Lu"); + JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(insertResult); + assertEquals(1, insertResult.getJSONArray("records").length()); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); + } + + + + /******************************************************************************* + ** test running an insert w/ a mapping and json as an argument + ** + *******************************************************************************/ + @Test + public void test_tableInsertJsonObjectArgumentWithMapping() + { + String mapping = """ + --mapping={"firstName":"first","lastName":"ln"} + """; + + String jsonBody = """ + --jsonBody={"first":"Chester","ln":"Cheese"} + """; + + TestOutput testOutput = testCli("person", "insert", mapping, jsonBody); + JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(insertResult); + assertEquals(1, insertResult.getJSONArray("records").length()); + assertEquals(6, insertResult.getJSONArray("records").getJSONObject(0).getInt("primaryKey")); + assertEquals("Chester", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("firstName")); + assertEquals("Cheese", insertResult.getJSONArray("records").getJSONObject(0).getJSONObject("values").getString("lastName")); + } + + + + /******************************************************************************* + ** test running an insert w/ a mapping and json as a multi-record file + ** + *******************************************************************************/ + @Test + public void test_tableInsertJsonArrayFileWithMapping() throws IOException + { + String mapping = """ + --mapping={"firstName":"first","lastName":"ln"} + """; + + String jsonContents = """ + [{"first":"Charlie","ln":"Bear"},{"first":"Coco","ln":"Bean"}] + """; + + File file = new File("/tmp/" + UUID.randomUUID() + ".json"); + file.deleteOnExit(); + FileUtils.writeStringToFile(file, jsonContents); + + TestOutput testOutput = testCli("person", "insert", mapping, "--jsonFile=" + file.getAbsolutePath()); + JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(insertResult); + JSONArray records = insertResult.getJSONArray("records"); + assertEquals(2, records.length()); + assertEquals(6, records.getJSONObject(0).getInt("primaryKey")); + assertEquals(7, records.getJSONObject(1).getInt("primaryKey")); + assertEquals("Charlie", records.getJSONObject(0).getJSONObject("values").getString("firstName")); + assertEquals("Bear", records.getJSONObject(0).getJSONObject("values").getString("lastName")); + assertEquals("Coco", records.getJSONObject(1).getJSONObject("values").getString("firstName")); + assertEquals("Bean", records.getJSONObject(1).getJSONObject("values").getString("lastName")); + } + + + + /******************************************************************************* + ** test running an insert w/ an index-based mapping and csv file + ** + *******************************************************************************/ + @Test + public void test_tableInsertCsvFileWithIndexMapping() throws IOException + { + String mapping = """ + --mapping={"firstName":1,"lastName":3} + """; + + String csvContents = """ + "Louis","P","Willikers",1024, + "Nestle","G","Crunch",1701, + + """; + + File file = new File("/tmp/" + UUID.randomUUID() + ".csv"); + file.deleteOnExit(); + FileUtils.writeStringToFile(file, csvContents); + + TestOutput testOutput = testCli("person", "insert", mapping, "--csvFile=" + file.getAbsolutePath()); + JSONObject insertResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(insertResult); + JSONArray records = insertResult.getJSONArray("records"); + assertEquals(2, records.length()); + assertEquals(6, records.getJSONObject(0).getInt("primaryKey")); + assertEquals(7, records.getJSONObject(1).getInt("primaryKey")); + assertEquals("Louis", records.getJSONObject(0).getJSONObject("values").getString("firstName")); + assertEquals("Willikers", records.getJSONObject(0).getJSONObject("values").getString("lastName")); + assertEquals("Nestle", records.getJSONObject(1).getJSONObject("values").getString("firstName")); + assertEquals("Crunch", records.getJSONObject(1).getJSONObject("values").getString("lastName")); + } + + + + /******************************************************************************* + ** test running a delete against a table ** *******************************************************************************/ @Test @@ -330,4 +488,4 @@ class QPicoCliImplementationTest this.errorLines = errorLines; } } -} \ No newline at end of file +} diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index b66b9073..f7e0c5d3 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -17,12 +17,14 @@ import static junit.framework.Assert.assertNotNull; /******************************************************************************* + ** Utility methods for unit tests. ** *******************************************************************************/ public class TestUtils { /******************************************************************************* + ** Prime a test database (e.g., h2, in-memory) ** *******************************************************************************/ @SuppressWarnings("unchecked") @@ -43,6 +45,7 @@ public class TestUtils /******************************************************************************* + ** Run an SQL Query in the test database ** *******************************************************************************/ public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception @@ -55,6 +58,7 @@ public class TestUtils /******************************************************************************* + ** Define the q-instance for testing (h2 rdbms and 'person' table) ** *******************************************************************************/ public static QInstance defineInstance() @@ -68,6 +72,7 @@ public class TestUtils /******************************************************************************* + ** Define the h2 rdbms backend ** *******************************************************************************/ public static QBackendMetaData defineBackend() @@ -85,6 +90,7 @@ public class TestUtils /******************************************************************************* + ** Define the person table ** *******************************************************************************/ public static QTableMetaData defineTablePerson()