From a46397df39242e82d3d4da1f719c973dce598bec Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 17 Oct 2024 12:34:41 -0500 Subject: [PATCH] CE-1887 Initial checkin - dev-tools for middleware api --- .../middleware/javalin/tools/APIUtils.java | 160 ++++++++++ .../middleware/javalin/tools/PublishAPI.java | 150 +++++++++ .../javalin/tools/ValidateAPIVersions.java | 253 +++++++++++++++ .../codegenerators/ExecutorCodeGenerator.java | 172 ++++++++++ .../codegenerators/SpecCodeGenerator.java | 300 ++++++++++++++++++ .../tools/codegenerators/package-info.java | 27 ++ .../javalin/tools/package-info.java | 27 ++ 7 files changed, 1089 insertions(+) create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/APIUtils.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/PublishAPI.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/ValidateAPIVersions.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/ExecutorCodeGenerator.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/SpecCodeGenerator.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/codegenerators/package-info.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/tools/package-info.java 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