diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index 0ba0e454a..c5439fde4 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -509,6 +509,12 @@
com.fasterxml.jackson.core
jackson-databind
2.12.7.1
+
+
+
+ tools.jackson.dataformat
+ jackson-dataformat-yaml
+ 3.0.3
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
index 78b00a893..3845c33ea 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala
@@ -1,8 +1,16 @@
package code.api.ResourceDocs1_4_0
+import code.api.Constant.HostName
import code.api.OBPRestHelper
-import code.util.Helper.MdcLoggable
+import code.api.cache.Caching
+import code.api.util.APIUtil._
+import code.api.util.{APIUtil, ApiVersionUtils, YAMLUtils}
+import code.api.v1_4_0.JSONFactory1_4_0
+import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
+import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN}
+import com.openbankproject.commons.model.enums.ContentParam.{DYNAMIC, STATIC}
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
+import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S}
object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable {
@@ -152,6 +160,85 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md
route
})
})
+
+ // Register YAML endpoint using standard RestHelper approach
+ serve {
+ case Req("obp" :: versionStr :: "resource-docs" :: requestedApiVersionString :: "openapi.yaml" :: Nil, _, GetRequest) if versionStr == version.toString =>
+ val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
+
+ // Validate parameters
+ if (S.param("tags").exists(_.trim.isEmpty)) {
+ PlainTextResponse("Invalid tags parameter - empty values not allowed", 400)
+ } else if (S.param("functions").exists(_.trim.isEmpty)) {
+ PlainTextResponse("Invalid functions parameter - empty values not allowed", 400)
+ } else if (S.param("api-collection-id").exists(_.trim.isEmpty)) {
+ PlainTextResponse("Invalid api-collection-id parameter - empty values not allowed", 400)
+ } else if (S.param("content").isDefined && contentParam.isEmpty) {
+ PlainTextResponse("Invalid content parameter. Valid values: static, dynamic, all", 400)
+ } else {
+ try {
+ val requestedApiVersion = ApiVersionUtils.valueOf(requestedApiVersionString)
+ if (!versionIsAllowed(requestedApiVersion)) {
+ PlainTextResponse(s"API Version not supported: $requestedApiVersionString", 400)
+ } else if (locale.isDefined && APIUtil.obpLocaleValidation(locale.get) != SILENCE_IS_GOLDEN) {
+ PlainTextResponse(s"Invalid locale: ${locale.get}", 400)
+ } else {
+ val isVersion4OrHigher = true
+ val cacheKey = APIUtil.createResourceDocCacheKey(
+ Some("openapi31yaml"),
+ requestedApiVersionString,
+ resourceDocTags,
+ partialFunctions,
+ locale,
+ contentParam,
+ apiCollectionIdParam,
+ Some(isVersion4OrHigher)
+ )
+ val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey)
+
+ val yamlString = if (cacheValueFromRedis.isDefined) {
+ cacheValueFromRedis.get
+ } else {
+ // Generate OpenAPI JSON and convert to YAML
+ val openApiJValue = try {
+ val resourceDocsJsonFiltered = locale match {
+ case _ if (apiCollectionIdParam.isDefined) =>
+ val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId)
+ val resourceDocs = ResourceDoc.getResourceDocs(operationIds)
+ val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale)
+ resourceDocsJson.resource_docs
+ case _ =>
+ // Get all resource docs for the requested version
+ val allResourceDocs = ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(List.empty)
+ val filteredResourceDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(allResourceDocs, resourceDocTags, partialFunctions)
+ val resourceDocJson = JSONFactory1_4_0.createResourceDocsJson(filteredResourceDocs, isVersion4OrHigher, locale)
+ resourceDocJson.resource_docs
+ }
+
+ val hostname = HostName
+ val openApiDoc = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.createOpenAPI31Json(resourceDocsJsonFiltered, requestedApiVersionString, hostname)
+ code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.OpenAPI31JsonFormats.toJValue(openApiDoc)
+ } catch {
+ case e: Exception =>
+ logger.error(s"Error generating OpenAPI JSON: ${e.getMessage}", e)
+ throw e
+ }
+
+ val yamlResult = YAMLUtils.jValueToYAMLSafe(openApiJValue, s"# Error converting OpenAPI to YAML: ${openApiJValue.toString}")
+ Caching.setStaticSwaggerDocCache(cacheKey, yamlResult)
+ yamlResult
+ }
+
+ val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType)
+ val bytes = yamlString.getBytes("UTF-8")
+ InMemoryResponse(bytes, headers, Nil, 200)
+ }
+ } catch {
+ case _: Exception =>
+ PlainTextResponse(s"Invalid API version: $requestedApiVersionString", 400)
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
index 3d5aef287..9d5c894b3 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala
@@ -10,6 +10,7 @@ import code.api.util.ExampleValue.endpointMappingRequestBodyExample
import code.api.util.FutureUtil.EndpointContext
import code.api.util.NewStyle.HttpCode
import code.api.util._
+import code.api.util.YAMLUtils
import code.api.v1_4_0.JSONFactory1_4_0.ResourceDocsJson
import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0}
import code.api.v2_2_0.{APIMethods220, OBPAPI2_2_0}
@@ -32,7 +33,7 @@ import com.openbankproject.commons.model.{BankId, ListResult, User}
import com.openbankproject.commons.util.ApiStandards._
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.{Box, Empty, Full}
-import net.liftweb.http.LiftRules
+import net.liftweb.http.{InMemoryResponse, LiftRules, PlainTextResponse}
import net.liftweb.json
import net.liftweb.json.JsonAST.{JField, JString, JValue}
import net.liftweb.json._
@@ -769,6 +770,8 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
|This endpoint generates OpenAPI 3.1 compliant documentation with modern JSON Schema support.
|
+ |For YAML format, use the corresponding endpoint: /resource-docs/API_VERSION/openapi.yaml
+ |
|See the Resource Doc endpoint for more information.
|
|Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
@@ -811,6 +814,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
List(apiTagDocumentation, apiTagApi)
)
+ // Note: OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml)
+ // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly
+ // handle YAML content type. It provides the same functionality as the JSON endpoint
+ // but returns OpenAPI documentation in YAML format instead of JSON.
+
/**
* OpenAPI 3.1 endpoint with comprehensive parameter validation.
*
@@ -913,6 +921,25 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
}
}
+ // Note: The OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml)
+ // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly
+ // handle YAML content type and response format, rather than as a standard OBPEndpoint.
+
+
+
+
+ def convertResourceDocsToOpenAPI31YAMLAndSetCache(cacheKey: String, requestedApiVersionString: String, resourceDocsJson: List[JSONFactory1_4_0.ResourceDocJson]) : String = {
+ logger.debug(s"Generating OpenAPI 3.1 YAML-convertResourceDocsToOpenAPI31YAMLAndSetCache requestedApiVersion is $requestedApiVersionString")
+ val hostname = HostName
+ val openApiDoc = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.createOpenAPI31Json(resourceDocsJson, requestedApiVersionString, hostname)
+ val openApiJValue = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.OpenAPI31JsonFormats.toJValue(openApiDoc)
+
+ val yamlString = YAMLUtils.jValueToYAMLSafe(openApiJValue, "# Error converting to YAML")
+ Caching.setStaticSwaggerDocCache(cacheKey, yamlString)
+
+ yamlString
+ }
+
private def convertResourceDocsToOpenAPI31JvalueAndSetCache(cacheKey: String, requestedApiVersionString: String, resourceDocsJson: List[JSONFactory1_4_0.ResourceDocJson]) : JValue = {
logger.debug(s"Generating OpenAPI 3.1-convertResourceDocsToOpenAPI31JvalueAndSetCache requestedApiVersion is $requestedApiVersionString")
val hostname = HostName
diff --git a/obp-api/src/main/scala/code/api/util/YAMLUtils.scala b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala
new file mode 100644
index 000000000..16714ee50
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala
@@ -0,0 +1,107 @@
+/**
+Open Bank Project - API
+Copyright (C) 2011-2024, TESOBE GmbH.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+Email: contact@tesobe.com
+TESOBE GmbH.
+Osloer Strasse 16/17
+Berlin 13359, Germany
+
+This product includes software developed at
+TESOBE (http://www.tesobe.com/)
+
+*/
+package code.api.util
+
+import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import net.liftweb.json.JsonAST.JValue
+import net.liftweb.json._
+import net.liftweb.json.compactRender
+import code.util.Helper.MdcLoggable
+import scala.util.{Try, Success, Failure}
+
+/**
+ * Utility object for YAML conversion operations
+ *
+ * This utility provides methods to convert Lift's JValue objects to YAML format
+ * using Jackson's YAML support.
+ */
+object YAMLUtils extends MdcLoggable {
+
+ private val jsonMapper = new ObjectMapper()
+ private val yamlMapper = new ObjectMapper(new YAMLFactory())
+
+ /**
+ * Converts a JValue to YAML string
+ *
+ * @param jValue The Lift JValue to convert
+ * @return Try containing the YAML string or error
+ */
+ def jValueToYAML(jValue: JValue): Try[String] = {
+ Try {
+ // First convert JValue to JSON string
+ val jsonString = compactRender(jValue)
+
+ // Parse JSON string to Jackson JsonNode
+ val jsonNode: JsonNode = jsonMapper.readTree(jsonString)
+
+ // Convert JsonNode to YAML string
+ yamlMapper.writeValueAsString(jsonNode)
+ }.recoverWith {
+ case ex: Exception =>
+ logger.error(s"Failed to convert JValue to YAML: ${ex.getMessage}", ex)
+ Failure(new RuntimeException(s"YAML conversion failed: ${ex.getMessage}", ex))
+ }
+ }
+
+ /**
+ * Converts a JValue to YAML string with error handling that returns a default value
+ *
+ * @param jValue The Lift JValue to convert
+ * @param defaultValue Default value to return if conversion fails
+ * @return YAML string or default value
+ */
+ def jValueToYAMLSafe(jValue: JValue, defaultValue: String = ""): String = {
+ jValueToYAML(jValue) match {
+ case Success(yamlString) => yamlString
+ case Failure(ex) =>
+ logger.warn(s"YAML conversion failed, returning default value: ${ex.getMessage}")
+ defaultValue
+ }
+ }
+
+ /**
+ * Checks if the given content type indicates YAML format
+ *
+ * @param contentType The content type to check
+ * @return true if the content type indicates YAML
+ */
+ def isYAMLContentType(contentType: String): Boolean = {
+ val normalizedContentType = contentType.toLowerCase.trim
+ normalizedContentType.contains("application/x-yaml") ||
+ normalizedContentType.contains("application/yaml") ||
+ normalizedContentType.contains("text/yaml") ||
+ normalizedContentType.contains("text/x-yaml")
+ }
+
+ /**
+ * Gets the appropriate YAML content type
+ *
+ * @return Standard YAML content type
+ */
+ def getYAMLContentType: String = "application/x-yaml"
+}
\ No newline at end of file