small api doc cleanup; large refactor (resources as resources, not java strings)

This commit is contained in:
2023-03-30 11:31:04 -05:00
parent 7e6a09fc21
commit 944a93bd99
4 changed files with 4164 additions and 139 deletions

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.api.javalin;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -90,12 +91,14 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import com.kingsrook.qqq.backend.javalin.QJavalinAccessLogger;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.BooleanUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.json.JSONArray;
@ -141,8 +144,24 @@ public class QJavalinApiHandler
/////////////////////////////
ApiBuilder.post("/api/oauth/token", QJavalinApiHandler::handleAuthorization);
///////////////////////////////////////////////
// static endpoints to support rapidoc pages //
///////////////////////////////////////////////
ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-9.3.4.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT)));
ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> QJavalinApiHandler.serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS)));
//////////////////////////////////////////////
// default page is the current version spec //
//////////////////////////////////////////////
ApiBuilder.get("/api/", QJavalinApiHandler::doSpecHtml);
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
{
////////////////////////////////////////////
// default page for a version is its spec //
////////////////////////////////////////////
ApiBuilder.get("/", QJavalinApiHandler::doSpecHtml);
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson);
ApiBuilder.get("/openapi.html", QJavalinApiHandler::doSpecHtml);
@ -171,7 +190,6 @@ public class QJavalinApiHandler
});
ApiBuilder.get("/api/versions.json", QJavalinApiHandler::doVersions);
ApiBuilder.get("/api/qqq-api-styles.css", QJavalinApiHandler::doStyles);
ApiBuilder.before("/*", QJavalinApiHandler::setupCORS);
@ -199,6 +217,21 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void serveResource(Context context, String resourcePath, Map<String, String> headers)
{
InputStream resourceAsStream = QJavalinApiHandler.class.getClassLoader().getResourceAsStream(resourcePath);
for(Map.Entry<String, String> entry : CollectionUtils.nonNullMap(headers).entrySet())
{
context.header(entry.getKey(), entry.getValue());
}
context.result(resourceAsStream);
}
/*******************************************************************************
**
*******************************************************************************/
@ -248,43 +281,6 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void doSpecYaml(Context context)
{
try
{
QContext.init(qInstance, null);
String version = context.pathParam("version");
GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version);
try
{
if(StringUtils.hasContent(context.pathParam("tableName")))
{
input.setTableName(context.pathParam("tableName"));
}
}
catch(Exception e)
{
///////////////////////////
// leave table param out //
///////////////////////////
}
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(input);
context.contentType(ContentType.APPLICATION_YAML);
context.result(output.getYaml());
}
catch(Exception e)
{
handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -380,6 +376,43 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void doSpecYaml(Context context)
{
try
{
QContext.init(qInstance, null);
String version = context.pathParam("version");
GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version);
try
{
if(StringUtils.hasContent(context.pathParam("tableName")))
{
input.setTableName(context.pathParam("tableName"));
}
}
catch(Exception e)
{
///////////////////////////
// leave table param out //
///////////////////////////
}
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(input);
context.contentType(ContentType.APPLICATION_YAML);
context.result(output.getYaml());
}
catch(Exception e)
{
handleException(context, e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -422,71 +455,19 @@ public class QJavalinApiHandler
*******************************************************************************/
private static void doSpecHtml(Context context)
{
String version;
try
{
QContext.init(qInstance, null);
QBrandingMetaData branding = QContext.getQInstance().getBranding();
String html = """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
<link rel="stylesheet" href="/api/qqq-api-styles.css">
</head>
<body>
<rapi-doc
id="the-rapi-doc"
spec-url="/api/{version}/openapi.json"
regular-font="Roboto,Helvetica,Arial,sans-serif"
mono-font="Monaco, Menlo, Consolas, source-code-pro, monospace"
font-size="large"
show-header="false"
allow-server-selection="false"
allow-spec-file-download="true"
primary-color="{primaryColor}"
sort-endpoints-by="method"
persist-auth="true"
render-style="focused"
show-method-in-nav-bar="as-colored-block"
nav-item-spacing="relaxed"
css-file="qqq-api-styles.css"
css-classes="qqqApi"
info-description-headings-in-navbar="true"
>
{navLogoImg}
</rapi-doc>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
const rapidocEl = document.getElementById('the-rapi-doc');
rapidocEl.addEventListener('spec-loaded', (e) => {
console.log("rapidoc el: " + rapidocEl);
const shadowRoot = rapidocEl.shadowRoot;
console.log("shadowRoot: " + shadowRoot);
const collapseButton = shadowRoot.querySelector(".nav-bar-collapse-all")
collapseButton.click();
});
});
</script>
</body>
</html>
"""
.replace("{version}", context.pathParam("version"))
.replace("{primaryColor}", branding == null ? "#FF791A" : branding.getAccentColor());
if(branding != null && StringUtils.hasContent(branding.getLogo()))
{
html = html.replace("{navLogoImg}", "<img slot=\"nav-logo\" src=\"" + branding.getLogo() + "\" style=\"width: fit-content; max-width: 280px; margin: auto;\"/>");
}
context.contentType(ContentType.HTML);
context.result(html);
version = context.pathParam("version");
}
catch(Exception e)
{
handleException(context, e);
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance);
version = apiInstanceMetaData.getCurrentVersion().toString();
}
doSpecHtml(context, version);
}
@ -494,53 +475,45 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static void doStyles(Context context)
private static void doSpecHtml(Context context, String version)
{
try
{
QContext.init(qInstance, null);
QBrandingMetaData branding = qInstance.getBranding();
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance);
String css = """
#api-info
//////////////////////////////////
// read html from resource file //
//////////////////////////////////
InputStream resourceAsStream = QJavalinApiHandler.class.getClassLoader().getResourceAsStream("rapidoc/rapidoc-container.html");
String html = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8.name());
/////////////////////////////////
// do replacements in the html //
/////////////////////////////////
html = html.replace("{version}", version);
html = html.replace("{primaryColor}", branding == null ? "#FF791A" : branding.getAccentColor());
if(branding != null && StringUtils.hasContent(branding.getLogo()))
{
margin-left: 0px !important;
html = html.replace("{navLogoImg}", "<img id=\"navLogo\" slot=\"nav-logo\" src=\"" + branding.getLogo() + "\" />");
}
else
{
html = html.replace("{navLogoImg}", "");
}
#api-info button
{
width: auto !important;
}
html = html.replace("{title}", apiInstanceMetaData.getName() + " - " + version);
#api-title span
StringBuilder otherVersionOptions = new StringBuilder();
for(APIVersion supportedVersion : apiInstanceMetaData.getSupportedVersions())
{
font-size: 24px !important;
margin-left: 8px;
otherVersionOptions.append("<option value=\"/api/").append(supportedVersion).append("/openapi.html\">").append(supportedVersion).append("</option>");
}
html = html.replace("{otherVersionOptions}", otherVersionOptions.toString());
.nav-scroll
{
padding-left: 16px;
}
.tag-description.expanded
{
max-height: initial !important;
}
.tag-description .m-markdown p
{
margin-block-end: 0.5em !important;
}
api-response
{
margin-bottom: 50vh;
display: inline-block;
}
""";
context.contentType(ContentType.CSS);
context.result(css);
context.contentType(ContentType.HTML);
context.result(html);
}
catch(Exception e)
{

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
<!--
~ QQQ - Low-code Application Framework for Engineers.
~ Copyright (C) 2021-2023. 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 <https://www.gnu.org/licenses/>.
-->
<!doctype html>
<html lang="en-US">
<head>
<title>{title}</title>
<meta charset="utf-8">
<!-- <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script> -->
<script type="module" src="/api/docs/js/rapidoc.min.js"></script>
<link rel="stylesheet" href="/api/docs/css/qqq-api-styles.css">
</head>
<body>
<rapi-doc
id="the-rapi-doc"
spec-url="/api/{version}/openapi.json"
regular-font="Roboto,Helvetica,Arial,sans-serif"
mono-font="Monaco, Menlo, Consolas, source-code-pro, monospace"
font-size="large"
show-header="false"
allow-server-selection="false"
allow-spec-file-download="true"
primary-color="{primaryColor}"
sort-endpoints-by="method"
persist-auth="true"
render-style="focused"
show-method-in-nav-bar="as-colored-block"
nav-item-spacing="relaxed"
css-file="qqq-api-styles.css"
css-classes="qqqApi"
info-description-headings-in-navbar="true"
>
{navLogoImg}
<div slot="overview" id="otherVersions">
<label for="otherVersionsSelect">Other Versions:</label>
<select id="otherVersionsSelect" onchange=changeVersion()>
<option value="/api/">--</option>
{otherVersionOptions}
</select>
</div>
</rapi-doc>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
const rapidocEl = document.getElementById('the-rapi-doc');
rapidocEl.addEventListener('spec-loaded', (e) => {
const shadowRoot = rapidocEl.shadowRoot;
const collapseButton = shadowRoot.querySelector(".nav-bar-collapse-all");
if(collapseButton)
{
collapseButton.click();
}
const navLogo = document.querySelector("#navLogo");
if(navLogo)
{
navLogo.style.visibility = "visible";
}
});
});
function changeVersion()
{
document.location.href = document.getElementById("otherVersionsSelect").value;
}
</script>
</body>
</html>

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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 <https://www.gnu.org/licenses/>.
*/
#api-info
{
margin-left: 0 !important;
}
#api-info button
{
width: auto !important;
}
#api-title span
{
font-size: 24px !important;
margin-left: 8px;
}
.nav-scroll
{
padding-left: 16px;
}
.tag-description.expanded
{
max-height: initial !important;
}
.tag-description .m-markdown p
{
margin-block-end: 0.5em !important;
}
api-response
{
margin-bottom: 50vh;
display: inline-block;
}
#otherVersions
{
margin-bottom: 2em;
}
#navLogo
{
width: fit-content;
max-width: 280px;
margin-left: auto;
margin-right: auto;
visibility: hidden;
}