Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider
import code.util.Helper
import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
import net.liftweb.http.S
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.enums.ContentParam
import com.openbankproject.commons.model.enums.ContentParam.{ALL, DYNAMIC, STATIC}
Expand Down Expand Up @@ -735,39 +736,124 @@
|
|API_VERSION is the version you want documentation about e.g. v6.0.0
|
|You may filter this endpoint using the 'tags' url parameter e.g. ?tags=Account,Bank
|## Query Parameters
|
|(All endpoints are given one or more tags which for used in grouping)
|You may filter this endpoint using the following optional query parameters:
|
|You may filter this endpoint using the 'functions' url parameter e.g. ?functions=getBanks,bankById
|**tags** - Filter by endpoint tags (comma-separated list)
| • Example: ?tags=Account,Bank or ?tags=Account-Firehose
| • All endpoints are given one or more tags which are used for grouping
| • Empty values will return error OBP-10053
|
|(Each endpoint is implemented in the OBP Scala code by a 'function')
|**functions** - Filter by function names (comma-separated list)
| • Example: ?functions=getBanks,bankById
| • Each endpoint is implemented in the OBP Scala code by a 'function'
| • Empty values will return error OBP-10054
|
|**content** - Filter by endpoint type
| • Values: static, dynamic, all (case-insensitive)
| • static: Only show static/core API endpoints
| • dynamic: Only show dynamic/custom endpoints
| • all: Show both static and dynamic endpoints (default)
| • Invalid values will return error OBP-10052
|
|**locale** - Language for localized documentation
| • Example: ?locale=en_GB or ?locale=es_ES
| • Supported locales: en_GB, es_ES, ro_RO
| • Invalid locales will return error OBP-10041
|
|**api-collection-id** - Filter by API collection UUID
| • Example: ?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
| • Returns only endpoints belonging to the specified collection
| • Empty values will return error OBP-10055
|
|This endpoint generates OpenAPI 3.1 compliant documentation with modern JSON Schema support.
|
|See the Resource Doc endpoint for more information.
|
| Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
|Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds
|
|Following are more examples:
|## Examples
|
|Basic usage:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi
|
|Filter by tags:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account-Firehose
|
|Filter by content type:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=dynamic
|
|Filter by functions:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?functions=getBanks,bankById
|
|Combine multiple parameters:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&tags=Account-Firehose
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?tags=Account,Bank,PSD2&functions=getBanks,bankById
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?content=static&locale=en_GB&tags=Account
|
|Filter by API collection:
|${getObpApiRoot}/v6.0.0/resource-docs/v6.0.0/openapi?api-collection-id=4e866c86-60c3-4268-a221-cb0bbf1ad221
|
""",
EmptyBody,
EmptyBody,
InvalidApiVersionString ::
ApiVersionNotSupported ::
InvalidLocale ::
InvalidContentParameter ::
InvalidTagsParameter ::
InvalidFunctionsParameter ::
InvalidApiCollectionIdParameter ::
UnknownError :: Nil,
List(apiTagDocumentation, apiTagApi)
)

/**
* OpenAPI 3.1 endpoint with comprehensive parameter validation.
*
* This endpoint generates OpenAPI 3.1 documentation with the following validated query parameters:
* - tags: Comma-separated list of tags to filter endpoints (e.g., ?tags=Account,Bank)
* - functions: Comma-separated list of function names to filter endpoints
* - content: Filter type - "static", "dynamic", or "all"
* - locale: Language code for localization (e.g., "en_GB", "es_ES")
* - api-collection-id: UUID to filter by specific API collection
*
* Parameter validation guards ensure:
* - Empty parameters (e.g., ?tags=) return 400 error
* - Invalid content values return 400 error with valid options
* - All parameters are properly trimmed and sanitized
*
* Examples:
* - ?content=static&tags=Account-Firehose
* - ?tags=Account,Bank&functions=getBanks,bankById
* - ?content=dynamic&locale=en_GB
*/
def getResourceDocsOpenAPI31 : OBPEndpoint = {

Check failure on line 834 in obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZsHsRrRLhYFhiZlYNKV&open=AZsHsRrRLhYFhiZlYNKV&pullRequest=2643
case "resource-docs" :: requestedApiVersionString :: "openapi" :: Nil JsonGet _ => {
cc => {
implicit val ec = EndpointContext(Some(cc))
val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()

// Early validation for empty parameters using underlying S to bypass ObpS filtering
if (S.param("tags").exists(_.trim.isEmpty)) {
Full(errorJsonResponse(InvalidTagsParameter, 400))
} else if (S.param("functions").exists(_.trim.isEmpty)) {
Full(errorJsonResponse(InvalidFunctionsParameter, 400))
} else if (S.param("api-collection-id").exists(_.trim.isEmpty)) {
Full(errorJsonResponse(InvalidApiCollectionIdParameter, 400))
} else {
val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams()
for {
// Validate content parameter if provided
_ <- if (S.param("content").isDefined && contentParam.isEmpty) {
Helper.booleanToFuture(failMsg = InvalidContentParameter, cc = cc.callContext) {
false
}
} else {
Future.successful(true)
}
requestedApiVersion <- NewStyle.function.tryons(s"$InvalidApiVersionString Current Version is $requestedApiVersionString", 400, cc.callContext) {
ApiVersionUtils.valueOf(requestedApiVersionString)
}
Expand Down Expand Up @@ -819,8 +905,9 @@
convertResourceDocsToOpenAPI31JvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered)
}
}
} yield {
(openApiJValue, HttpCode.`200`(cc.callContext))
} yield {
(openApiJValue, HttpCode.`200`(cc.callContext))
}
}
}
}
Expand Down Expand Up @@ -980,7 +1067,7 @@
case _ => Empty
}

def stringToContentParam (x: String) : Option[ContentParam] = x.toLowerCase match {
def stringToContentParam (x: String) : Option[ContentParam] = x.toLowerCase.trim match {
case "dynamic" => Some(DYNAMIC)
case "static" => Some(STATIC)
case "all" => Some(ALL)
Expand All @@ -1000,14 +1087,18 @@
case Empty => None
case _ => {
val commaSeparatedList : String = rawTagsParam.getOrElse("")
val tagList : List[String] = commaSeparatedList.trim().split(",").toList
val resourceDocTags =
for {
y <- tagList
} yield {
ResourceDocTag(y)
}
Some(resourceDocTags)
val tagList : List[String] = commaSeparatedList.trim().split(",").toList.filter(_.nonEmpty)
if (tagList.nonEmpty) {
val resourceDocTags =
for {
y <- tagList
} yield {
ResourceDocTag(y.trim())
}
Some(resourceDocTags)
} else {
None
}
}
}
logger.debug(s"tagsOption is $tags")
Expand All @@ -1023,14 +1114,18 @@
case Empty => None
case _ => {
val commaSeparatedList : String = rawPartialFunctionNames.getOrElse("")
val stringList : List[String] = commaSeparatedList.trim().split(",").toList
val pfns =
for {
y <- stringList
} yield {
y
}
Some(pfns)
val stringList : List[String] = commaSeparatedList.trim().split(",").toList.filter(_.nonEmpty)
if (stringList.nonEmpty) {
val pfns =
for {
y <- stringList
} yield {
y.trim()
}
Some(pfns)
} else {
None
}
}
}
logger.debug(s"partialFunctionNames is $partialFunctionNames")
Expand All @@ -1047,7 +1142,8 @@

val apiCollectionIdParam = for {
x <- ObpS.param("api-collection-id")
} yield x
if x.trim.nonEmpty
} yield x.trim
logger.debug(s"apiCollectionIdParam is $apiCollectionIdParam")


Expand Down
5 changes: 5 additions & 0 deletions obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ object ErrorMessages {

val createFxCurrencyIssue = "OBP-10050: Cannot create FX currency. "
val invalidLogLevel = "OBP-10051: Invalid log level. "
val InvalidContentParameter = "OBP-10052: Invalid content parameter. Valid values are: static, dynamic, all"
val InvalidTagsParameter = "OBP-10053: Invalid tags parameter. Tags cannot be empty when provided"
val InvalidFunctionsParameter = "OBP-10054: Invalid functions parameter. Functions cannot be empty when provided"
val InvalidApiCollectionIdParameter = "OBP-10055: Invalid api-collection-id parameter. API collection ID cannot be empty when provided"




Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import code.api.ResourceDocs1_4_0.ResourceDocs140.ImplementationsResourceDocs
import code.api.berlin.group.ConstantsBG
import code.api.util.APIUtil.OAuth._
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
import code.api.util.ErrorMessages.{InvalidApiCollectionIdParameter, UserHasMissingRoles, UserNotLoggedIn}
import code.api.util.{ApiRole, CustomJsonFormats}
import code.api.v1_4_0.JSONFactory1_4_0.ResourceDocsJson
import code.setup.{DefaultUsers, PropsReset}
Expand Down Expand Up @@ -74,7 +74,7 @@

feature(s"test ${ApiEndpoint1.name} ") {
scenario(s"We will test ${ApiEndpoint1.name} Api -v6.0.0", ApiEndpoint1, VersionOfApi) {
val requestGetObp = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "obp").GET

Check failure on line 77 in obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "v6.0.0" 9 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZsHsRvPLhYFhiZlYNKW&open=AZsHsRvPLhYFhiZlYNKW&pullRequest=2643
val responseGetObp = makeGetRequest(requestGetObp)
And("We should get 200 and the response can be extract to case classes")
val responseDocs = responseGetObp.body.extract[ResourceDocsJson]
Expand All @@ -100,6 +100,52 @@
//This should not throw any exceptions
responseDocs.resource_docs.map(responseDoc => stringToNodeSeq(responseDoc.description))
}

scenario("Test OpenAPI endpoint with valid parameters", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("content", "static"), ("tags", "Account"))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(200)
}

scenario("Test OpenAPI endpoint with invalid content parameter", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("content", "invalid"))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(400)
responseGetOpenAPI.body.toString should include("OBP-10052")
}

scenario("Test OpenAPI endpoint with empty tags parameter", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("tags", ""))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(400)
responseGetOpenAPI.body.toString should include("OBP-10053")
}

scenario("Test OpenAPI endpoint with empty functions parameter", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("functions", ""))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(400)
responseGetOpenAPI.body.toString should include("OBP-10054")
}

scenario("Test OpenAPI endpoint with valid multiple tags", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("tags", "Account,Bank"), ("content", "static"))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(200)
}

scenario("Test OpenAPI endpoint with Account-Firehose tag and static content", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("content", "static"), ("tags", "Account-Firehose"))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(200)
}

scenario("Test OpenAPI endpoint with empty api-collection-id parameter", ApiEndpoint1, VersionOfApi) {
val requestGetOpenAPI = (ResourceDocsV6_0Request / "resource-docs" / "v6.0.0" / "openapi").GET <<? List(("api-collection-id", ""))
val responseGetOpenAPI = makeGetRequest(requestGetOpenAPI)
responseGetOpenAPI.code should equal(400)
responseGetOpenAPI.body.toString should include(InvalidApiCollectionIdParameter)
}
scenario(s"We will test ${ApiEndpoint1.name} Api -v5.1.0", ApiEndpoint1, VersionOfApi) {
val requestGetObp = (ResourceDocsV5_0Request / "resource-docs" / "v5.1.0" / "obp").GET
val responseGetObp = makeGetRequest(requestGetObp)
Expand Down