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