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 5 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,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_SANITIZE_CMD = "sanitize";

public enum Mode {
SERVICE,
Expand Down
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}
subcommands = {Add.class, Sanitize.class}
)
public class OpenApiCmd implements BLauncherCmd {
private static final String CMD_NAME = "openapi";
Expand Down
314 changes: 314 additions & 0 deletions openapi-cli/src/main/java/io/ballerina/openapi/cmd/Sanitize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com).
*
* 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
*
* 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;
import io.ballerina.openapi.core.generators.common.OASModifier;
import io.ballerina.openapi.core.generators.common.exception.BallerinaOpenApiException;
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 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.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_SANITIZE_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 "sanitize" subcommand which is used to flatten and sanitize the OpenAPI definition
* by generating Ballerina friendly type schema names and Ballerina type name extensions.
*
* @since 2.2.0
*/
@CommandLine.Command(
name = "sanitize",
description = ""
)
public class Sanitize implements BLauncherCmd {
private static final String COMMAND_IDENTIFIER = "openapi-sanitize";
private static final String COMMA = ",";

private static final String INFO_OUTPUT_WRITTEN_MSG = "INFO: sanitized 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 " +
"sanitize 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 sanitized 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 sanitized OpenAPI definition.")
private String outputPath;

@CommandLine.Option(names = {"-n", "--name"}, description = "Name of the sanitized OpenAPI definition file.")
private String fileName;

@CommandLine.Option(names = {"-f", "--format"}, description = "Output format of the sanitized OpenAPI definition.")
private String format;

@CommandLine.Option(names = {"-t", "--tags"}, description = "Tags that need to be considered when sanitizing.")
public String tags;

@CommandLine.Option(names = {"--operations"}, description = "Operations that need to be included when sanitizing.")
public String operations;

public Sanitize() {
}

public Sanitize(PrintStream errorStream, boolean exitWhenFinish) {
this.errorStream = errorStream;
this.exitWhenFinish = 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();
generateSanitizedOpenAPI();
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(fileName)) {
fileName = "sanitized_openapi";
TharmiganK marked this conversation as resolved.
Show resolved Hide resolved
}

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 generateSanitizedOpenAPI() {
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 = getSanitizedOpenAPI(openAPIFileContent);
if (openAPIOptional.isEmpty()) {
exitError();
return;
}
writeSanitizedOpenAPIFile(openAPIOptional.get());
}

private Optional<OpenAPI> getSanitizedOpenAPI(String openAPIFileContent) {
// Read the contents of the file with default parser options
// Sanitizing 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);
}
return Optional.empty();
}

filterOpenAPIOperations(openAPI);

// Flatten the OpenAPI definition with `flattenComposedSchemas: true` and `camelCaseFlattenNaming: true`
InlineModelResolver inlineModelResolver = new InlineModelResolver(true, true);
inlineModelResolver.flatten(openAPI);

return sanitizeOpenAPI(openAPI);
}

private void writeSanitizedOpenAPIFile(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, fileName + getFileExtension(), format.equals("json"));
}

private String getFileExtension() {
return (Objects.nonNull(format) && format.equals("json")) ? JSON_EXTENSION : YAML_EXTENSION;
}

private void filterOpenAPIOperations(OpenAPI openAPI) {
TharmiganK marked this conversation as resolved.
Show resolved Hide resolved
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) {
OASModifier oasSanitizer = new OASModifier();
try {
return Optional.of(oasSanitizer.modifyWithBallerinaConventions(openAPI));
} catch (BallerinaOpenApiException exp) {
errorStream.printf("ERROR: %s%n", exp.getMessage());
return Optional.empty();
}
}

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_SANITIZE_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
@@ -0,0 +1,58 @@
NAME
bal openapi sanitize - Sanitize the OpenAPI contract file according to
the best practices of Ballerina.

SYNOPSIS
bal openapi sanitize [-i | --input] <openapi-contract-file-path>
[-o | --output] <output-file-path>
[-n | --name] <generated-file-name>
[-f | --format] [json|yaml]
[-t | --tags] <tag-names>
[--operations] <operation-names>

DESCRIPTION
Sanitize the OpenAPI contract file according to the best naming
practices of Ballerina. The OpenAPI contract is flatten and the type
schema names are made Ballerina friendly. The Ballerina name
extensions are added to the schemas which can not be modified
directly.


OPTIONS
-i, --input <openapi-contract-file-path>
This is a mandatory input. The given OpenAPI contract will be sanitized.
The OpenAPI contract can be either a YAML or a JSON.

-o, --output <output-file-path>
This is an optional input. The given output file path will be used to
save the sanitized OpenAPI contract. The default output file path is
the executed directory.

-n, --name <generated-file-name>
This is an optional input. The given name will be used to save the
sanitized OpenAPI contract. The default name is `sanitized_openapi`.

-f, --format [json|yaml]
This is an optional input. The sanitized OpenAPI contract will be
saved in the given format. The format can be either JSON or YAML.
The default format is same as the input file format.

-t, --tags <tag-names>
This is an optional input. The sanitized OpenAPI contract will only
have the operations with the given tags.

--operations <operation-names>
This is an optional input. The sanitized OpenAPI contract will only
have the given operations.

EXAMPLES
Sanitize the `service.yaml` OpenAPI contract file.
$ bal openapi sanitize -i service.yaml

Sanitize the `service.yaml` OpenAPI contract file and save it as
`sanitized_svc.json` file.
$ bal openapi sanitize -i hello.yaml -n sanitized_svc -f json

Sanitize the `service.json` OpenAPI contract file by filtering the
operations with the `service` tag.
$ bal openapi sanitize -i service.json -t service
Loading
Loading