commit 39379022ef45e38978c102b562f0249cd8b56503
parent 17012c45d710b4ec2b061262ba60735ac3e8fe99
Author: Antoine A <>
Date: Tue, 17 Mar 2026 10:21:24 +0100
common: improve OpenAPI schema generation with contributions by Maki Bytes
Diffstat:
4 files changed, 63 insertions(+), 104 deletions(-)
diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -30,6 +30,7 @@ import tech.libeufin.bank.cli.LibeufinBank
import tech.libeufin.bank.db.Database
import tech.libeufin.common.api.OpenApiInfo
import tech.libeufin.common.api.talerApi
+import tech.libeufin.common.VERSION
import com.github.ajalt.clikt.core.main
val logger: Logger = LoggerFactory.getLogger("libeufin-bank")
@@ -39,8 +40,21 @@ fun Application.corebankWebApp(db: Database, cfg: BankConfig, serveSpec: Boolean
LoggerFactory.getLogger("libeufin-bank-api"),
OpenApiInfo(
title = "LibEuFin Bank API",
- version = "1.4.0",
- description = "Taler corebank, wire gateway, wire transfer gateway, integration, conversion, revenue, and observability APIs for LibEuFin Bank"
+ version = VERSION,
+ description = "Taler corebank, wire gateway, wire transfer gateway, integration, conversion, revenue, and observability APIs for LibEuFin Bank",
+ securityConfig = {
+ securityScheme("bearerAuth") {
+ type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
+ scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BEARER
+ bearerFormat = "token"
+ description = "Bearer tokens minted by libeufin-bank /accounts/{USERNAME}/token"
+ }
+ securityScheme("basicAuth") {
+ type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
+ scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BASIC
+ description = "HTTP Basic authentication, supported only on compatibility-enabled endpoints"
+ }
+ }
),
serveSpec = serveSpec
) {
diff --git a/libeufin-common/build.gradle b/libeufin-common/build.gradle
@@ -71,6 +71,8 @@ dependencies {
// OpenAPI spec generation
implementation("io.github.smiley4:ktor-openapi:5.6.0")
implementation("io.github.smiley4:schema-kenerator-core:2.6.0")
+ implementation("io.github.smiley4:schema-kenerator-serialization:2.6.0")
+ implementation("io.github.smiley4:schema-kenerator-swagger:2.6.0")
implementation("com.github.ajalt.clikt:clikt:$clikt_version")
diff --git a/libeufin-common/src/main/kotlin/api/server.kt b/libeufin-common/src/main/kotlin/api/server.kt
@@ -21,8 +21,15 @@ package tech.libeufin.common.api
import io.github.smiley4.ktoropenapi.OpenApi
import io.github.smiley4.ktoropenapi.OpenApiPlugin
-import io.github.smiley4.ktoropenapi.config.OpenApiPluginConfig
-import io.github.smiley4.ktoropenapi.config.OutputFormat
+import io.github.smiley4.ktoropenapi.config.*
+import io.github.smiley4.ktoropenapi.openApi
+import io.github.smiley4.schemakenerator.serialization.SerializationSteps.analyzeTypeUsingKotlinxSerialization
+import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.compileReferencingRoot
+import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.generateSwaggerSchema
+import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.withTitle
+import io.github.smiley4.schemakenerator.swagger.SwaggerSteps.RequiredHandling
+import io.github.smiley4.schemakenerator.core.CoreSteps.addDiscriminatorProperty
+import io.github.smiley4.schemakenerator.swagger.data.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
@@ -154,7 +161,8 @@ fun talerPlugin(logger: Logger): ApplicationPlugin<Unit> {
data class OpenApiInfo(
val title: String,
val version: String,
- val description: String? = null
+ val description: String? = null,
+ val securityConfig: SecurityConfig.() -> Unit
)
/** Set up web server handlers for a Taler API */
@@ -170,20 +178,24 @@ fun Application.talerApi(logger: Logger, openApiInfo: OpenApiInfo? = null, serve
url = "/"
description = "Same host"
}
- security {
- securityScheme("bearerAuth") {
- type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
- scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BEARER
- bearerFormat = "token"
- description = "Bearer tokens minted by libeufin-bank /accounts/{USERNAME}/token"
- }
- securityScheme("basicAuth") {
- type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
- scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BASIC
- description = "HTTP Basic authentication, supported only on compatibility-enabled endpoints"
+ security(openApiInfo.securityConfig)
+ outputFormat = OutputFormat.YAML
+ schemas {
+ generator = { type ->
+ type
+ .analyzeTypeUsingKotlinxSerialization()
+ .addDiscriminatorProperty("type")
+ .generateSwaggerSchema {
+ nullables = RequiredHandling.NON_REQUIRED
+ optionals = RequiredHandling.REQUIRED
+ }
+ .withTitle(TitleType.SIMPLE)
+ .compileReferencingRoot(
+ explicitNullTypes = false,
+ pathType = RefType.OPENAPI_SIMPLE
+ )
}
}
- outputFormat = OutputFormat.YAML
}
}
install(CallId) {
@@ -322,93 +334,11 @@ fun Application.talerApi(logger: Logger, openApiInfo: OpenApiInfo? = null, serve
routing {
routes()
if (serveSpec) {
- get("openapi.yaml") {
- call.respondText(
- getOpenApiSpec(),
- ContentType.parse("application/yaml")
- )
- }
- }
- }
-}
-
-/** Get the generated OpenAPI spec as a string (available after application start) */
-fun getOpenApiSpec(): String =
- normalizeOpenApiSpec(OpenApiPlugin.getOpenApiSpec(OpenApiPluginConfig.DEFAULT_SPEC_ID))
-
-private fun normalizeOpenApiSpec(spec: String): String {
- val lines = spec.lines()
- val schemaNames = mutableListOf<String>()
- var inSchemas = false
-
- for (line in lines) {
- when {
- !inSchemas && line == " schemas:" -> inSchemas = true
- inSchemas && line.startsWith(" ") && !line.startsWith(" ") -> break
- inSchemas && line.startsWith(" ") && !line.startsWith(" ") && line.endsWith(":") -> {
- schemaNames += line.removePrefix(" ").removeSuffix(":")
+ route("openapi.yaml") {
+ openApi()
}
}
}
- if (schemaNames.isEmpty()) return spec
-
- val renamed = LinkedHashMap<String, String>()
- val used = mutableSetOf<String>()
-
- fun sanitizeSchemaName(name: String): String {
- val cleaned = name
- .replace("tech.libeufin.bank.", "")
- .replace("tech.libeufin.common.", "")
- .replace("tech.libeufin.nexus.", "")
- .replace("tech.libeufin.ebics.", "")
- .replace("kotlin.collections.", "")
- .replace("kotlin.", "")
- .replace("java.net.", "")
- .replace(Regex("[^A-Za-z0-9]+"), "_")
- .trim('_')
- return cleaned.ifEmpty { "Schema" }
- }
-
- for (original in schemaNames) {
- val base = sanitizeSchemaName(original)
- var candidate = base
- var i = 2
- while (!used.add(candidate)) {
- candidate = "${base}_$i"
- i += 1
- }
- renamed[original] = candidate
- }
-
- val normalizedLines = buildList {
- var insideSchemas = false
- for (line in lines) {
- when {
- !insideSchemas && line == " schemas:" -> {
- insideSchemas = true
- add(line)
- }
- insideSchemas && line.startsWith(" ") && !line.startsWith(" ") -> {
- insideSchemas = false
- add(line)
- }
- insideSchemas && line.startsWith(" ") && !line.startsWith(" ") && line.endsWith(":") -> {
- val original = line.removePrefix(" ").removeSuffix(":")
- add(" ${renamed.getValue(original)}:")
- }
- else -> add(line)
- }
- }
- }
-
- var normalized = normalizedLines.joinToString("\n")
- for ((original, shortName) in renamed.entries.sortedByDescending { it.key.length }) {
- normalized = normalized.replace(
- "#/components/schemas/$original",
- "#/components/schemas/$shortName"
- )
- }
- return normalized
}
// Dirty local variable to stop the server in test TODO remove this ugly hack
diff --git a/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023-2025 Taler Systems S.A.
+ * Copyright (C) 2023-2025, 2026 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -32,6 +32,7 @@ import kotlinx.serialization.Contextual
import tech.libeufin.common.api.OpenApiInfo
import tech.libeufin.common.api.talerApi
import tech.libeufin.common.setupSecurityProperties
+import tech.libeufin.common.VERSION
import tech.libeufin.nexus.api.revenueApi
import tech.libeufin.nexus.api.preparedTransferAPI
import tech.libeufin.nexus.api.wireGatewayApi
@@ -52,8 +53,20 @@ fun Application.nexusApi(db: Database, cfg: NexusConfig, serveSpec: Boolean = fa
LoggerFactory.getLogger("libeufin-nexus-api"),
OpenApiInfo(
title = "LibEuFin Nexus API",
- version = "1.4.0",
- description = "Taler wire gateway, wire transfer gateway, revenue, and observability APIs for LibEuFin Nexus"
+ version = VERSION,
+ description = "Taler wire gateway, wire transfer gateway, revenue, and observability APIs for LibEuFin Nexus",
+ securityConfig = {
+ securityScheme("bearerAuth") {
+ type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
+ scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BEARER
+ bearerFormat = "token"
+ }
+ securityScheme("basicAuth") {
+ type = io.github.smiley4.ktoropenapi.config.AuthType.HTTP
+ scheme = io.github.smiley4.ktoropenapi.config.AuthScheme.BASIC
+ description = "HTTP Basic authentication, supported only on compatibility-enabled endpoints"
+ }
+ }
),
serveSpec = serveSpec
) {