de.lancom.openapi.utils.MergeUtils.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openapi-parser Show documentation
Show all versions of openapi-parser Show documentation
This open-source project provides an OpenAPI 3.0 Parser implemented in Kotlin, utilizing immutable data classes
package de.lancom.openapi.utils
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.readValue
import de.lancom.openapi.entity.*
import de.lancom.openapi.field.Field
import de.lancom.openapi.refs.ReferenceOrInstance
import de.lancom.openapi.refs.Referenceable
import de.lancom.openapi.tools.toYamlString
import de.lancom.openapi.tools.updateOperations
import de.lancom.openapi.tools.yamlMapper
import kotlin.collections.flatten
private data class Rename(
val component: String,
val name: String,
val suffix: String,
) {
private val path = "#/components/$component/$name"
fun renameYaml(subject: String): String {
return subject.replace("'$path'", "'$path$suffix'")
}
fun renameComponent(openApi: OpenApi): OpenApi {
return openApi.updateComponents { components: Components? ->
components?.let(::renameComponent)
}
}
fun renameComponent(
map: Map?>?
): Map?>? {
if (map == null) {
return null
}
val value = map[name] ?: return map
return (map - name) + ("$name$suffix" to value)
}
fun renameComponent(components: Components): Components {
return when (component) {
"schemas" ->
components.updateSchemas(::renameComponent)
"responses" ->
components.updateResponses(::renameComponent)
"parameters" ->
components.updateParameters(::renameComponent)
"examples" ->
components.updateExamples(::renameComponent)
"requestBodies" ->
components.updateRequestBodies(::renameComponent)
"headers" ->
components.updateHeaders(::renameComponent)
"securitySchemes" ->
components.updateSecuritySchemes(::renameComponent)
"links" ->
components.updateLinks(::renameComponent)
"callbacks" ->
components.updateCallbacks(::renameComponent)
else ->
TODO()
}
}
}
private fun Components?.componentsToRename(
other: Components?,
suffix: String
): List {
if (this == null || other == null) {
return emptyList()
}
return mapOf(
"schemas" to Components::schemas,
"responses" to Components::responses,
"parameters" to Components::parameters,
"examples" to Components::examples,
"requestBodies" to Components::requestBodies,
"headers" to Components::headers,
"securitySchemes" to Components::securitySchemes,
"links" to Components::links,
"callbacks" to Components::callbacks,
).flatMap { (name, getter) ->
val ownMap = getter(this) ?: emptyMap()
val otherMap = getter(other) ?: emptyMap()
ownMap.keys.intersect(otherMap.keys).mapNotNull { commonKey: String ->
val ownValue: Any? = ownMap[commonKey]
val otherValue: Any? = otherMap[commonKey]
if (ownValue == otherValue) {
null
} else {
Rename(name, commonKey, suffix)
}
}
}
}
private inline fun T.editYaml(editor: (String) -> String): T {
val oldYamlString = toYamlString()
val newYamlString = editor(oldYamlString)
return yamlMapper.readValue(newYamlString)
}
fun Map.mergeOpenApi(
default: OpenApi = OpenApi(),
prefixTags: Boolean = true,
prefixPaths: Boolean = true,
createTagGroups: Boolean = true,
): OpenApi {
return toList().fold(default) { left, (name, right) ->
val prefixed = if (prefixPaths) {
right.prefixPaths("cloud-service-$name")
} else {
right
}
// rename schemas in prefixes
val renames: List = left.components.componentsToRename(prefixed.components, "_$name")
val renamed = renames.fold(prefixed) { openApi, rename ->
rename.renameComponent(openApi)
}.editYaml { oldYaml: String ->
renames.fold(oldYaml) { subject: String, rename: Rename ->
rename.renameYaml(subject)
}
}.let { renamed ->
if (prefixTags) {
renamed.prefixTags("$name:")
} else {
renamed
}
}.addRedocTagGroups(name, createTagGroups)
left.merge(renamed)
}.copy(_info = default._info) // keep default info
}
private fun OpenApi.addRedocTagGroups(service: String, createTagGroups: Boolean): OpenApi {
val usedTags =
paths?.operations?.mapNotNull(Operation::tags)?.flatten()?.filterIsInstance()?.toSet() ?: emptySet()
val definedTags = tags?.filterNotNull()?.map(Tag::name)?.filterNotNull()?.toSet() ?: emptySet()
val allTags = usedTags + definedTags
return if (createTagGroups && allTags.isNotEmpty()) {
val extName = "x-tagGroups"
val tagGroupsExtension = extensions.get(extName) as? TagGroupsExtension
?: TagGroupsExtension()
val updated = tagGroupsExtension
.addTags(
TagGroupsExtensionEntry()
.setName(service)
.addTags(allTags.toList())
)
addExtension(extName, updated)
} else {
this
}
}
private fun OpenApi.prefixTags(prefix: String): OpenApi {
return updateTags { tags ->
tags?.map { tag ->
if (tag == null) {
null
} else {
val displayName = tag.extensions.get("x-displayName")
val withExtension = if (displayName == null) {
val jsonNode = yamlMapper.valueToTree(tag.name)
tag.addExtension("x-displayName", RawExtension(Field(jsonNode)))
} else {
tag
}
withExtension.setName("$prefix${tag.name}")
}
}
}.updatePaths { paths: Paths? ->
paths?.updatePathItems { pathItems: Map ->
pathItems.mapValues { (_, pathItem: PathItem?) ->
pathItem?.updateOperations { operation: Operation? ->
operation?.updateTags { tags: List? ->
tags?.map { tag ->
"$prefix$tag"
}
}
}
}
}
}
}
private val Paths.operations: List
get() = pathItems.values.filterNotNull().flatMap(PathItem::operations)
private val PathItem.operations: List
get() = listOfNotNull(
get,
put,
post,
delete,
options,
head,
patch,
trace,
)
private fun OpenApi.prefixPaths(prefix: String): OpenApi {
return updatePaths { p: Paths? ->
p?.updatePathItems { pi: Map ->
pi.mapKeys { (path, _) ->
"/$prefix$path"
}
}
}
}