Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce bal openapi sanitize sub command #1770

Merged
merged 18 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ 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,
Expand Down
264 changes: 16 additions & 248 deletions openapi-cli/src/main/java/io/ballerina/openapi/cmd/Flatten.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,24 @@
*/
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;
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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;
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;

/**
* Main class to implement "flatten" subcommand which is used to flatten the OpenAPI definition
Expand All @@ -66,213 +47,41 @@
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 = ",";
public class Flatten extends SubCmdBase {

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 static final String ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES = "ERROR: error occurred while " +
"generating schema names";

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;

@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 fileName;

@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;

public Flatten() {
super(CommandType.FLATTEN);
}

public Flatten(PrintStream errorStream, boolean exitWhenFinish) {
this.errorStream = errorStream;
this.exitWhenFinish = exitWhenFinish;
super(CommandType.FLATTEN, errorStream, exitWhenFinish);
}

@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();
public String getDefaultFileName() {
return "flattened_openapi";
}

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(fileName)) {
fileName = "flattened_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;
}

Optional<OpenAPI> openAPIOptional = getFlattenOpenAPI(openAPIFileContent);
if (openAPIOptional.isEmpty()) {
exitError();
return;
}
writeFlattenOpenAPIFile(openAPIOptional.get());
}

private Optional<OpenAPI> getFlattenOpenAPI(String openAPIFileContent) {
// Read the contents of the file with default parser options
// Flattening will be done after filtering the operations
SwaggerParseResult parserResult = new OpenAPIParser().readContents(openAPIFileContent, null,
new ParseOptions());
if (!parserResult.getMessages().isEmpty() &&
parserResult.getMessages().contains(UNSUPPORTED_OPENAPI_VERSION_PARSER_MESSAGE)) {
errorStream.println(ERROR_UNSUPPORTED_OPENAPI_VERSION);
return Optional.empty();
}

OpenAPI openAPI = parserResult.getOpenAPI();
if (Objects.isNull(openAPI)) {
errorStream.println(ERROR_OCCURRED_WHILE_PARSING_THE_INPUT_OPENAPI_FILE);
if (!parserResult.getMessages().isEmpty()) {
errorStream.println(FOUND_PARSER_DIAGNOSTICS);
parserResult.getMessages().forEach(errorStream::println);
}
@Override
public Optional<OpenAPI> generate(String openAPIFileContent) {
Optional<OpenAPI> filteredOpenAPI = getFilteredOpenAPI(openAPIFileContent);
if (filteredOpenAPI.isEmpty()) {
return Optional.empty();
}

OpenAPI openAPI = filteredOpenAPI.get();
Components components = openAPI.getComponents();
List<String> 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 sanitizeOpenAPI(openAPI, existingComponentNames);
}

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();
}
return getFlattenOpenAPI(openAPI)
.flatMap(flattenOpenAPI -> sanitizeGeneratedFlattenNames(flattenOpenAPI, existingComponentNames));
}

private String getOutputFileName() {
return resolveContractFileName(targetPath, fileName + 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;
}

// 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()) &&
operation.getTags().stream().noneMatch(filter.getTags()::contains)) {
pathItem.operation(httpMethod, null);
}
})
);

// Remove the paths which do not have any operations after filtering
List<String> pathsToRemove = new ArrayList<>();
openAPI.getPaths().forEach((path, pathItem) -> {
if (pathItem.readOperationsMap().isEmpty()) {
pathsToRemove.add(path);
}
});
pathsToRemove.forEach(openAPI.getPaths()::remove);
}

private Optional<OpenAPI> sanitizeOpenAPI(OpenAPI openAPI, List<String> existingComponentNames) {
private Optional<OpenAPI> sanitizeGeneratedFlattenNames(OpenAPI openAPI, List<String> existingComponentNames) {
Map<String, String> proposedNameMapping = getProposedNameMapping(openAPI, existingComponentNames);
if (proposedNameMapping.isEmpty()) {
return Optional.of(openAPI);
Expand All @@ -281,10 +90,10 @@ private Optional<OpenAPI> sanitizeOpenAPI(OpenAPI openAPI, List<String> existing
SwaggerParseResult parserResult = OASModifier.getOASWithSchemaNameModification(openAPI, proposedNameMapping);
openAPI = parserResult.getOpenAPI();
if (Objects.isNull(openAPI)) {
errorStream.println(ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES);
printError(ERROR_OCCURRED_WHILE_GENERATING_SCHEMA_NAMES);
if (!parserResult.getMessages().isEmpty()) {
errorStream.println(FOUND_PARSER_DIAGNOSTICS);
parserResult.getMessages().forEach(errorStream::println);
printError(FOUND_PARSER_DIAGNOSTICS);
parserResult.getMessages().forEach(this::printError);
}
return Optional.empty();
}
Expand Down Expand Up @@ -315,45 +124,4 @@ public Map<String, String> getProposedNameMapping(OpenAPI openapi, List<String>
}
return getResolvedNameMapping(nameMap);
}

private Filter getFilter() {
List<String> tagList = new ArrayList<>();
List<String> 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 void exitError() {
if (exitWhenFinish) {
Runtime.getRuntime().exit(1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
@CommandLine.Command(
name = "openapi",
description = "Generate the Ballerina sources for a given OpenAPI definition and vice versa.",
subcommands = {Add.class, Flatten.class}
subcommands = {Add.class, Flatten.class, Sanitize.class}
)
public class OpenApiCmd implements BLauncherCmd {
private static final String CMD_NAME = "openapi";
Expand Down
Loading
Loading