From 304dcf3dad2313ac1da54ce15be7e01d350dcfa9 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Mon, 9 Sep 2024 21:09:00 +0530 Subject: [PATCH 01/12] Add bal openapi flatten sub command --- .../ballerina/openapi/cmd/CmdConstants.java | 1 + .../io/ballerina/openapi/cmd/Flatten.java | 255 ++++++++++++++++++ .../io/ballerina/openapi/cmd/OpenApiCmd.java | 2 +- .../cli-help/ballerina-openapi-flatten.help | 53 ++++ 4 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java create mode 100644 openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java index d68e35b0d..0cc33f488 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/CmdConstants.java @@ -206,6 +206,7 @@ public String getValue() { List.of("2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"); public static final String DEFAULT_CLIENT_ID = "oas_%s_%s"; public static final String OPENAPI_ADD_CMD = "add"; + public static final String OPENAPI_FLATTEN_CMD = "flatten"; public enum Mode { SERVICE, diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java new file mode 100644 index 000000000..d7637aec7 --- /dev/null +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -0,0 +1,255 @@ +package io.ballerina.openapi.cmd; + +import io.ballerina.cli.BLauncherCmd; +import io.ballerina.openapi.core.generators.common.model.Filter; +import io.ballerina.openapi.service.mapper.utils.CodegenUtils; +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static io.ballerina.openapi.cmd.CmdConstants.JSON_EXTENSION; +import static io.ballerina.openapi.cmd.CmdConstants.OPENAPI_FLATTEN_CMD; +import static io.ballerina.openapi.cmd.CmdConstants.YAML_EXTENSION; +import static io.ballerina.openapi.cmd.CmdConstants.YML_EXTENSION; +import static io.ballerina.openapi.core.generators.common.GeneratorConstants.UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE; +import static io.ballerina.openapi.service.mapper.utils.CodegenUtils.resolveContractFileName; + +/** + * Main class to implement "flatten" subcommand which is used to flatten the OpenAPI definition + * by moving all the inline schemas to the "#/components/schemas" section. + * + * @since 1.9.0 + */ +@CommandLine.Command( + name = "flatten", + description = "Flatten the OpenAPI definition by moving all the inline schemas to the " + + "\"#/components/schemas\" section." +) +public class Flatten implements BLauncherCmd { + private static final String COMMAND_IDENTIFIER = "openapi-flatten"; + private static final String COMMA = ","; + + private static final String INFO_OUTPUT_WRITTEN_MSG = "[INFO] flattened OpenAPI definition file was successfully" + + " written to: %s%n"; + private static final String WARNING_INVALID_OUTPUT_FORMAT = "[WARNING] invalid output format. The output format" + + " should be either \"json\" or \"yaml\".Defaulting to format of the input file."; + private static final String ERROR_INPUT_PATH_IS_REQUIRED = "[ERROR] an OpenAPI definition path is required to " + + "flatten the OpenAPI definition."; + private static final String ERROR_INVALID_INPUT_FILE_EXTENSION = "[ERROR] invalid input OpenAPI definition file " + + "extension. The OpenAPI definition file should be in YAML or JSON format."; + private static final String ERROR_OCCURRED_WHILE_READING_THE_INPUT_FILE = "[ERROR] error occurred while reading " + + "the OpenAPI definition file."; + private static final String ERROR_UNSUPPORTED_OPENAPI_VERSION = "[ERROR] provided OpenAPI contract version is " + + "not supported in the tool. Use OpenAPI specification version 2 or higher"; + private static final String ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE = "[Error] error occurred while " + + "parsing the OpenAPI definition file."; + private static final String FOUND_PARSER_DIAGNOSTICS = "found the following parser diagnostic messages:"; + private static final String ERROR_OCCURRED_WHILE_WRITING_THE_OUTPUT_OPENAPI_FILE = "[ERROR] error occurred while " + + "writing the flattened OpenAPI definition file"; + + private final PrintStream infoStream = System.out; + private final PrintStream errorStream = System.err; + + private Path targetPath = Paths.get(System.getProperty("user.dir")); + + @CommandLine.Option(names = {"-h", "--help"}, hidden = true) + public boolean helpFlag; + + @CommandLine.Option(names = {"-i", "--input"}, description = "OpenAPI definition file path.") + public String inputPath; + + @CommandLine.Option(names = {"-o", "--output"}, description = "Location of the flattened OpenAPI definition.") + private String outputPath; + + @CommandLine.Option(names = {"-n", "--name"}, description = "Name of the flattened OpenAPI definition file.") + private String name; + + @CommandLine.Option(names = {"-f", "--format"}, description = "Output format of the flattened OpenAPI definition.") + private String format; + + @CommandLine.Option(names = {"-t", "--tags"}, description = "Tags that need to be considered when flattening.") + public String tags; + + @CommandLine.Option(names = {"--operations"}, description = "Operations that need to be included when flattening.") + public String operations; + + @Override + public void execute() { + if (helpFlag) { + String commandUsageInfo = BLauncherCmd.getCommandUsageInfo(COMMAND_IDENTIFIER); + infoStream.println(commandUsageInfo); + return; + } + + if (Objects.isNull(inputPath) || inputPath.isBlank()) { + errorStream.println(ERROR_INPUT_PATH_IS_REQUIRED); + exitError(); + return; + } + + if (inputPath.endsWith(YAML_EXTENSION) || inputPath.endsWith(JSON_EXTENSION) || + inputPath.endsWith(YML_EXTENSION)) { + populateInputOptions(); + generateFlattenOpenAPI(); + return; + } + + errorStream.println(ERROR_INVALID_INPUT_FILE_EXTENSION); + exitError(); + } + + private void populateInputOptions() { + if (Objects.nonNull(format)) { + if (!format.equalsIgnoreCase("json") && !format.equalsIgnoreCase("yaml")) { + setDefaultFormat(); + errorStream.println(WARNING_INVALID_OUTPUT_FORMAT); + } + } else { + setDefaultFormat(); + } + + if (Objects.isNull(name)) { + name = "flatten_openapi"; + } + + if (Objects.nonNull(outputPath)) { + targetPath = Paths.get(outputPath).isAbsolute() ? + Paths.get(outputPath) : Paths.get(targetPath.toString(), outputPath); + } + } + + private void setDefaultFormat() { + format = inputPath.endsWith(JSON_EXTENSION) ? "json" : "yaml"; + } + + private void generateFlattenOpenAPI() { + String openAPIFileContent; + try { + openAPIFileContent = Files.readString(Path.of(inputPath)); + } catch (Exception e) { + errorStream.println(ERROR_OCCURRED_WHILE_READING_THE_INPUT_FILE); + exitError(); + return; + } + + SwaggerParseResult parseResult = getOpenAPIWithFlattenOption(openAPIFileContent); + if (!parseResult.getMessages().isEmpty()) { + if (parseResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { + errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION); + exitError(); + return; + } + } + + OpenAPI openAPI = parseResult.getOpenAPI(); + if (Objects.isNull(openAPI)) { + errorStream.println(ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE); + if (Objects.nonNull(parseResult.getMessages())) { + errorStream.println(FOUND_PARSER_DIAGNOSTICS); + parseResult.getMessages().forEach(errorStream::println); + } + exitError(); + return; + } + + filterOpenAPIOperations(openAPI); + writeFlattenOpenAPIFile(openAPI); + } + + private static SwaggerParseResult getOpenAPIWithFlattenOption(String openAPIFileContent) { + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setFlatten(true); + parseOptions.setCamelCaseFlattenNaming(true); + parseOptions.setFlattenComposedSchemas(true); + return new OpenAPIParser().readContents(openAPIFileContent, null, parseOptions); + } + + private void writeFlattenOpenAPIFile(OpenAPI openAPI) { + String outputFileNameWithExt = getOutputFileName(); + try { + CodegenUtils.writeFile(targetPath.resolve(outputFileNameWithExt), + outputFileNameWithExt.endsWith(JSON_EXTENSION) ? Json.pretty(openAPI) : Yaml.pretty(openAPI)); + infoStream.printf(INFO_OUTPUT_WRITTEN_MSG, targetPath.resolve(outputFileNameWithExt)); + } catch (IOException exception) { + errorStream.println(ERROR_OCCURRED_WHILE_WRITING_THE_OUTPUT_OPENAPI_FILE); + exitError(); + } + } + + private String getOutputFileName() { + return resolveContractFileName(targetPath, name + getFileExtension(), format.equals("json")); + } + + private String getFileExtension() { + return (Objects.nonNull(format) && format.equals("json")) ? JSON_EXTENSION : YAML_EXTENSION; + } + + private void filterOpenAPIOperations(OpenAPI openAPI) { + Filter filter = getFilter(); + if (filter.getOperations().isEmpty() && filter.getTags().isEmpty()) { + return; + } + + openAPI.getPaths().forEach((path, pathItem) -> pathItem.readOperationsMap() + .forEach((httpMethod, operation) -> { + if (!filter.getOperations().contains(operation.getOperationId()) && + operation.getTags().stream().noneMatch(filter.getTags()::contains)) { + pathItem.operation(httpMethod, null); + } + }) + ); + } + + private Filter getFilter() { + List tagList = new ArrayList<>(); + List operationList = new ArrayList<>(); + + if (Objects.nonNull(tags) && !tags.isEmpty()) { + tagList.addAll(Arrays.asList(tags.split(COMMA))); + } + + if (Objects.nonNull(operations) && !operations.isEmpty()) { + operationList.addAll(Arrays.asList(operations.split(COMMA))); + } + + return new Filter(tagList, operationList); + } + + @Override + public String getName() { + return OPENAPI_FLATTEN_CMD; + } + + @Override + public void printLongDesc(StringBuilder stringBuilder) { + //This is the long description of the command and all handle within help command + } + + @Override + public void printUsage(StringBuilder stringBuilder) { + //This is the usage description of the command and all handle within help command + } + + @Override + public void setParentCmdParser(CommandLine commandLine) { + //This is not used in this command + } + + private static void exitError() { + Runtime.getRuntime().exit(1); + } +} diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java index 8689eb5c4..0450ebe23 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java @@ -84,7 +84,7 @@ @CommandLine.Command( name = "openapi", description = "Generate the Ballerina sources for a given OpenAPI definition and vice versa.", - subcommands = {Add.class} + subcommands = {Add.class, Flatten.class} ) public class OpenApiCmd implements BLauncherCmd { private static final String CMD_NAME = "openapi"; diff --git a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help new file mode 100644 index 000000000..6b053be55 --- /dev/null +++ b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help @@ -0,0 +1,53 @@ +NAME + bal openapi flatten - Flatten the OpenAPI contract file + +SYNOPSIS + bal openapi flatten [-i | --input] + [-o | --output] + [-n | --name] + [-f | --format] [json|yaml] + [-t | --tags] + [--operations] + +DESCRIPTION + Flatten the OpenAPI contract file by moving all the inline schemas to the + components section. + +OPTIONS + -i, --input + This is a mandatory input. The given OpenAPI contract will be flatten. + The OpenAPI contract can be either a YAML or a JSON. + + -o, --output + This is an optional input. The given output file path will be used to + save the flatten OpenAPI contract. The default output file path is the + executed directory. + + -n, --name + This is an optional input. The given name will be used to save the + flatten OpenAPI contract. The default name is `flattened_openapi`. + + -f, --format [json|yaml] + This is an optional input. The flatten OpenAPI contract will be saved + in given format. The format can be either JSON or YAML. + The default format is same as the input format. + + -t, --tags + This is an optional input. The flatten OpenAPI contract will only have + the operations with the given tags. + + --operations + This is an optional input. The flatten OpenAPI contract will only have + the given operations. + +EXAMPLES + Flatten the `service.yaml` OpenAPI contract file. + $ bal openapi flatten -i service.yaml + + Flatten the `service.yaml` OpenAPI contract file and save it as + `flatten_service.json` file. + $ bal openapi flatten -i hello.yaml -n flatten_service -f json + + Flatten the `service.json` OpenAPI contract file by filtering the + operations with the `service` tag. + $ bal openapi flatten -i service.json -t service From 42d4c6bef8f096082c84cd0c03b75cf5a33b4ff7 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 09:53:02 +0530 Subject: [PATCH 02/12] Refactor flatten logic --- .../io/ballerina/openapi/cmd/Flatten.java | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index d7637aec7..e4819d467 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.parser.core.models.ParseOptions; import io.swagger.v3.parser.core.models.SwaggerParseResult; +import io.swagger.v3.parser.util.InlineModelResolver; import picocli.CommandLine; import java.io.IOException; @@ -20,6 +21,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import static io.ballerina.openapi.cmd.CmdConstants.JSON_EXTENSION; import static io.ballerina.openapi.cmd.CmdConstants.OPENAPI_FLATTEN_CMD; @@ -123,7 +125,7 @@ private void populateInputOptions() { } if (Objects.isNull(name)) { - name = "flatten_openapi"; + name = "flattened_openapi"; } if (Objects.nonNull(outputPath)) { @@ -146,12 +148,23 @@ private void generateFlattenOpenAPI() { return; } - SwaggerParseResult parseResult = getOpenAPIWithFlattenOption(openAPIFileContent); + Optional openAPIOptional = getFlattenOpenAPI(openAPIFileContent); + if (openAPIOptional.isEmpty()) { + exitError(); + return; + } + writeFlattenOpenAPIFile(openAPIOptional.get()); + } + + private Optional getFlattenOpenAPI(String openAPIFileContent) { + // Read the contents of the file with default parser options + // Flattening will be done after filtering the operations + SwaggerParseResult parseResult = new OpenAPIParser().readContents(openAPIFileContent, null, + new ParseOptions()); if (!parseResult.getMessages().isEmpty()) { if (parseResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION); - exitError(); - return; + return Optional.empty(); } } @@ -162,20 +175,14 @@ private void generateFlattenOpenAPI() { errorStream.println(FOUND_PARSER_DIAGNOSTICS); parseResult.getMessages().forEach(errorStream::println); } - exitError(); - return; + return Optional.empty(); } filterOpenAPIOperations(openAPI); - writeFlattenOpenAPIFile(openAPI); - } - - private static SwaggerParseResult getOpenAPIWithFlattenOption(String openAPIFileContent) { - ParseOptions parseOptions = new ParseOptions(); - parseOptions.setFlatten(true); - parseOptions.setCamelCaseFlattenNaming(true); - parseOptions.setFlattenComposedSchemas(true); - return new OpenAPIParser().readContents(openAPIFileContent, null, parseOptions); + // Flatten the OpenAPI definition with `flattenComposedSchemas: true` and `camelCaseFlattenNaming: true` + InlineModelResolver inlineModelResolver = new InlineModelResolver(true, true); + inlineModelResolver.flatten(openAPI); + return Optional.of(openAPI); } private void writeFlattenOpenAPIFile(OpenAPI openAPI) { @@ -204,6 +211,7 @@ private void filterOpenAPIOperations(OpenAPI openAPI) { return; } + // Remove the operations which are not present in the filter openAPI.getPaths().forEach((path, pathItem) -> pathItem.readOperationsMap() .forEach((httpMethod, operation) -> { if (!filter.getOperations().contains(operation.getOperationId()) && @@ -212,6 +220,15 @@ private void filterOpenAPIOperations(OpenAPI openAPI) { } }) ); + + // Remove the paths which do not have any operations after filtering + List pathsToRemove = new ArrayList<>(); + openAPI.getPaths().forEach((path, pathItem) -> { + if (pathItem.readOperationsMap().isEmpty()) { + pathsToRemove.add(path); + } + }); + pathsToRemove.forEach(openAPI.getPaths()::remove); } private Filter getFilter() { From a064441f3bbd9789001bffe274c89f392bf9645b Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 09:53:12 +0530 Subject: [PATCH 03/12] Add positive test cases --- .../ballerina/openapi/cmd/OpenAPICmdTest.java | 100 ++++++++++++ .../flatten/flattened_openapi_expected.json | 145 ++++++++++++++++++ .../flatten/flattened_openapi_expected.yaml | 99 ++++++++++++ .../flattened_openapi_expected_list_pets.json | 66 ++++++++ .../flattened_openapi_expected_pets.json | 102 ++++++++++++ .../test/resources/cmd/flatten/openapi.json | 145 ++++++++++++++++++ .../test/resources/cmd/flatten/openapi.yaml | 92 +++++++++++ 7 files changed, 749 insertions(+) create mode 100644 openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.json create mode 100644 openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.yaml create mode 100644 openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_list_pets.json create mode 100644 openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_pets.json create mode 100644 openapi-cli/src/test/resources/cmd/flatten/openapi.json create mode 100644 openapi-cli/src/test/resources/cmd/flatten/openapi.yaml diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java index 980ce071a..ba7319b95 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java @@ -915,6 +915,106 @@ public void testAddCmd() throws IOException { Assert.assertTrue(tomlContent.contains(generatedTool)); } + @Test(description = "Test openapi flatten sub command with default options with the json file") + public void testFlattenCmdDefaultJson() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected.json")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-o", tmpDir.toString()}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.json")); + } + + private void compareFiles(Path expectedFilePath, Path generatedFilePath) throws IOException { + if (Files.exists(generatedFilePath)) { + String expectedOpenAPIContent = ""; + try (Stream expectedOpenAPIContentLines = Files.lines(expectedFilePath)) { + expectedOpenAPIContent = expectedOpenAPIContentLines.collect(Collectors.joining(LINE_SEPARATOR)); + } catch (IOException e) { + Files.deleteIfExists(generatedFilePath); + Assert.fail(e.getMessage()); + } + + String generatedOpenAPIContent = ""; + try (Stream generatedOpenAPIContentLines = + Files.lines(generatedFilePath)) { + generatedOpenAPIContent = generatedOpenAPIContentLines.collect(Collectors.joining(LINE_SEPARATOR)); + } catch (IOException e) { + Files.deleteIfExists(generatedFilePath); + Assert.fail(e.getMessage()); + } + + generatedOpenAPIContent = (generatedOpenAPIContent.trim()).replaceAll("\\s+", ""); + expectedOpenAPIContent = (expectedOpenAPIContent.trim()).replaceAll("\\s+", ""); + Assert.assertEquals(expectedOpenAPIContent, generatedOpenAPIContent); + + Files.deleteIfExists(generatedFilePath); + } else { + Assert.fail("Generation failed: " + readOutput(true)); + } + } + + @Test(description = "Test openapi flatten sub command with default options with the yaml file") + public void testFlattenCmdDefaultYaml() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected.yaml")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.yaml", "-o", tmpDir.toString()}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.yaml")); + } + + @Test(description = "Test openapi flatten sub command with the name option") + public void testFlattenCmdName() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected.json")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-o", tmpDir.toString(), "-n", "flattened"}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened.json")); + } + + @Test(description = "Test openapi flatten sub command with the json format option") + public void testFlattenCmdJsonFormat() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected.json")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.yaml", "-o", tmpDir.toString(), "-f", "json"}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.json")); + } + + @Test(description = "Test openapi flatten sub command with the yaml format option") + public void testFlattenCmdYamlFormat() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected.yaml")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-o", tmpDir.toString(), "-f", "yaml"}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.yaml")); + } + + @Test(description = "Test openapi flatten sub command with the tags option") + public void testFlattenCmdTags() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected_pets.json")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-o", tmpDir.toString(), "-t", "pets"}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.json")); + } + + @Test(description = "Test openapi flatten sub command with the operations option") + public void testFlattenCmdOperations() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_expected_list_pets.json")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-o", tmpDir.toString(), "--operations", + "listPets"}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.json")); + } + @AfterTest public void clean() { System.setErr(null); diff --git a/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.json b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.json new file mode 100644 index 000000000..0fe12171f --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.json @@ -0,0 +1,145 @@ +{ + "openapi" : "3.0.0", + "info" : { + "title" : "Swagger Petstore", + "license" : { + "name" : "MIT" + }, + "version" : "1.0.0" + }, + "servers" : [ { + "url" : "/" + } ], + "paths" : { + "/pets" : { + "get" : { + "tags" : [ "pets" ], + "summary" : "List all pets", + "operationId" : "listPets", + "responses" : { + "200" : { + "description" : "An paged array of pets", + "headers" : { + "x-next" : { + "description" : "A link to the next page of responses", + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } + }, + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/InlineResponse200" + } + } + } + } + } + } + }, + "post" : { + "tags" : [ "pets" ], + "summary" : "Create a pet", + "operationId" : "createPets", + "requestBody" : { + "description" : "Created pet object", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PetsBody" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "Null response" + } + } + } + }, + "/locations" : { + "get" : { + "tags" : [ "locations" ], + "summary" : "List all locations", + "operationId" : "listLocations", + "parameters" : [ { + "name" : "area", + "in" : "query", + "description" : "area to filter by", + "required" : false, + "style" : "form", + "explode" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "An paged array of locations", + "headers" : { + "x-next" : { + "description" : "A link to the next page of responses", + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } + }, + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/PetsBody" + } + } + } + } + } + } + } + } + }, + "components" : { + "schemas" : { + "InlineResponse200" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64" + }, + "name" : { + "type" : "string" + }, + "tag" : { + "type" : "string" + } + } + }, + "PetsBody" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64" + }, + "name" : { + "type" : "string" + }, + "tag" : { + "type" : "string" + } + } + } + } + } +} diff --git a/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.yaml b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.yaml new file mode 100644 index 000000000..8e25637c6 --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected.yaml @@ -0,0 +1,99 @@ +openapi: 3.0.0 +info: + title: Swagger Petstore + license: + name: MIT + version: 1.0.0 +servers: + - url: / +paths: + /pets: + get: + tags: + - pets + summary: List all pets + operationId: listPets + responses: + "200": + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + style: simple + explode: false + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InlineResponse200" + post: + tags: + - pets + summary: Create a pet + operationId: createPets + requestBody: + description: Created pet object + content: + application/json: + schema: + $ref: "#/components/schemas/PetsBody" + required: true + responses: + "201": + description: Null response + /locations: + get: + tags: + - locations + summary: List all locations + operationId: listLocations + parameters: + - name: area + in: query + description: area to filter by + required: false + style: form + explode: true + schema: + type: string + responses: + "200": + description: An paged array of locations + headers: + x-next: + description: A link to the next page of responses + style: simple + explode: false + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PetsBody" +components: + schemas: + InlineResponse200: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + PetsBody: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string diff --git a/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_list_pets.json b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_list_pets.json new file mode 100644 index 000000000..6d6e51f6b --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_list_pets.json @@ -0,0 +1,66 @@ +{ + "openapi" : "3.0.0", + "info" : { + "title" : "Swagger Petstore", + "license" : { + "name" : "MIT" + }, + "version" : "1.0.0" + }, + "servers" : [ { + "url" : "/" + } ], + "paths" : { + "/pets" : { + "get" : { + "tags" : [ "pets" ], + "summary" : "List all pets", + "operationId" : "listPets", + "responses" : { + "200" : { + "description" : "An paged array of pets", + "headers" : { + "x-next" : { + "description" : "A link to the next page of responses", + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } + }, + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/InlineResponse200" + } + } + } + } + } + } + } + } + }, + "components" : { + "schemas" : { + "InlineResponse200" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64" + }, + "name" : { + "type" : "string" + }, + "tag" : { + "type" : "string" + } + } + } + } + } +} diff --git a/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_pets.json b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_pets.json new file mode 100644 index 000000000..7c6531eba --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_expected_pets.json @@ -0,0 +1,102 @@ +{ + "openapi" : "3.0.0", + "info" : { + "title" : "Swagger Petstore", + "license" : { + "name" : "MIT" + }, + "version" : "1.0.0" + }, + "servers" : [ { + "url" : "/" + } ], + "paths" : { + "/pets" : { + "get" : { + "tags" : [ "pets" ], + "summary" : "List all pets", + "operationId" : "listPets", + "responses" : { + "200" : { + "description" : "An paged array of pets", + "headers" : { + "x-next" : { + "description" : "A link to the next page of responses", + "style" : "simple", + "explode" : false, + "schema" : { + "type" : "string" + } + } + }, + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/InlineResponse200" + } + } + } + } + } + } + }, + "post" : { + "tags" : [ "pets" ], + "summary" : "Create a pet", + "operationId" : "createPets", + "requestBody" : { + "description" : "Created pet object", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/PetsBody" + } + } + }, + "required" : true + }, + "responses" : { + "201" : { + "description" : "Null response" + } + } + } + } + }, + "components" : { + "schemas" : { + "InlineResponse200" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64" + }, + "name" : { + "type" : "string" + }, + "tag" : { + "type" : "string" + } + } + }, + "PetsBody" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "integer", + "format" : "int64" + }, + "name" : { + "type" : "string" + }, + "tag" : { + "type" : "string" + } + } + } + } + } +} diff --git a/openapi-cli/src/test/resources/cmd/flatten/openapi.json b/openapi-cli/src/test/resources/cmd/flatten/openapi.json new file mode 100644 index 000000000..2e00b94ed --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/openapi.json @@ -0,0 +1,145 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "responses": { + "200": { + "description": "An paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "requestBody": { + "description": "Created pet object", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Null response" + } + } + } + }, + "/locations": { + "get": { + "summary": "List all locations", + "operationId": "listLocations", + "tags": [ + "locations" + ], + "parameters": [ + { + "name": "area", + "in": "query", + "description": "area to filter by", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "An paged array of locations", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-cli/src/test/resources/cmd/flatten/openapi.yaml b/openapi-cli/src/test/resources/cmd/flatten/openapi.yaml new file mode 100644 index 000000000..8ddab3021 --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/openapi.yaml @@ -0,0 +1,92 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + responses: + '200': + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + post: + summary: Create a pet + operationId: createPets + tags: + - pets + requestBody: + description: Created pet object + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + required: true + responses: + '201': + description: Null response + /locations: + get: + summary: List all locations + operationId: listLocations + tags: + - locations + parameters: + - name: area + in: query + description: area to filter by + schema: + type: string + responses: + '200': + description: An paged array of locations + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string From e4213a2233071c04ed3940f5c4c2b47da4964e8e Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 09:57:14 +0530 Subject: [PATCH 04/12] Add license header --- .../java/io/ballerina/openapi/cmd/Flatten.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index e4819d467..05d3f083c 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package io.ballerina.openapi.cmd; import io.ballerina.cli.BLauncherCmd; From 726e2a5b42e080192da59afbdecc6e204cdadb7b Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 10:00:06 +0530 Subject: [PATCH 05/12] Change the field name for file name --- .../src/main/java/io/ballerina/openapi/cmd/Flatten.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index 05d3f083c..46deab0c5 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -95,7 +95,7 @@ public class Flatten implements BLauncherCmd { private String outputPath; @CommandLine.Option(names = {"-n", "--name"}, description = "Name of the flattened OpenAPI definition file.") - private String name; + private String fileName; @CommandLine.Option(names = {"-f", "--format"}, description = "Output format of the flattened OpenAPI definition.") private String format; @@ -141,8 +141,8 @@ private void populateInputOptions() { setDefaultFormat(); } - if (Objects.isNull(name)) { - name = "flattened_openapi"; + if (Objects.isNull(fileName)) { + fileName = "flattened_openapi"; } if (Objects.nonNull(outputPath)) { @@ -215,7 +215,7 @@ private void writeFlattenOpenAPIFile(OpenAPI openAPI) { } private String getOutputFileName() { - return resolveContractFileName(targetPath, name + getFileExtension(), format.equals("json")); + return resolveContractFileName(targetPath, fileName + getFileExtension(), format.equals("json")); } private String getFileExtension() { From dd5a48e7a7fbd52d0cbf1eaa5d1e40a6fcd1cbd2 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 10:22:59 +0530 Subject: [PATCH 06/12] Fix logging errors --- .../main/java/io/ballerina/openapi/cmd/Flatten.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index 46deab0c5..f4cb4a0f4 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -178,17 +178,16 @@ private Optional getFlattenOpenAPI(String openAPIFileContent) { // Flattening will be done after filtering the operations SwaggerParseResult parseResult = new OpenAPIParser().readContents(openAPIFileContent, null, new ParseOptions()); - if (!parseResult.getMessages().isEmpty()) { - if (parseResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { - errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION); - return Optional.empty(); - } + if (!parseResult.getMessages().isEmpty() && + parseResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { + errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION); + return Optional.empty(); } OpenAPI openAPI = parseResult.getOpenAPI(); if (Objects.isNull(openAPI)) { errorStream.println(ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE); - if (Objects.nonNull(parseResult.getMessages())) { + if (!parseResult.getMessages().isEmpty()) { errorStream.println(FOUND_PARSER_DIAGNOSTICS); parseResult.getMessages().forEach(errorStream::println); } From a9bf37594266785cc850c736e4327a01574746b5 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 12:40:16 +0530 Subject: [PATCH 07/12] Add sanitization logic for generated flatten schema names --- .../io/ballerina/openapi/cmd/Flatten.java | 74 ++++++++- .../cli-help/ballerina-openapi-flatten.help | 2 +- .../ballerina/openapi/cmd/OpenAPICmdTest.java | 10 ++ .../flattened_openapi_composed_schema.yaml | 150 ++++++++++++++++++ .../cmd/flatten/openapi_composed_schema.yaml | 135 ++++++++++++++++ .../core/generators/common/OASModifier.java | 34 ++-- 6 files changed, 383 insertions(+), 22 deletions(-) create mode 100644 openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_composed_schema.yaml create mode 100644 openapi-cli/src/test/resources/cmd/flatten/openapi_composed_schema.yaml diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index f4cb4a0f4..973035b56 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -18,12 +18,15 @@ package io.ballerina.openapi.cmd; import io.ballerina.cli.BLauncherCmd; +import io.ballerina.openapi.core.generators.common.OASModifier; import io.ballerina.openapi.core.generators.common.model.Filter; import io.ballerina.openapi.service.mapper.utils.CodegenUtils; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.parser.core.models.ParseOptions; import io.swagger.v3.parser.core.models.SwaggerParseResult; import io.swagger.v3.parser.util.InlineModelResolver; @@ -36,7 +39,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -45,6 +51,8 @@ import static io.ballerina.openapi.cmd.CmdConstants.YAML_EXTENSION; import static io.ballerina.openapi.cmd.CmdConstants.YML_EXTENSION; import static io.ballerina.openapi.core.generators.common.GeneratorConstants.UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE; +import static io.ballerina.openapi.core.generators.common.OASModifier.getResolvedNameMapping; +import static io.ballerina.openapi.core.generators.common.OASModifier.getValidNameForType; import static io.ballerina.openapi.service.mapper.utils.CodegenUtils.resolveContractFileName; /** @@ -79,6 +87,8 @@ public class Flatten implements BLauncherCmd { private static final String FOUND_PARSER_DIAGNOSTICS = "found the following parser diagnostic messages:"; private static final String ERROR_OCCURRED_WHILE_WRITING_THE_OUTPUT_OPENAPI_FILE = "[ERROR] error occurred while " + "writing the flattened OpenAPI definition file"; + private static final String ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES = "[ERROR] error occurred while " + + "generating schema names"; private final PrintStream infoStream = System.out; private final PrintStream errorStream = System.err; @@ -176,29 +186,35 @@ private void generateFlattenOpenAPI() { private Optional getFlattenOpenAPI(String openAPIFileContent) { // Read the contents of the file with default parser options // Flattening will be done after filtering the operations - SwaggerParseResult parseResult = new OpenAPIParser().readContents(openAPIFileContent, null, + SwaggerParseResult parserResult = new OpenAPIParser().readContents(openAPIFileContent, null, new ParseOptions()); - if (!parseResult.getMessages().isEmpty() && - parseResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { + if (!parserResult.getMessages().isEmpty() && + parserResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) { errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION); return Optional.empty(); } - OpenAPI openAPI = parseResult.getOpenAPI(); + OpenAPI openAPI = parserResult.getOpenAPI(); if (Objects.isNull(openAPI)) { errorStream.println(ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE); - if (!parseResult.getMessages().isEmpty()) { + if (!parserResult.getMessages().isEmpty()) { errorStream.println(FOUND_PARSER_DIAGNOSTICS); - parseResult.getMessages().forEach(errorStream::println); + parserResult.getMessages().forEach(errorStream::println); } return Optional.empty(); } + Components components = openAPI.getComponents(); + List existingComponentNames = Objects.nonNull(components) && Objects.nonNull(components.getSchemas()) ? + new ArrayList<>(components.getSchemas().keySet()) : new ArrayList<>(); + filterOpenAPIOperations(openAPI); + // Flatten the OpenAPI definition with `flattenComposedSchemas: true` and `camelCaseFlattenNaming: true` InlineModelResolver inlineModelResolver = new InlineModelResolver(true, true); inlineModelResolver.flatten(openAPI); - return Optional.of(openAPI); + + return sanitizeOpenAPI(openAPI, existingComponentNames); } private void writeFlattenOpenAPIFile(OpenAPI openAPI) { @@ -247,6 +263,50 @@ private void filterOpenAPIOperations(OpenAPI openAPI) { pathsToRemove.forEach(openAPI.getPaths()::remove); } + private Optional sanitizeOpenAPI(OpenAPI openAPI, List existingComponentNames) { + Map proposedNameMapping = getProposedNameMapping(openAPI, existingComponentNames); + if (proposedNameMapping.isEmpty()) { + return Optional.of(openAPI); + } + + SwaggerParseResult parserResult = OASModifier.getOASWithSchemaNameModification(openAPI, proposedNameMapping); + openAPI = parserResult.getOpenAPI(); + if (Objects.isNull(openAPI)) { + errorStream.println(ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES); + if (!parserResult.getMessages().isEmpty()) { + errorStream.println(FOUND_PARSER_DIAGNOSTICS); + parserResult.getMessages().forEach(errorStream::println); + } + return Optional.empty(); + } + + return Optional.of(openAPI); + } + + public Map getProposedNameMapping(OpenAPI openapi, List existingComponentNames) { + Map nameMap = new HashMap<>(); + if (Objects.isNull(openapi.getComponents())) { + return Collections.emptyMap(); + } + Components components = openapi.getComponents(); + Map schemas = components.getSchemas(); + if (Objects.isNull(schemas)) { + return Collections.emptyMap(); + } + + for (Map.Entry schemaEntry: schemas.entrySet()) { + if (existingComponentNames.contains(schemaEntry.getKey())) { + continue; + } + String modifiedName = getValidNameForType(schemaEntry.getKey()); + if (modifiedName.equals(schemaEntry.getKey())) { + continue; + } + nameMap.put(schemaEntry.getKey(), modifiedName); + } + return getResolvedNameMapping(nameMap); + } + private Filter getFilter() { List tagList = new ArrayList<>(); List operationList = new ArrayList<>(); diff --git a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help index 6b053be55..fd2625281 100644 --- a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help +++ b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help @@ -11,7 +11,7 @@ SYNOPSIS DESCRIPTION Flatten the OpenAPI contract file by moving all the inline schemas to the - components section. + components section. The generated schema names will be Ballerina friendly. OPTIONS -i, --input diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java index ba7319b95..aa6b2869e 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/OpenAPICmdTest.java @@ -1015,6 +1015,16 @@ public void testFlattenCmdOperations() throws IOException { compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.json")); } + @Test(description = "Test openapi flatten sub command with composed schema") + public void testFlattenCmdWithComposedSchema() throws IOException { + Path expectedFilePath = resourceDir.resolve(Paths.get("cmd/flatten/flattened_openapi_composed_schema.yaml")); + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi_composed_schema.yaml", "-o", tmpDir.toString()}; + Flatten flatten = new Flatten(); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + compareFiles(expectedFilePath, tmpDir.resolve("flattened_openapi.yaml")); + } + @AfterTest public void clean() { System.setErr(null); diff --git a/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_composed_schema.yaml b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_composed_schema.yaml new file mode 100644 index 000000000..8850cc79b --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/flattened_openapi_composed_schema.yaml @@ -0,0 +1,150 @@ +openapi: 3.0.1 +info: + title: Swagger Petstore + license: + name: MIT + version: 1.0.0 +servers: + - url: / +paths: + /pets: + get: + tags: + - pets + summary: List all pets + operationId: listPets + responses: + "200": + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + style: simple + explode: false + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/InlineResponse200" + post: + tags: + - pets + summary: Create a pet + operationId: createPets + requestBody: + description: Created pet object + content: + application/json: + schema: + $ref: "#/components/schemas/PetsBody" + required: true + responses: + "201": + description: Null response + /pets/{petId}: + get: + tags: + - pets + summary: Info for a specific pet + operationId: showPetById + parameters: + - name: petId + in: path + description: The id of the pet to retrieve + required: true + style: simple + explode: false + schema: + type: integer + format: int64 + responses: + "200": + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/InlineResponse2001" + /locations: + get: + tags: + - locations + summary: List all locations + operationId: listLocations + parameters: + - name: area + in: query + description: area to filter by + required: false + style: form + explode: true + schema: + type: string + responses: + "200": + description: An paged array of locations + headers: + x-next: + description: A link to the next page of responses + style: simple + explode: false + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PetsOneOf1" +components: + schemas: + PetspetsOneOf12: + type: object + properties: + name: + type: string + tag: + type: string + InlineResponse2001: + oneOf: + - type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + - type: object + properties: + code: + type: integer + message: + type: string + InlineResponse200: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + PetsOneOf1: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + PetsBody: + oneOf: + - $ref: "#/components/schemas/PetsOneOf1" + - $ref: "#/components/schemas/PetspetsOneOf12" diff --git a/openapi-cli/src/test/resources/cmd/flatten/openapi_composed_schema.yaml b/openapi-cli/src/test/resources/cmd/flatten/openapi_composed_schema.yaml new file mode 100644 index 000000000..1d77298ae --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/openapi_composed_schema.yaml @@ -0,0 +1,135 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + responses: + '200': + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + post: + summary: Create a pet + operationId: createPets + tags: + - pets + requestBody: + description: Created pet object + content: + application/json: + schema: + oneOf: + - type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + - type: object + properties: + name: + type: string + tag: + type: string + required: true + responses: + '201': + description: Null response + /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: integer + format: int64 + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + oneOf: + - type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + - type: object + properties: + code: + type: integer + message: + type: string + /locations: + get: + summary: List all locations + operationId: listLocations + tags: + - locations + parameters: + - name: area + in: query + description: area to filter by + schema: + type: string + responses: + '200': + description: An paged array of locations + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/OASModifier.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/OASModifier.java index d4ad288f7..130cf04e8 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/OASModifier.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/OASModifier.java @@ -82,7 +82,7 @@ public Map getProposedNameMapping(OpenAPI openapi) { return getResolvedNameMapping(nameMap); } - private static Map getResolvedNameMapping(Map nameMap) { + public static Map getResolvedNameMapping(Map nameMap) { Map resolvedNames = new HashMap<>(); Map nameCount = new HashMap<>(); @@ -132,9 +132,26 @@ public OpenAPI modifyWithBallerinaConventions(OpenAPI openapi) throws BallerinaO private static OpenAPI modifyOASWithSchemaName(OpenAPI openapi, Map nameMap) throws BallerinaOpenApiException { + SwaggerParseResult parseResult = getOASWithSchemaNameModification(openapi, nameMap); + OpenAPI openAPI = parseResult.getOpenAPI(); + if (Objects.isNull(openAPI)) { + List messages = parseResult.getMessages(); + if (Objects.nonNull(messages) && !messages.isEmpty()) { + outErrorStream.println("Schema name sanitization contains errors: "); + messages.forEach(outErrorStream::println); + } + throw new BallerinaOpenApiException("Failed to generate the sanitized OpenAPI specification. Please " + + "consider generating the client/service without the sanitization option."); + } + return openAPI; + } + + public static SwaggerParseResult getOASWithSchemaNameModification(OpenAPI openapi, Map nameMap) { Components components = openapi.getComponents(); if (Objects.isNull(components) || nameMap.isEmpty() || Objects.isNull(components.getSchemas())) { - return openapi; + SwaggerParseResult result = new SwaggerParseResult(); + result.setOpenAPI(openapi); + return result; } Map schemas = components.getSchemas(); @@ -169,18 +186,7 @@ private static OpenAPI modifyOASWithSchemaName(OpenAPI openapi, Map messages = parseResult.getMessages(); - if (Objects.nonNull(messages) && !messages.isEmpty()) { - outErrorStream.println("Sanitized OpenAPI contains errors: "); - messages.forEach(outErrorStream::println); - } - throw new BallerinaOpenApiException("Failed to generate the sanitized OpenAPI specification. Please " + - "consider generating the client/service without the sanitization option."); - } - return openAPI; + return new OpenAPIParser().readContents(openApiJson, null, parseOptions); } private static PathDetails updateParameterNameDetails(Map.Entry path) { From ea9f2a17fb5854887443ce4a2057c444fed8b9be Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 12:58:14 +0530 Subject: [PATCH 08/12] Refactor error messages and constructors --- .../io/ballerina/openapi/cmd/Flatten.java | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index 973035b56..cabbfbcc1 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -70,30 +70,31 @@ public class Flatten implements BLauncherCmd { private static final String COMMAND_IDENTIFIER = "openapi-flatten"; private static final String COMMA = ","; - private static final String INFO_OUTPUT_WRITTEN_MSG = "[INFO] flattened OpenAPI definition file was successfully" + + private static final String INFO_OUTPUT_WRITTEN_MSG = "INFO: flattened OpenAPI definition file was successfully" + " written to: %s%n"; - private static final String WARNING_INVALID_OUTPUT_FORMAT = "[WARNING] invalid output format. The output format" + + private static final String WARNING_INVALID_OUTPUT_FORMAT = "WARNING: invalid output format. The output format" + " should be either \"json\" or \"yaml\".Defaulting to format of the input file."; - private static final String ERROR_INPUT_PATH_IS_REQUIRED = "[ERROR] an OpenAPI definition path is required to " + + private static final String ERROR_INPUT_PATH_IS_REQUIRED = "ERROR: an OpenAPI definition path is required to " + "flatten the OpenAPI definition."; - private static final String ERROR_INVALID_INPUT_FILE_EXTENSION = "[ERROR] invalid input OpenAPI definition file " + + private static final String ERROR_INVALID_INPUT_FILE_EXTENSION = "ERROR: invalid input OpenAPI definition file " + "extension. The OpenAPI definition file should be in YAML or JSON format."; - private static final String ERROR_OCCURRED_WHILE_READING_THE_INPUT_FILE = "[ERROR] error occurred while reading " + + private static final String ERROR_OCCURRED_WHILE_READING_THE_INPUT_FILE = "ERROR: error occurred while reading " + "the OpenAPI definition file."; - private static final String ERROR_UNSUPPORTED_OPENAPI_VERSION = "[ERROR] provided OpenAPI contract version is " + + private static final String ERROR_UNSUPPORTED_OPENAPI_VERSION = "ERROR: provided OpenAPI contract version is " + "not supported in the tool. Use OpenAPI specification version 2 or higher"; - private static final String ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE = "[Error] error occurred while " + + private static final String ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE = "ERROR: error occurred while " + "parsing the OpenAPI definition file."; private static final String FOUND_PARSER_DIAGNOSTICS = "found the following parser diagnostic messages:"; - private static final String ERROR_OCCURRED_WHILE_WRITING_THE_OUTPUT_OPENAPI_FILE = "[ERROR] error occurred while " + + private static final String ERROR_OCCURRED_WHILE_WRITING_THE_OUTPUT_OPENAPI_FILE = "ERROR: error occurred while " + "writing the flattened OpenAPI definition file"; - private static final String ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES = "[ERROR] error occurred while " + + private static final String ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES = "ERROR: error occurred while " + "generating schema names"; - private final PrintStream infoStream = System.out; - private final PrintStream errorStream = System.err; + private PrintStream infoStream = System.out; + private PrintStream errorStream = System.err; private Path targetPath = Paths.get(System.getProperty("user.dir")); + private boolean exitWhenFinish = true; @CommandLine.Option(names = {"-h", "--help"}, hidden = true) public boolean helpFlag; @@ -116,6 +117,14 @@ public class Flatten implements BLauncherCmd { @CommandLine.Option(names = {"--operations"}, description = "Operations that need to be included when flattening.") public String operations; + public Flatten() { + } + + public Flatten(PrintStream errorStream, boolean exitWhenFinish) { + this.errorStream = errorStream; + this.exitWhenFinish = exitWhenFinish; + } + @Override public void execute() { if (helpFlag) { @@ -342,7 +351,9 @@ public void setParentCmdParser(CommandLine commandLine) { //This is not used in this command } - private static void exitError() { - Runtime.getRuntime().exit(1); + private void exitError() { + if (exitWhenFinish) { + Runtime.getRuntime().exit(1); + } } } From a692d7caf154d6ef57804deffdd8571c8eefd0d9 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 12:58:24 +0530 Subject: [PATCH 09/12] Add negative tests --- .../openapi/cmd/NegativeCmdTests.java | 54 +++++++++++ .../cmd/flatten/openapi_invalid.json | 92 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 openapi-cli/src/test/resources/cmd/flatten/openapi_invalid.json diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java index 65f108e16..5755f11e2 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/cmd/NegativeCmdTests.java @@ -43,4 +43,58 @@ public void testInvalidBallerinaPackage() throws IOException { String output = readOutput(true); Assert.assertTrue(output.contains("ERROR: invalid Ballerina package directory:")); } + + @Test(description = "Test without the input OpenAPI file in `flatten` sub command") + public void testFlattenWithOutInputOpenAPIFile() throws IOException { + String[] addArgs = {"-f", "json"}; + Flatten flatten = new Flatten(printStream, false); + new CommandLine(flatten).parseArgs(addArgs); + flatten.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("ERROR: an OpenAPI definition path is required to flatten the OpenAPI " + + "definition.")); + } + + @Test(description = "Test with the invalid input OpenAPI file in `flatten` sub command") + public void testFlattenWithInvalidInputOpenAPIFile() throws IOException { + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi-1.json"}; + Flatten flatten = new Flatten(printStream, false); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("ERROR: error occurred while reading the OpenAPI definition file.")); + } + + @Test(description = "Test with invalid invalid input OpenAPI file extension in `flatten` sub command") + public void testFlattenWithInvalidInputOpenAPIFileExtension() throws IOException { + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.txt"}; + Flatten flatten = new Flatten(printStream, false); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("ERROR: invalid input OpenAPI definition file extension. The OpenAPI " + + "definition file should be in YAML or JSON format.")); + } + + @Test(description = "Test with the invalid output OpenAPI file format in `flatten` sub command") + public void testFlattenWithInvalidOutputOpenAPIFileFormat() throws IOException { + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi.json", "-f", "txt", "-o", tmpDir.toString()}; + Flatten flatten = new Flatten(printStream, false); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("WARNING: invalid output format. The output format should be either " + + "\"json\" or \"yaml\".Defaulting to format of the input file.")); + } + + @Test(description = "Test with the input OpenAPI file in `flatten` sub command which has parsing issues") + public void testFlattenWithInputOpenAPIFileParsingIssues() throws IOException { + String[] args = {"-i", resourceDir + "/cmd/flatten/openapi_invalid.json", "-f", "txt", "-o", tmpDir.toString()}; + Flatten flatten = new Flatten(printStream, false); + new CommandLine(flatten).parseArgs(args); + flatten.execute(); + String output = readOutput(true); + Assert.assertTrue(output.contains("WARNING: invalid output format. The output format should be either " + + "\"json\" or \"yaml\".Defaulting to format of the input file.")); + } } diff --git a/openapi-cli/src/test/resources/cmd/flatten/openapi_invalid.json b/openapi-cli/src/test/resources/cmd/flatten/openapi_invalid.json new file mode 100644 index 000000000..8ddab3021 --- /dev/null +++ b/openapi-cli/src/test/resources/cmd/flatten/openapi_invalid.json @@ -0,0 +1,92 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + responses: + '200': + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + post: + summary: Create a pet + operationId: createPets + tags: + - pets + requestBody: + description: Created pet object + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + required: true + responses: + '201': + description: Null response + /locations: + get: + summary: List all locations + operationId: listLocations + tags: + - locations + parameters: + - name: area + in: query + description: area to filter by + schema: + type: string + responses: + '200': + description: An paged array of locations + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string From 50964da6e0bcc91e86669383347ec9080ad9b3b3 Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 13:41:58 +0530 Subject: [PATCH 10/12] Update help text --- .../main/resources/cli-help/ballerina-openapi-flatten.help | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help index fd2625281..29df43ecc 100644 --- a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help +++ b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help @@ -29,8 +29,8 @@ OPTIONS -f, --format [json|yaml] This is an optional input. The flatten OpenAPI contract will be saved - in given format. The format can be either JSON or YAML. - The default format is same as the input format. + in the given format. The format can be either JSON or YAML. The + default format is same as the input file format. -t, --tags This is an optional input. The flatten OpenAPI contract will only have From 40e85dd9f7a7c72fde86d17c1daf44d1779ed069 Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:47:28 +0530 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Sumudu Nissanka --- .../src/main/java/io/ballerina/openapi/cmd/Flatten.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java index cabbfbcc1..d3d17d04c 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java @@ -1,7 +1,7 @@ /* * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -59,7 +59,7 @@ * Main class to implement "flatten" subcommand which is used to flatten the OpenAPI definition * by moving all the inline schemas to the "#/components/schemas" section. * - * @since 1.9.0 + * @since 2.2.0 */ @CommandLine.Command( name = "flatten", From 51aacbab7278fdd298230d430d83c5f3e78efb8b Mon Sep 17 00:00:00 2001 From: TharmiganK Date: Tue, 10 Sep 2024 15:35:56 +0530 Subject: [PATCH 12/12] Update the description --- .../main/resources/cli-help/ballerina-openapi-flatten.help | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help index 29df43ecc..0356f0dce 100644 --- a/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help +++ b/openapi-cli/src/main/resources/cli-help/ballerina-openapi-flatten.help @@ -10,8 +10,9 @@ SYNOPSIS [--operations] DESCRIPTION - Flatten the OpenAPI contract file by moving all the inline schemas to the - components section. The generated schema names will be Ballerina friendly. + Make the OpenAPI contract more readable by relocating all inline + embedded schemas to the components section and assigning each a + unique, Ballerina-friendly name. OPTIONS -i, --input