diff --git a/boat-engine/src/main/java/com/backbase/oss/boat/transformers/Decomposer.java b/boat-engine/src/main/java/com/backbase/oss/boat/transformers/Decomposer.java index 2df3ca91d..c1cb2cd7a 100644 --- a/boat-engine/src/main/java/com/backbase/oss/boat/transformers/Decomposer.java +++ b/boat-engine/src/main/java/com/backbase/oss/boat/transformers/Decomposer.java @@ -18,7 +18,7 @@ public class Decomposer implements Transformer { public OpenAPI transform(OpenAPI openAPI, Map options) { List composedSchemas = openAPI.getComponents().getSchemas().values().stream() - .filter(schema -> schema instanceof ComposedSchema) + .filter(ComposedSchema.class::isInstance) .collect(Collectors.toList()); composedSchemas.forEach(composedSchema -> mergeComposedSchema(openAPI, composedSchema)); diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateFromDirectoryDocMojo.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateFromDirectoryDocMojo.java index e61e648a0..3620312e4 100644 --- a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateFromDirectoryDocMojo.java +++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateFromDirectoryDocMojo.java @@ -51,62 +51,66 @@ private void fileInputExecute(File inputSpecFile) throws MojoExecutionException, inputSpecs = findAllOpenApiSpecs(inputSpecFile); if (inputSpecs.length == 0) { - throw new MojoExecutionException("No OpenAPI specs found in: " + inputSpec); - } + log.warn("No OpenAPI specs found in: " + inputSpec); + } else { - List success = new ArrayList<>(); - List failed = new ArrayList<>(); - for (File f : inputSpecs) { - inputSpec = f.getPath(); - output = new File(outPutDirectory.getPath(), f.getName().substring(0, f.getName().lastIndexOf("."))); - - if (!output.exists()) { - try { - Files.createDirectory(output.toPath()); - } catch (IOException e) { - log.error("Failed to create output directory", e); - } + List success = new ArrayList<>(); + List failed = new ArrayList<>(); + for (File inputSpec : inputSpecs) { + executeInputFile(outPutDirectory, success, failed, inputSpec); } + writeMarkers(success, failed); + } + } else { + log.info("inputSpec being read as a single file"); + super.execute(); + } + } - log.info(" Generating docs for spec {} in directory", f.getName()); - try { - super.execute(); - success.add(f); - } catch (MojoExecutionException | MojoFailureException e) { - log.error("Failed to generate doc for spec: {}", inputSpec); - failed.add(f); + private void writeMarkers(List success, List failed) throws MojoExecutionException { + if (markersDirectory != null) { + try { + if (!markersDirectory.exists()) { + Files.createDirectory(markersDirectory.toPath()); } - } - if (markersDirectory != null) { - try { - if (!markersDirectory.exists()) { - Files.createDirectory(markersDirectory.toPath()); - } - - Files.write(new File(markersDirectory, "success.lst").toPath(), - listOfFilesToString(success).getBytes(StandardCharsets.UTF_8), - StandardOpenOption.CREATE, - StandardOpenOption.APPEND); - Files.write(new File(markersDirectory, "failed.lst").toPath(), - listOfFilesToString(failed).getBytes(StandardCharsets.UTF_8), - StandardOpenOption.CREATE, - StandardOpenOption.APPEND); - - } catch (IOException e) { - log.error("Failed to write BOAT markers to: {}", markersDirectory, e); - throw new MojoExecutionException("Failed to write BOAT markers", e); + Files.write(new File(markersDirectory, "success.lst").toPath(), + listOfFilesToString(success).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + Files.write(new File(markersDirectory, "failed.lst").toPath(), + listOfFilesToString(failed).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND); - } + } catch (IOException e) { + log.error("Failed to write BOAT markers to: {}", markersDirectory, e); + throw new MojoExecutionException("Failed to write BOAT markers", e); } + } + } - } else { + private void executeInputFile(File outPutDirectory, List success, List failed, File f) { + inputSpec = f.getPath(); + output = new File(outPutDirectory.getPath(), f.getName().substring(0, f.getName().lastIndexOf("."))); - log.info("inputSpec being read as a single file"); - super.execute(); + if (!output.exists()) { + try { + Files.createDirectory(output.toPath()); + } catch (IOException e) { + log.error("Failed to create output directory", e); + } + } + log.info(" Generating docs for spec {} in directory", f.getName()); + try { + super.execute(); + success.add(f); + } catch (MojoExecutionException | MojoFailureException e) { + log.error("Failed to generate doc for spec: {}", inputSpec); + failed.add(f); } } diff --git a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateMojo.java b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateMojo.java index d3193be90..dc2009bf2 100644 --- a/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateMojo.java +++ b/boat-maven-plugin/src/main/java/com/backbase/oss/boat/GenerateMojo.java @@ -509,9 +509,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { default: String message = format("Input spec %s matches more than one single file", inputSpec); getLog().error(message); - Stream.of(files).forEach(f -> { - getLog().error(format(" %s", f)); - }); + Stream.of(files).forEach(f -> getLog().error(format(" %s", f))); throw new MojoExecutionException( format("Input spec %s matches more than one single file", inputSpec)); } @@ -966,7 +964,7 @@ private URL inputSpecRemoteUrl() { /** * Get specification hash file. * - * @param inputSpecFile - Openapi specification input file to calculate it's hash. + * @param inputSpecFile - Openapi specification input file to calculate it's hash. * Does not taken into account if input spec is hosted on remote resource * @return a file with previously calculated hash */ @@ -1044,7 +1042,7 @@ private void adjustAdditionalProperties(final CodegenConfig config) { private static boolean isValidURI(String urlString) { try { - URI uri = new URI(urlString); + new URI(urlString); return true; } catch (Exception exception) { return false; diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatExampleUtils.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatExampleUtils.java index 131f3fa92..40a4b6b25 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatExampleUtils.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/doc/BoatExampleUtils.java @@ -2,20 +2,31 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; -import java.util.List; +import io.swagger.v3.oas.models.responses.ApiResponse; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @Slf4j @UtilityClass @SuppressWarnings("java:S3740") public class BoatExampleUtils { + private static final String PATHS_REF_PREFIX = "#/paths"; + private static final String COMPONENTS_EXAMPLES_REF_PREFIX = "#/components/examples/"; + + public static void convertExamples(OpenAPI openAPI, MediaType mediaType, String responseCode, String contentType, List examples) { if (mediaType.getExample() != null) { Object example = mediaType.getExample(); @@ -62,27 +73,126 @@ private static boolean isJson(String contentType) { } public static void inlineExamples(String name, List examples, OpenAPI openAPI) { + List nonExistingExamples = new ArrayList<>(); + examples.stream() - .filter(boatExample -> boatExample.getExample().get$ref() != null) - .forEach(boatExample -> { - String ref = boatExample.getExample().get$ref(); - if (ref.startsWith("#/components/examples")) { - ref = StringUtils.substringAfterLast(ref, "/"); - if (openAPI.getComponents() != null && openAPI.getComponents().getExamples() != null) { - Example example = openAPI.getComponents().getExamples().get(ref); - if (example == null) { - log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); - } else { - log.debug("Replacing Example ref: {} used in: {} with example from components: {}", ref, name, example); - boatExample.setExample(example); - } + .filter(boatExample -> { + Example example = boatExample.getExample(); + if (example == null) { + log.warn("Example :{} refers to an example that does not exist", boatExample.getKey()); + nonExistingExamples.add(boatExample); + } + return example != null && example.get$ref() != null; + }) + .forEach(boatExample -> { + String ref = boatExample.getExample().get$ref(); + if (ref.startsWith(COMPONENTS_EXAMPLES_REF_PREFIX)) { + resolveComponentsExamples(name, openAPI, boatExample, ref); + } else if (ref.startsWith(PATHS_REF_PREFIX)) { + resolvePathsExamples(name, openAPI, boatExample, ref); } else { - log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); + log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); } - } else { - log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); - } - }); + }); + // Ensure non existing examples are removed to prevent further errors down the road + examples.removeAll(nonExistingExamples); + } + + private static void resolveComponentsExamples( + String name, OpenAPI openAPI, BoatExample boatExample, String ref) { + String exampleName = ref.replace(COMPONENTS_EXAMPLES_REF_PREFIX, ""); + if (openAPI.getComponents() == null || openAPI.getComponents().getExamples() == null) { + log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); + return; + } + Example example = openAPI.getComponents().getExamples().get(exampleName); + if (example == null) { + log.warn("Example ref: {} used in: {} refers to an example that does not exist", ref, name); + return; + } + log.debug("Replacing Example ref: {} used in: {} with example from components: {}", ref, name, example); + boatExample.setExample(example); + } + + private static void resolvePathsExamples( + String name, OpenAPI openAPI, BoatExample boatExample, String ref) { + + // #/paths/ + // ~1client-api~1v2~1accounts~1balance-history~1%7BarrangementIds%7D/ + // get/ + // responses/ + // 200/ + // content/ + // text~1csv/ + // example + if (openAPI.getPaths() == null) { + log.warn("Example ref: {} refers to '/paths' but it is not there.", ref); + return; + } + String[] refParts = Arrays.stream(ref.replace(PATHS_REF_PREFIX, "").split("/")) + .map(s -> s.replace("~1", "/")) + .toArray(String[]::new); + + String pathName = refParts[1]; + PathItem pathItem = openAPI.getPaths().get(pathName); + if (pathItem == null) { + log.warn("Example ref: {} refers to path {} but it is not defined.", ref, pathName); + return; + } + + String operationName = refParts[2]; + Operation operation = findOperation(pathItem, operationName); + if (operation == null) { + log.warn("Example ref: {} refers to operation {} but it is not defined.", ref, operationName); + return; + } + + Content content = null; + String mediaTypeName = null; + if ("requestBody".equals(refParts[3])) { + content = operation.getRequestBody().getContent(); + mediaTypeName = refParts[5]; + } else { + ApiResponse apiResponse = operation.getResponses().get(refParts[4]); + if (apiResponse == null) { + log.warn("Example ref: {} refers to response that is not defined.", ref); + return; + } + content = apiResponse.getContent(); + mediaTypeName = refParts[6]; + } + if (content == null) { + log.warn("Example ref: {} refers to content that is not defined.", ref); + return; + } + + MediaType mediaType = content.get(mediaTypeName); + if (mediaType == null) { + log.warn("Example ref: {} refers to mediaType {} that is not defined.", ref, mediaTypeName); + return; + } + + Example example = new Example().value(mediaType.getExample()); + log.warn("Incorrect example reference found! Replacing Example ref: {} used in: {} with example from components", ref, name); + boatExample.setExample(example); + } + + private static Operation findOperation(PathItem pathItem, String operationName) { + String o = operationName.toLowerCase(); + switch (o) { + case "get": + return pathItem.getGet(); + case "post": + return pathItem.getPost(); + case "put": + return pathItem.getPut(); + case "patch": + return pathItem.getPatch(); + case "delete": + return pathItem.getDelete(); + default: + throw new IllegalArgumentException("Unsupported operationName " + o); + } } } diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/doc/BoatDocsTest.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/doc/BoatDocsTest.java index e84020e72..f31326839 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/doc/BoatDocsTest.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/doc/BoatDocsTest.java @@ -34,6 +34,11 @@ void testOpenAPiWithExamples() throws OpenAPILoaderException { assertDoesNotThrow(() -> generateDocs(getFile("/openapi-with-examples/openapi.yaml"))); } + @Test + public void testGenerateDocsExampleRefs() { + assertDoesNotThrow(() -> generateDocs(getFile("/oas-examples/petstore-example-refs.yaml"))); + } + @Test void testGenerateDocs() throws IOException { generateDocs(getFile("/openapi-with-examples/openapi-with-json.yaml")); diff --git a/boat-scaffold/src/test/resources/logback.xml b/boat-scaffold/src/test/resources/logback.xml index 575b81d2c..c39bccbe8 100644 --- a/boat-scaffold/src/test/resources/logback.xml +++ b/boat-scaffold/src/test/resources/logback.xml @@ -1,4 +1,6 @@ + + %highlight([%level]) [%logger{10}] %msg%n diff --git a/boat-scaffold/src/test/resources/oas-examples/petstore-example-refs.yaml b/boat-scaffold/src/test/resources/oas-examples/petstore-example-refs.yaml new file mode 100644 index 000000000..e3f1cbe80 --- /dev/null +++ b/boat-scaffold/src/test/resources/oas-examples/petstore-example-refs.yaml @@ -0,0 +1,254 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + example: + $ref: "#/paths/~1pets/get/responses/invalid/content/application~1json/example" + + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/get/responses/default/content/application~2json/example" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + '401': + description: BadRequestError + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/components/examples/BadRequestError" + '500': + description: InternalServerError + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/components/examples/InternalServerError" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/get/responses/default/content/application~1json/example" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + example: + $ref: "#/paths/~1pets/get/responses/default/content/application~1json/example" + '500': + description: InternalServerError + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + InternalServerError: + $ref: "#/components/examples/BadRequestError" + OtherError: + $ref: "#/components/examples/BadRequestError" + '404': + description: InternalServerError + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + InternalServerError: + $ref: "#/components/examples/BadRequestError" + OtherError: + $ref: "#/components/examples/InvalidReference" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/post/responses/default/content/application~1json/example" + put: + summary: Update pet + operationId: updatePet + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + example: + id: 1 + name: "Joep" + tag: "Gun dog" + responses: + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/put/responses/default/content/application~1json/example" + patch: + summary: Update pet + operationId: patchPet + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to patch + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + example: + id: 1 + name: "Joep" + tag: "Gun dog" + responses: + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/patch/responses/default/content/application~1json/example" + delete: + summary: Delete pet + operationId: deletePet + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to delete + schema: + type: string + responses: + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + $ref: "#/paths/~1pets/delete/responses/default/content/application~1json/example" + + +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + examples: + BadRequestError: + summary: BadRequestError + value: + message: Bad Request + errors: + - message: "Value Exceeded. Must be between {min} and {max}." + key: common.api.shoesize + context: + max: "50" + min: "1" \ No newline at end of file