From 467eb6c3b47a4d3abe6bb0cabe61bbea91c11959 Mon Sep 17 00:00:00 2001 From: Conor Gallagher Date: Mon, 29 Jan 2024 20:34:00 +0000 Subject: [PATCH] Supported OpenApi 3.1.0 nullable changes. (#263) * Supported OpenApi 3.1.0 nullable changes. Fabrikt will now detect of the newer 3.1.0 specification is declared and automatically downgrade any of the json schema nullable types to the previous format. EG, this: ``` NewNullableFormat: type: object required: - version properties: version: type: - string - 'null' ``` Will get converted to this: ``` NewNullableFormat: type: object required: - version properties: version: type: string nullable: true ``` * improve code --- .../com/cjbooms/fabrikt/util/YamlUtils.kt | 59 +++++++++++++++---- .../fabrikt/generators/ModelGeneratorTest.kt | 1 + .../resources/examples/openapi310/api.yaml | 14 +++++ .../examples/openapi310/models/Models.kt | 12 ++++ 4 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/examples/openapi310/api.yaml create mode 100644 src/test/resources/examples/openapi310/models/Models.kt diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/YamlUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/YamlUtils.kt index 115a5ed9..f57fcdad 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/util/YamlUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/util/YamlUtils.kt @@ -19,33 +19,62 @@ object YamlUtils { ObjectMapper( YAMLFactory() .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) - .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES), ) .registerKotlinModule() .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) private val internalMapper: ObjectMapper = ObjectMapper(YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)) + private val NULL_TYPE: JsonNode = objectMapper.valueToTree("null") + fun mergeYamlTrees(mainTree: String, updateTree: String) = internalMapper.writeValueAsString( mergeNodes( internalMapper.readTree(mainTree), - internalMapper.readTree(updateTree) - ) + internalMapper.readTree(updateTree), + ), )!! fun parseOpenApi(input: String, inputDir: Path = Paths.get("").toAbsolutePath()): OpenApi3 = try { - OpenApi3Parser().parse(input, inputDir.toUri().toURL()) + val root: JsonNode = objectMapper.readTree(input) + if (root["openapi"].asText() == "3.1.0") { + downgradeNullableSyntax(root) + } + OpenApi3Parser().parse(root, inputDir.toUri().toURL()) } catch (ex: NullPointerException) { throw IllegalArgumentException( "The Kaizen openapi-parser library threw a NPE exception when parsing this API. " + "This is commonly due to an external schema reference that is unresolvable, " + "possibly due to a lack of internet connection", - ex + ex, ) } + private fun downgradeNullableSyntax(node: JsonNode) { + when { + node.isObject -> { + var requiresNullable = false + node.fields().forEach { (key, maybeTypeArray) -> + if (key == "type" && maybeTypeArray.isArray && maybeTypeArray.contains(NULL_TYPE)) { + val nonNullType = maybeTypeArray.first { it != NULL_TYPE } + (node as ObjectNode).replace("type", nonNullType) + requiresNullable = true + } + downgradeNullableSyntax(maybeTypeArray) + } + if (requiresNullable) { + (node as ObjectNode).put("nullable", true) + } + } + + node.isArray -> { + node.forEach { downgradeNullableSyntax(it) } + } + } + } + /** * The below merge function has been shamelessly stolen from Stackoverflow: https://stackoverflow.com/a/32447591/1026785 * and converted to much nicer Kotlin implementation @@ -56,14 +85,20 @@ object YamlUtils { val incomingNode = incomingTree.get(fieldName) if (currentNode is ArrayNode && incomingNode is ArrayNode) { incomingNode.forEach { - if (currentNode.contains(it)) mergeNodes( - currentNode.get(currentNode.indexOf(it)), - it - ) - else currentNode.add(it) + if (currentNode.contains(it)) { + mergeNodes( + currentNode.get(currentNode.indexOf(it)), + it, + ) + } else { + currentNode.add(it) + } } - } else if (currentNode is ObjectNode) mergeNodes(currentNode, incomingNode) - else (currentTree as? ObjectNode)?.replace(fieldName, incomingNode) + } else if (currentNode is ObjectNode) { + mergeNodes(currentNode, incomingNode) + } else { + (currentTree as? ObjectNode)?.replace(fieldName, incomingNode) + } } return currentTree } diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt index f20e3d96..2b26c8c3 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt @@ -54,6 +54,7 @@ class ModelGeneratorTest { "webhook", "instantDateTime", "discriminatedOneOf", + "openapi310", ) @BeforeEach diff --git a/src/test/resources/examples/openapi310/api.yaml b/src/test/resources/examples/openapi310/api.yaml new file mode 100644 index 00000000..1cd16b58 --- /dev/null +++ b/src/test/resources/examples/openapi310/api.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 +components: + schemas: + NewNullableFormat: + type: object + required: + - version + properties: + version: + type: + - string + - 'null' + description: The resolved version or `null` if there is no matching version. + diff --git a/src/test/resources/examples/openapi310/models/Models.kt b/src/test/resources/examples/openapi310/models/Models.kt new file mode 100644 index 00000000..0de7d82d --- /dev/null +++ b/src/test/resources/examples/openapi310/models/Models.kt @@ -0,0 +1,12 @@ +package examples.openapi310.models + +import com.fasterxml.jackson.`annotation`.JsonProperty +import javax.validation.constraints.NotNull +import kotlin.String + +public data class NewNullableFormat( + @param:JsonProperty("version") + @get:JsonProperty("version") + @get:NotNull + public val version: String, +)