libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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:
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 18++++++++++++++++--
Mlibeufin-common/build.gradle | 2++
Mlibeufin-common/src/main/kotlin/api/server.kt | 128++++++++++++++++++-------------------------------------------------------------
Mlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 19++++++++++++++++---
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 ) {