diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java
new file mode 100644
index 00000000..72c85d7c
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java
@@ -0,0 +1,160 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.tools;
+
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import org.yaml.snakeyaml.LoaderOptions;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class APIUtils
+{
+ public static final String PUBLISHED_API_LOCATION = "qqq-middleware-javalin/src/main/resources/openapi/";
+ public static final List FILE_FORMATS = List.of("json", "yaml");
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String getPublishedAPIFile(String apiPath, String name, String fileFormat) throws Exception
+ {
+ String fileLocation = apiPath + "/" + name + "." + fileFormat;
+ File file = new File(fileLocation);
+ if(!file.exists())
+ {
+ throw (new Exception("Error: The file [" + file.getPath() + "] could not be found."));
+ }
+
+ Path path = Paths.get(fileLocation);
+ return (StringUtils.join("\n", Files.readAllLines(path)));
+ }
+
+
+
+ /*******************************************************************************
+ ** get a map representation of the yaml.
+ ** we'll remove things from that map, writing out specific sub-files that we know we want (e.g., per-table & process).
+ ** also, there are some objects we just don't care about (e.g., tags, they'll always be in lock-step with the tables).
+ ** then we'll write out everything else left in the map at the end.
+ *******************************************************************************/
+ @SuppressWarnings("unchecked")
+ static Map> splitUpYamlForPublishing(String openApiYaml) throws JsonProcessingException
+ {
+ Map apiYaml = parseYaml(openApiYaml);
+ Map components = (Map) apiYaml.get("components");
+ Map schemas = (Map) components.get("schemas");
+ Map paths = (Map) apiYaml.remove("paths");
+ apiYaml.remove("tags");
+
+ Map> groupedPaths = new HashMap<>();
+ for(Map.Entry entry : paths.entrySet())
+ {
+ ///////////////////////////////////////////////////////////////////////////////
+ // keys here look like: apiName/apiVersion/table-or-process/ //
+ // let's discard the apiName & version, and group under the table-or-process //
+ ///////////////////////////////////////////////////////////////////////////////
+ String key = entry.getKey();
+ String[] parts = key.split("/");
+ String uniquePart = parts[3];
+ groupedPaths.computeIfAbsent(uniquePart, k -> new TreeMap<>());
+ groupedPaths.get(uniquePart).put(entry.getKey(), entry.getValue());
+ }
+
+ for(Map.Entry> entry : groupedPaths.entrySet())
+ {
+ String name = entry.getKey();
+ Map subMap = entry.getValue();
+ if(schemas.containsKey(name))
+ {
+ subMap.put("schema", schemas.remove(name));
+ }
+
+ name += "SearchResult";
+ if(schemas.containsKey(name))
+ {
+ subMap.put("searchResultSchema", schemas.remove(name));
+ }
+ }
+
+ ////////////////////////////////////////////////////////
+ // put the left-over yaml as a final entry in the map //
+ ////////////////////////////////////////////////////////
+ groupedPaths.put("openapi", apiYaml);
+
+ return groupedPaths;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @SuppressWarnings("unchecked")
+ private static Map parseYaml(String yaml) throws JsonProcessingException
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // need a larger limit than you get by default (and qqq's yamlUtils doens't let us customize this...) //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ LoaderOptions loaderOptions = new LoaderOptions();
+ loaderOptions.setCodePointLimit(100 * 1024 * 1024); // 100 MB
+ YAMLFactory yamlFactory = YAMLFactory.builder()
+ .loaderOptions(loaderOptions)
+ .build();
+ YAMLMapper mapper = new YAMLMapper(yamlFactory);
+
+ mapper.findAndRegisterModules();
+ return (mapper.readValue(yaml, Map.class));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static File[] listPublishedAPIFiles(String path) throws Exception
+ {
+ File file = new File(path);
+ if(!file.exists())
+ {
+ throw (new Exception("Error: API Directory [" + file.getPath() + "] could not be found."));
+ }
+
+ File[] files = file.listFiles();
+ return (files);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java
new file mode 100644
index 00000000..5e5bc7dd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java
@@ -0,0 +1,150 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.tools;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.YamlUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
+import picocli.CommandLine;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@CommandLine.Command(name = "publishAPI")
+public class PublishAPI implements Callable
+{
+ @CommandLine.Option(names = { "-r", "--repoRoot" })
+ private String repoRoot;
+
+ @CommandLine.Option(names = { "--sortFileContentsForHuman" }, description = "By default, properties in the yaml are sorted alphabetically, to help with stability (for diffing). This option preserves the 'natural' order of the file, so may look a little bette for human consumption")
+ private boolean sortFileContentsForHuman = false;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args) throws Exception
+ {
+ // for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" };
+ int exitCode = new CommandLine(new PublishAPI()).execute(args);
+ System.exit(exitCode);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Integer call() throws Exception
+ {
+ AbstractMiddlewareVersion middlewareVersion = new MiddlewareVersionV1();
+
+ if(!StringUtils.hasContent(repoRoot))
+ {
+ throw (new QException("Repo root argument was not given."));
+ }
+
+ if(!new File(repoRoot).exists())
+ {
+ throw (new QException("Repo root directory [" + repoRoot + "] was not found."));
+ }
+
+ String allApisPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/";
+ if(!new File(allApisPath).exists())
+ {
+ throw (new QException("APIs directory [" + allApisPath + "] was not found."));
+ }
+
+ File versionDirectory = new File(allApisPath + middlewareVersion.getVersion() + "/");
+ if(!versionDirectory.exists())
+ {
+ if(!versionDirectory.mkdirs())
+ {
+ // CTEngCliUtils.printError("Error: An error occurred creating directory [" + apiDirectory.getPath() + "].");
+ System.err.println("Error: An error occurred creating directory [" + versionDirectory.getPath() + "].");
+ return (1);
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // build the openapi spec - then run it through a "grouping" function, which will make several //
+ // subsets of it (e.g., grouped by table mostly) - then we'll write out each such file //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ OpenAPI openAPI = middlewareVersion.generate("qqq");
+ String yaml = YamlUtils.toYaml(openAPI, mapper ->
+ {
+ if(sortFileContentsForHuman)
+ {
+ ////////////////////////////////////////////////
+ // this is actually the default mapper config //
+ ////////////////////////////////////////////////
+ }
+ else
+ {
+ mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
+ }
+ });
+
+ writeFile(yaml, versionDirectory, "openapi.yaml");
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // if we want to split up by some paths, components, we could use a version of this //
+ //////////////////////////////////////////////////////////////////////////////////////
+ // Map> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml);
+ // for(String name : groupedPaths.keySet())
+ // {
+ // writeFile(groupedPaths.get(name), versionDirectory, name + ".yaml");
+ // }
+ // CTEngCliUtils.printSuccess("Files for [" + apiInstanceMetaData.getName() + "] [" + apiVersion + "] have been successfully published.");
+ // System.out.println("Files for [" + middlewareVersion.getClass().getSimpleName() + "] have been successfully published.");
+
+ return (0);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeFile(String yaml, File directory, String fileBaseName) throws IOException
+ {
+ String yamlFileName = directory.getAbsolutePath() + "/" + fileBaseName;
+ Path yamlPath = Paths.get(yamlFileName);
+ Files.write(yamlPath, yaml.getBytes());
+ System.out.println("Wrote [" + yamlPath + "]");
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java
new file mode 100644
index 00000000..d7986fe1
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java
@@ -0,0 +1,253 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.tools;
+
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.kingsrook.qqq.backend.core.utils.YamlUtils;
+import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
+import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
+import com.kingsrook.qqq.openapi.model.OpenAPI;
+import picocli.CommandLine;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+@CommandLine.Command(name = "validateApiVersions")
+public class ValidateAPIVersions implements Callable
+{
+ @CommandLine.Option(names = { "-r", "--repoRoot" })
+ String repoRoot;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args) throws Exception
+ {
+ // for a run from the IDE, to override args... args = new String[] { "-r", "/Users/dkelkhoff/git/kingsrook/qqq/" };
+ int exitCode = new CommandLine(new ValidateAPIVersions()).execute(args);
+ System.out.println("Exiting with code: " + exitCode);
+ System.exit(exitCode);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Integer call() throws Exception
+ {
+ String fileFormat = "yaml";
+ boolean hadErrors = false;
+ List errorHeaders = new ArrayList<>();
+
+ List specList = List.of(new MiddlewareVersionV1());
+
+ for(AbstractMiddlewareVersion middlewareVersion : specList)
+ {
+ String version = middlewareVersion.getVersion();
+ boolean hadErrorsThisVersion = false;
+
+ //////////
+ // todo //
+ //////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // // if this api version is in the list of "future" versions, consider it a "beta" and don't do any validation //
+ // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // for(APIVersion futureAPIVersion : apiInstanceMetaData.getFutureVersions())
+ // {
+ // if(apiVersion.equals(futureAPIVersion))
+ // {
+ // continue versionLoop;
+ // }
+ // }
+
+ try
+ {
+ ////////////////////////////////////////////////////////////
+ // list current files - so we can tell if all get diff'ed //
+ ////////////////////////////////////////////////////////////
+ Set existingFileNames = new HashSet<>();
+ String versionPath = repoRoot + "/" + APIUtils.PUBLISHED_API_LOCATION + "/" + version + "/";
+ versionPath = versionPath.replaceAll("/+", "/");
+ for(File file : APIUtils.listPublishedAPIFiles(versionPath))
+ {
+ existingFileNames.add(file.getPath().replaceFirst(versionPath, ""));
+ }
+
+ ///////////////////////////////////////////////////////////
+ // generate a new spec based on current code in codebase //
+ ///////////////////////////////////////////////////////////
+ OpenAPI openAPI = middlewareVersion.generate("qqq");
+ String yaml = YamlUtils.toYaml(openAPI, mapper ->
+ {
+ mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
+ });
+
+ /////////////////////////////////////////////////////////////////////
+ // get the published API file - then diff it to what we just wrote //
+ /////////////////////////////////////////////////////////////////////
+ String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, "openapi", fileFormat);
+
+ String newFileName = "/tmp/" + version + "-new." + fileFormat;
+ String publishedFileName = "/tmp/" + version + "-published." + fileFormat;
+ Files.write(Path.of(newFileName), yaml.getBytes());
+ Files.write(Path.of(publishedFileName), publishedAPI.getBytes());
+
+ Runtime rt = Runtime.getRuntime();
+ String[] commands = { "diff", "-w", publishedFileName, newFileName };
+ Process proc = rt.exec(commands);
+
+ BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+
+ StringBuilder diffOutput = new StringBuilder();
+ String s;
+ while((s = stdInput.readLine()) != null)
+ {
+ diffOutput.append(s).append("\n");
+ }
+
+ if(!"".contentEquals(diffOutput))
+ {
+ String message = "Error: Differences were found in openapi.yaml file between the published docs and the newly generated file for API Version [" + version + "].";
+ errorHeaders.add(message);
+ System.out.println(message);
+ System.out.println(diffOutput);
+ hadErrors = true;
+ hadErrorsThisVersion = true;
+ }
+
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // if we want to split up by some paths, components, we could use a version of this //
+ //////////////////////////////////////////////////////////////////////////////////////
+ /*
+ Map> groupedPaths = APIUtils.splitUpYamlForPublishing(yaml);
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // for each of the groupings (e.g., files), compare to what was previously published //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ for(Map.Entry> entry : groupedPaths.entrySet())
+ {
+ try
+ {
+ String name = entry.getKey();
+ String newFileToDiff = YamlUtils.toYaml(entry.getValue());
+
+ /////////////////////////////////////////////////////////////////////
+ // get the published API file - then diff it to what we just wrote //
+ /////////////////////////////////////////////////////////////////////
+ String publishedAPI = APIUtils.getPublishedAPIFile(versionPath, name, fileFormat);
+ existingFileNames.remove(name + "." + fileFormat);
+
+ String newFileName = "/tmp/" + version + "-new." + fileFormat;
+ String publishedFileName = "/tmp/" + version + "-published." + fileFormat;
+ Files.write(Path.of(newFileName), newFileToDiff.getBytes());
+ Files.write(Path.of(publishedFileName), publishedAPI.getBytes());
+
+ Runtime rt = Runtime.getRuntime();
+ String[] commands = { "diff", "-w", publishedFileName, newFileName };
+ Process proc = rt.exec(commands);
+
+ BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+
+ StringBuilder diffOutput = new StringBuilder();
+ String s;
+ while((s = stdInput.readLine()) != null)
+ {
+ diffOutput.append(s).append("\n");
+ }
+
+ if(!"".contentEquals(diffOutput))
+ {
+ String message = "Error: Differences were found in file [" + name + "] between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "].";
+ errorHeaders.add(message);
+ System.out.println(message);
+ System.out.println(diffOutput);
+ hadErrors = true;
+ hadErrorsThisVersion = true;
+ }
+ }
+ catch(Exception e)
+ {
+ errorHeaders.add(e.getMessage());
+ System.out.println(e.getMessage());
+ hadErrors = true;
+ hadErrorsThisVersion = true;
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // if any existing files weren't evaluated in the loop above, then that's an error //
+ // e.g., someone removed a thing that was previously in the API //
+ /////////////////////////////////////////////////////////////////////////////////////
+ if(!existingFileNames.isEmpty())
+ {
+ hadErrors = true;
+ hadErrorsThisVersion = true;
+ for(String existingFileName : existingFileNames)
+ {
+ String message = "Error: Previously published file [" + existingFileName + "] was not found in the current OpenAPI object for API Version [" + version + "].";
+ errorHeaders.add(message);
+ System.out.println(message);
+ }
+ }
+ */
+ }
+ catch(Exception e)
+ {
+ errorHeaders.add(e.getMessage());
+ System.out.println(e.getMessage());
+ hadErrors = true;
+ hadErrorsThisVersion = true;
+ }
+
+ if(!hadErrorsThisVersion)
+ {
+ System.out.println("Success: No differences were found between the published docs and the newly generated " + fileFormat + " file for API Version [" + version + "].");
+ }
+ }
+
+ if(!errorHeaders.isEmpty())
+ {
+ System.out.println("\nError summary:");
+ errorHeaders.forEach(e -> System.out.println(" - " + e));
+ }
+
+ return (hadErrors ? 1 : 0);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java
new file mode 100644
index 00000000..92a79cbd
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java
@@ -0,0 +1,172 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.FileUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+class ExecutorCodeGenerator
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static void main(String[] args)
+ {
+ try
+ {
+ String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/";
+ new ExecutorCodeGenerator().writeAllFiles(qqqDir, "ProcessMetaData"); // don't include "Executor" on the end.
+ }
+ catch(IOException e)
+ {
+ //noinspection CallToPrintStackTrace
+ e.printStackTrace();
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void writeOne(String fullPath, String content) throws IOException
+ {
+ File file = new File(fullPath);
+ File directory = file.getParentFile();
+
+ if(!directory.exists())
+ {
+ throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments)."));
+ }
+
+ if(file.exists())
+ {
+ throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files."));
+ }
+
+ System.out.println("Writing: " + file);
+ FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void writeAllFiles(String rootPath, String baseName) throws IOException
+ {
+ if(baseName.endsWith("Executor"))
+ {
+ throw new IllegalArgumentException("Base name must not end with 'Executor'.");
+ }
+
+ String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/";
+ writeOne(basePath + "executors/" + baseName + "Executor.java", makeExecutor(baseName));
+ writeOne(basePath + "executors/io/" + baseName + "Input.java", makeInput(baseName));
+ writeOne(basePath + "executors/io/" + baseName + "OutputInterface.java", makeOutputInterface(baseName));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeExecutor(String baseName)
+ {
+ return """
+ package com.kingsrook.qqq.middleware.javalin.executors;
+
+
+ import com.kingsrook.qqq.backend.core.exceptions.QException;
+ import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}Input;
+ import com.kingsrook.qqq.middleware.javalin.executors.io.${baseName}OutputInterface;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public class ${baseName}Executor extends AbstractMiddlewareExecutor<${baseName}Input, ${baseName}OutputInterface>
+ {
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void execute(${baseName}Input input, ${baseName}OutputInterface output) throws QException
+ {
+ }
+
+ }
+ """.replaceAll("\\$\\{baseName}", baseName);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeInput(String baseName)
+ {
+ return """
+ package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public class ${baseName}Input extends AbstractMiddlewareInput
+ {
+
+ }
+ """.replaceAll("\\$\\{baseName}", baseName);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeOutputInterface(String baseName)
+ {
+ return """
+ package com.kingsrook.qqq.middleware.javalin.executors.io;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public interface ${baseName}OutputInterface extends AbstractMiddlewareOutputInterface
+ {
+
+ }
+ """.replaceAll("\\$\\{baseName}", baseName);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java
new file mode 100644
index 00000000..64af0f2f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java
@@ -0,0 +1,300 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.FileUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+class SpecCodeGenerator
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static void main(String[] args)
+ {
+ try
+ {
+ String qqqDir = "/Users/dkelkhoff/git/kingsrook/qqq/";
+
+ /////////////////
+ // normal case //
+ /////////////////
+ new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessMetaData");
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // if the executor isn't named the same as the spec (e.g., reused executors) //
+ ///////////////////////////////////////////////////////////////////////////////
+ // new SpecCodeGenerator().writeAllFiles(qqqDir, "v1", "ProcessInsert", "ProcessInsertOrSetp");
+ }
+ catch(IOException e)
+ {
+ //noinspection CallToPrintStackTrace
+ e.printStackTrace();
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void writeOne(String fullPath, String content) throws IOException
+ {
+ File file = new File(fullPath);
+ File directory = file.getParentFile();
+
+ if(!directory.exists())
+ {
+ throw (new RuntimeException("Directory for: " + fullPath + " does not exists, and I refuse to mkdir (do it yourself and/or fix your arguments)."));
+ }
+
+ if(file.exists())
+ {
+ throw (new RuntimeException("File at: " + fullPath + " already exists, and I refuse to overwrite files."));
+ }
+
+ System.out.println("Writing: " + file);
+ FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void writeAllFiles(String rootPath, String version, String baseName) throws IOException
+ {
+ writeAllFiles(rootPath, version, baseName, baseName);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void writeAllFiles(String rootPath, String version, String baseName, String executorBaseName) throws IOException
+ {
+ String basePath = rootPath + "qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/";
+ writeOne(basePath + "specs/" + version.toLowerCase() + "/" + baseName + "Spec" + version.toUpperCase() + ".java", makeSpec(version, baseName, executorBaseName));
+ writeOne(basePath + "specs/" + version.toLowerCase() + "/responses/" + baseName + "Response" + version.toUpperCase() + ".java", makeResponse(version, baseName, executorBaseName));
+
+ System.out.println();
+ System.out.println("Hey - You probably want to add a line like:");
+ System.out.println(" list.add(new " + baseName + "Spec" + version.toUpperCase() + "());");
+ System.out.println("In:");
+ System.out.println(" MiddlewareVersion" + version.toUpperCase() + ".java");
+ System.out.println();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeSpec(String version, String baseName, String executorBaseName)
+ {
+ return """
+ package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase};
+
+
+ import java.util.List;
+ import java.util.Map;
+ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+ import com.kingsrook.qqq.middleware.javalin.executors.${executorBaseName}Executor;
+ import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}Input;
+ import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
+ import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
+ import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
+ import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses.${executorBaseName}Response${version.toUpperCase};
+ import com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.utils.Tags${version.toUpperCase};
+ import com.kingsrook.qqq.openapi.model.Content;
+ import com.kingsrook.qqq.openapi.model.Example;
+ import com.kingsrook.qqq.openapi.model.HttpMethod;
+ import com.kingsrook.qqq.openapi.model.In;
+ import com.kingsrook.qqq.openapi.model.Parameter;
+ import com.kingsrook.qqq.openapi.model.RequestBody;
+ import com.kingsrook.qqq.openapi.model.Schema;
+ import com.kingsrook.qqq.openapi.model.Type;
+ import io.javalin.http.ContentType;
+ import io.javalin.http.Context;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public class ${baseName}Spec${version.toUpperCase} extends AbstractEndpointSpec<${executorBaseName}Input, ${baseName}Response${version.toUpperCase}, ${executorBaseName}Executor>
+ {
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public BasicOperation defineBasicOperation()
+ {
+ return new BasicOperation()
+ .withPath(TODO)
+ .withHttpMethod(HttpMethod.TODO)
+ .withTag(Tags${version.toUpperCase}.TODO)
+ .withShortSummary(TODO)
+ .withLongDescription(""\"
+ TODO""\"
+ );
+ }
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public List defineRequestParameters()
+ {
+ return List.of(
+ new Parameter()
+ .withName(TODO)
+ .withDescription(TODO)
+ .withRequired(TODO)
+ .withSchema(new Schema().withType(Type.TODO))
+ .withExamples(Map.of("TODO", new Example().withValue(TODO)))
+ .withIn(In.TODO)
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public RequestBody defineRequestBody()
+ {
+ return new RequestBody()
+ .withContent(Map.of(
+ ContentType.TODO.getMimeType(), new Content()
+ .withSchema(new Schema()
+ .withType(Type.TODO)
+ .withProperties(Map.of(
+ "TODO", new Schema()
+ ))
+ )
+ ));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public ${executorBaseName}Input buildInput(Context context) throws Exception
+ {
+ ${executorBaseName}Input input = new ${executorBaseName}Input();
+ input.setTODO
+ return (input);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public BasicResponse defineBasicSuccessResponse()
+ {
+ Map examples = Map.of(
+
+ "TODO", new Example()
+ .withValue(new ${baseName}Response${version.toUpperCase}()
+ .withTODO
+ )
+
+ );
+
+ return new BasicResponse(""\"
+ TODO""\",
+
+ new ${baseName}Response${version.toUpperCase}().toSchema(),
+ examples
+ );
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void handleOutput(Context context, ${baseName}Response${version.toUpperCase} output) throws Exception
+ {
+ context.result(JsonUtils.toJson(output));
+ }
+
+ }
+ """
+ .replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase())
+ .replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase())
+ .replaceAll("\\$\\{executorBaseName}", executorBaseName)
+ .replaceAll("\\$\\{baseName}", baseName)
+ ;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeResponse(String version, String baseName, String executorBaseName)
+ {
+ return """
+ package com.kingsrook.qqq.middleware.javalin.specs.${version.toLowerCase}.responses;
+
+
+ import com.kingsrook.qqq.middleware.javalin.executors.io.${executorBaseName}OutputInterface;
+ import com.kingsrook.qqq.middleware.javalin.schemabuilder.annotations.OpenAPIDescription;
+ import com.kingsrook.qqq.middleware.javalin.specs.ToSchema;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public class ${baseName}Response${version.toUpperCase} implements ${executorBaseName}OutputInterface, ToSchema
+ {
+ @OpenAPIDescription(TODO)
+ private String TODO;
+
+ TODO gsw
+ }
+ """
+ .replaceAll("\\$\\{version.toLowerCase}", version.toLowerCase())
+ .replaceAll("\\$\\{version.toUpperCase}", version.toUpperCase())
+ .replaceAll("\\$\\{executorBaseName}", executorBaseName)
+ .replaceAll("\\$\\{baseName}", baseName)
+ ;
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java
new file mode 100644
index 00000000..4f4cfe11
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/*******************************************************************************
+ ** These classes are meant as tools to be executed manually by a developer,
+ ** to create other new classes (since there's a bit of boilerplate, innit?)
+ **
+ *******************************************************************************/
+package com.kingsrook.qqq.middleware.javalin.tools.codegenerators;
\ No newline at end of file
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java
new file mode 100644
index 00000000..2e2a4b8f
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/*******************************************************************************
+ ** This tools path - is for non-production code - rather, development and CI/CD
+ ** tools.
+ **
+ *******************************************************************************/
+package com.kingsrook.qqq.middleware.javalin.tools;
\ No newline at end of file