From a4204956e6fed70e89e3f367c2bd622d8ada9c85 Mon Sep 17 00:00:00 2001 From: Denislav Prinov Date: Mon, 2 Oct 2023 11:04:52 +0300 Subject: [PATCH] Openapi restdoc generation v2 (#1442) * OpenApi restdoc generation v2 Signed-off-by: Denislav Prinov * Attach json and yaml artifacts only if -DskipTests is not provided Signed-off-by: Denislav Prinov * Add missing header Signed-off-by: Denislav Prinov * Add license header Signed-off-by: Denislav Prinov --------- Signed-off-by: Denislav Prinov --- docs/README.md | 7 +- docs/build-htmls.sh | 4 +- docs/pom.xml | 31 ++-- docs/split-doc.sh | 52 ------ hawkbit-runtime/hawkbit-update-server/pom.xml | 102 ++++++------ .../eclipse/hawkbit/app/RestApiDocTest.java | 154 ++++++++++++++++++ 6 files changed, 222 insertions(+), 128 deletions(-) delete mode 100644 docs/split-doc.sh create mode 100644 hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/RestApiDocTest.java diff --git a/docs/README.md b/docs/README.md index 198b861139..21dda6a65d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,10 +4,9 @@ theme. Compiling the documentation is not included within the regular Maven buil ## Prerequisites 1. **Install Hugo**: see [installing Hugo](https://gohugo.io/getting-started/installing/) documentation on how to install Hugo. -2. **Install JQ**: see [installing jq](https://jqlang.github.io/jq/download/) documentation on how to install jq. -3. **Install NODE.js and npm** see [installing Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) documentation on how to install Node.js and npm -4. **Install Redocly CLI** see [installing Redocly CLI](https://redocly.com/docs/cli/installation/) documentation on how to install Redocly CLI -5. **Install hawkBit**: run `mvn install` in the parent directory to generate the latest REST docs for hawkBit. +2. **Install NODE.js and npm** see [installing Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) documentation on how to install Node.js and npm +3. **Install Redocly CLI** see [installing Redocly CLI](https://redocly.com/docs/cli/installation/) documentation on how to install Redocly CLI +4. **Install hawkBit**: run `mvn install` in the parent directory to generate the latest REST docs for hawkBit. ## Build and Serve documentation diff --git a/docs/build-htmls.sh b/docs/build-htmls.sh index 0f0a616935..a1506c59e7 100644 --- a/docs/build-htmls.sh +++ b/docs/build-htmls.sh @@ -24,7 +24,7 @@ else fi # Execute the npx command -npx @redocly/cli build-docs ${CURRENT_DIR}/content/rest-api/mgmt.json -o ${CURRENT_DIR}/content/rest-api/mgmt.html +npx @redocly/cli build-docs ${CURRENT_DIR}/content/rest-api/mgmt.yaml -o ${CURRENT_DIR}/content/rest-api/mgmt.html if [ $? != 0 ]; then echo "[ERROR] Failed to execute the Redoc CLI command form MGMT API." @@ -34,7 +34,7 @@ else fi # Execute the npx command -npx @redocly/cli build-docs ${CURRENT_DIR}/content/rest-api/ddi.json -o ${CURRENT_DIR}/content/rest-api/ddi.html +npx @redocly/cli build-docs ${CURRENT_DIR}/content/rest-api/ddi.yaml -o ${CURRENT_DIR}/content/rest-api/ddi.html if [ $? != 0 ]; then echo "[ERROR] Failed to execute the Redoc CLI command form DDI API." diff --git a/docs/pom.xml b/docs/pom.xml index ad9af91a1d..a4f17816dc 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -72,11 +72,21 @@ org.eclipse.hawkbit hawkbit-update-server ${project.version} - openapi - json + mgmt-openapi + yaml true ${basedir}/content/rest-api - openapi.json + mgmt.yaml + + + org.eclipse.hawkbit + hawkbit-update-server + ${project.version} + ddi-openapi + yaml + true + ${basedir}/content/rest-api + ddi.yaml @@ -88,21 +98,6 @@ exec-maven-plugin ${exec-maven-plugin.version} - - split-docs - - exec - - install - - ${shell} - ${project.basedir} - - ${shell.option} - split-doc.${batch.ext} - - - build-htmls diff --git a/docs/split-doc.sh b/docs/split-doc.sh deleted file mode 100644 index 9125e90f24..0000000000 --- a/docs/split-doc.sh +++ /dev/null @@ -1,52 +0,0 @@ -# -# Copyright (c) 2018 Bosch Software Innovations GmbH and others -# -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# - -#!/bin/bash - -CURRENT_DIR=$(pwd) -input_file=${CURRENT_DIR}/content/rest-api/openapi.json -mgmt_file=${CURRENT_DIR}/content/rest-api/mgmt.json -ddi_file=${CURRENT_DIR}/content/rest-api/ddi.json - -jq ' - .paths |= with_entries( - select( - reduce .value[] as $item ( - false; - . or ($item.tags? | index("DDI Root Controller")) == null - ) - ) - ) - | .tags |= map(select(.name | contains("DDI") | not)) - | .components.schemas = (.components.schemas | with_entries(select(.key | startswith("Ddi") | not))) -' "$input_file" > "$mgmt_file" - -jq ' - .paths |= with_entries( - select( - reduce .value[] as $item ( - false; - . or ($item.tags? | index("DDI Root Controller")) != null - ) - ) - ) - | .tags |= map(select(.name | contains("DDI"))) - | .components.schemas = ( - .components.schemas - | with_entries( - select( - (.key | startswith("Ddi")) - or (.key | . == "Link") - or (.key | . == "ExceptionInfo") - ) - ) - ) -' "$input_file" > "$ddi_file" - diff --git a/hawkbit-runtime/hawkbit-update-server/pom.xml b/hawkbit-runtime/hawkbit-update-server/pom.xml index 6b27beb3d1..d62be5878e 100644 --- a/hawkbit-runtime/hawkbit-update-server/pom.xml +++ b/hawkbit-runtime/hawkbit-update-server/pom.xml @@ -32,65 +32,63 @@ + + attach-artifacts-profile + + + + !skipTests + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/rest-api/mgmt-openapi.json + json + mgmt-openapi + + + ${project.build.directory}/rest-api/mgmt-openapi.yaml + yaml + mgmt-openapi + + + ${project.build.directory}/rest-api/ddi-openapi.json + json + ddi-openapi + + + ${project.build.directory}/rest-api/ddi-openapi.yaml + yaml + ddi-openapi + + + + + + + + + - - - org.springdoc - springdoc-openapi-maven-plugin - 1.4 - - - - generate-json - integration-test - - generate - - - openapi.json - http://localhost:8080/v3/api-docs - true - - - - - generate-yaml - integration-test - - generate - - - openapi.yaml - http://localhost:8080/v3/api-docs.yaml - true - - - - org.springframework.boot spring-boot-maven-plugin - - -Dspring.application.admin.enabled=true - org.eclipse.hawkbit.app.Start - - - pre-integration-test - pre-api-docs-generation - - start - - - - post-api-docs-generation - post-integration-test - - stop - - repackage diff --git a/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/RestApiDocTest.java b/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/RestApiDocTest.java new file mode 100644 index 0000000000..79f1c6ffad --- /dev/null +++ b/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/RestApiDocTest.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2023 Bosch.IO GmbH and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.hawkbit.app; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles({"maven"}) +class RestApiDocTest { + private static final String MANAGEMENT_PREFIX = "mgmt-openapi"; + private static final String DDI_PREFIX = "ddi-openapi"; + private static final String TARGET_DIRECTORY = "target/rest-api/"; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void openapiJson() throws IOException { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class); + String openapiDoc = response.getBody(); + assertThat(openapiDoc).isNotNull(); + splitDocumentation(openapiDoc); + } + + private void splitDocumentation(String json) throws IOException { + processDocumentation(json, true); + processDocumentation(json, false); + } + + private void processDocumentation(String json, boolean isMgmt) throws IOException { + JsonNode rootNode = objectMapper.readTree(json); + updateJsonNodeForApi(rootNode, isMgmt); + saveDocumentation(rootNode, isMgmt); + } + + private void updateJsonNodeForApi(JsonNode rootNode, boolean isMgmt) { + removeTags(rootNode, isMgmt); + removePaths(rootNode, isMgmt); + removeComponents(rootNode, isMgmt); + } + + private void removeTags(JsonNode rootNode, boolean isMgmt) { + ArrayNode tagsNode = (ArrayNode) rootNode.get("tags"); + ArrayNode modifiedTagsNode = objectMapper.createArrayNode(); + + for (JsonNode tagNode : tagsNode) { + String tagName = tagNode.get("name").asText(); + if (isMgmt != tagName.startsWith("DDI")) { + modifiedTagsNode.add(tagNode); + } + } + + ((ObjectNode) rootNode).set("tags", modifiedTagsNode); + } + private void removePaths(JsonNode rootNode, boolean isMgmt) { + ObjectNode pathsNode = (ObjectNode) rootNode.get("paths"); + List fieldsToRemove = new ArrayList<>(); + pathsNode.fieldNames().forEachRemaining(fieldName -> { + JsonNode pathNode = pathsNode.get(fieldName); + pathNode.fieldNames().forEachRemaining(path -> { + JsonNode methodNode = pathNode.get(path); + JsonNode tagsNode = methodNode.get("tags"); + if (tagsNode != null) { + for (JsonNode tagNode : tagsNode) { + String tag = tagNode.asText(); + if (isMgmt == tag.startsWith("DDI")) { + fieldsToRemove.add(fieldName); + break; + } + } + } + }); + }); + fieldsToRemove.forEach(pathsNode::remove); + } + + private void removeComponents(JsonNode rootNode, boolean isMgmt) { + ObjectNode schemasNode = (ObjectNode) rootNode.get("components").get("schemas"); + + List fieldsToRemove = new ArrayList<>(); + schemasNode.fieldNames().forEachRemaining(fieldName -> { + if (shouldDeleteComponent(fieldName, isMgmt)) { + fieldsToRemove.add(fieldName); + } + }); + fieldsToRemove.forEach(schemasNode::remove); + } + + private boolean shouldDeleteComponent(String fieldName, boolean isMgmt) { + if (isMgmt) { + return fieldName.startsWith("Ddi"); + } + return !(fieldName.startsWith("Ddi") || fieldName.equals("Link") || fieldName.equals("ExceptionInfo")); + + } + + private void saveDocumentation(JsonNode rootNode, boolean isMgmt) throws IOException { + String prefix = isMgmt ? MANAGEMENT_PREFIX : DDI_PREFIX; + saveAsJson(rootNode, prefix); + saveAsYaml(rootNode, prefix); + } + + private void saveAsJson(JsonNode rootNode, String prefix) throws IOException { + Path targetPath = getTargetPath(prefix, ".json"); + Files.writeString(targetPath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode)); + } + + private void saveAsYaml(JsonNode rootNode, String prefix) throws IOException { + YAMLMapper yamlMapper = new YAMLMapper(); + Path targetPath = getTargetPath(prefix, ".yaml"); + Files.writeString(targetPath, yamlMapper.writeValueAsString(rootNode)); + } + + private Path getTargetPath(String prefix, String extension) throws IOException { + Path targetPath = Paths.get(TARGET_DIRECTORY + prefix + extension); + Files.createDirectories(targetPath.getParent()); + return targetPath; + } +} +