Merge pull request #2653 from hongwei1/refactor/AddedResourceDocs

Refactor/added resource docs
This commit is contained in:
Simon Redfern 2026-01-06 12:22:21 +01:00 committed by GitHub
commit 876985325c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 145 additions and 51 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@
*.code-workspace
.zed
.cursor
.trae
.classpath
.project
.cache

View File

@ -76,7 +76,7 @@ MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" mvn -pl obp-http4s-runner -am
java -jar obp-http4s-runner/target/obp-http4s-runner.jar
```
The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8181`).
The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8086`).
### ZED IDE Setup

View File

@ -1691,6 +1691,6 @@ securelogging_mask_email=true
############################################
# Host and port for http4s server (used by bootstrap.http4s.Http4sServer)
# Defaults (if not set) are 127.0.0.1 and 8181
# Defaults (if not set) are 127.0.0.1 and 8086
http4s.host=127.0.0.1
http4s.port=8086

View File

@ -11,17 +11,15 @@ import org.http4s.implicits._
import scala.language.higherKinds
object Http4sServer extends IOApp {
val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] =
code.api.v7_0_0.Http4s700.wrappedRoutesV700Services
val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound
//Start OBP relevant objects, and settings
//Start OBP relevant objects and settings; this step MUST be executed first
new bootstrap.http4s.Http4sBoot().boot
val port = APIUtil.getPropsAsIntValue("http4s.port",8181)
val port = APIUtil.getPropsAsIntValue("http4s.port",8086)
val host = APIUtil.getPropsValue("http4s.host","127.0.0.1")
val services: HttpRoutes[IO] = code.api.v7_0_0.Http4s700.wrappedRoutesV700Services
val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound
override def run(args: List[String]): IO[ExitCode] = EmberServerBuilder
.default[IO]

View File

@ -1,8 +1,10 @@
package code.api.ResourceDocs1_4_0
import code.api.Constant.{GET_DYNAMIC_RESOURCE_DOCS_TTL, GET_STATIC_RESOURCE_DOCS_TTL, PARAM_LOCALE, HostName}
import code.api.Constant.{GET_DYNAMIC_RESOURCE_DOCS_TTL, GET_STATIC_RESOURCE_DOCS_TTL, HostName, PARAM_LOCALE}
import code.api.OBPRestHelper
import code.api.cache.Caching
import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint
import code.api.dynamic.entity.OBPAPIDynamicEntity
import code.api.util.APIUtil._
import code.api.util.ApiRole.{canReadDynamicResourceDocsAtOneBank, canReadResourceDoc}
import code.api.util.ApiTag._
@ -20,12 +22,9 @@ import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0}
import code.api.v5_0_0.OBPAPI5_0_0
import code.api.v5_1_0.OBPAPI5_1_0
import code.api.v6_0_0.OBPAPI6_0_0
import code.api.dynamic.endpoint.OBPAPIDynamicEndpoint
import code.api.dynamic.entity.OBPAPIDynamicEntity
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}
@ -33,6 +32,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, S}
import net.liftweb.http.{InMemoryResponse, LiftRules, PlainTextResponse}
import net.liftweb.json
import net.liftweb.json.JsonAST.{JField, JString, JValue}
@ -118,6 +118,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
logger.debug(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion")
val resourceDocs = requestedApiVersion match {
case ApiVersion.v7_0_0 => code.api.v7_0_0.Http4s700.resourceDocs
case ApiVersion.v6_0_0 => OBPAPI6_0_0.allResourceDocs
case ApiVersion.v5_1_0 => OBPAPI5_1_0.allResourceDocs
case ApiVersion.v5_0_0 => OBPAPI5_0_0.allResourceDocs
@ -139,6 +140,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
logger.debug(s"There are ${resourceDocs.length} resource docs available to $requestedApiVersion")
val versionRoutes = requestedApiVersion match {
case ApiVersion.v7_0_0 => Nil
case ApiVersion.v6_0_0 => OBPAPI6_0_0.routes
case ApiVersion.v5_1_0 => OBPAPI5_1_0.routes
case ApiVersion.v5_0_0 => OBPAPI5_0_0.routes
@ -165,7 +167,10 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
val versionRoutesClasses = versionRoutes.map { vr => vr.getClass }
// Only return the resource docs that have available routes
val activeResourceDocs = resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
val activeResourceDocs = requestedApiVersion match {
case ApiVersion.v7_0_0 => resourceDocs
case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
}
logger.debug(s"There are ${activeResourceDocs.length} resource docs available to $requestedApiVersion")
@ -1251,4 +1256,3 @@ so the caller must specify any required filtering by catalog explicitly.
}

View File

@ -27,6 +27,7 @@ TESOBE (http://www.tesobe.com/)
package code.api.util
import bootstrap.liftweb.CustomDBVendor
import cats.effect.IO
import code.accountholders.AccountHolders
import code.api.Constant._
import code.api.OAuthHandshake._
@ -96,6 +97,7 @@ import net.liftweb.util.Helpers._
import net.liftweb.util._
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.http4s.HttpRoutes
import java.io.InputStream
import java.net.URLDecoder
@ -1636,7 +1638,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
isFeatured: Boolean = false,
specialInstructions: Option[String] = None,
var specifiedUrl: Option[String] = None, // A derived value: Contains the called version (added at run time). See the resource doc for resource doc!
createdByBankId: Option[String] = None //we need to filter the resource Doc by BankId
createdByBankId: Option[String] = None, //we need to filter the resource Doc by BankId
http4sPartialFunction: Http4sEndpoint = None // http4s endpoint handler
) {
// this code block will be merged to constructor.
{
@ -2789,6 +2792,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
type OBPEndpoint = PartialFunction[Req, CallContext => Box[JsonResponse]]
type OBPReturnType[T] = Future[(T, Option[CallContext])]
type Http4sEndpoint = Option[HttpRoutes[IO]]
def getAllowedEndpoints (endpoints : Iterable[OBPEndpoint], resourceDocs: ArrayBuffer[ResourceDoc]) : List[OBPEndpoint] = {

View File

@ -19,6 +19,7 @@ object ApiVersionUtils {
v5_0_0 ::
v5_1_0 ::
v6_0_0 ::
v7_0_0 ::
`dynamic-endpoint` ::
`dynamic-entity` ::
scannedApis
@ -41,6 +42,7 @@ object ApiVersionUtils {
case v5_0_0.fullyQualifiedVersion | v5_0_0.apiShortVersion => v5_0_0
case v5_1_0.fullyQualifiedVersion | v5_1_0.apiShortVersion => v5_1_0
case v6_0_0.fullyQualifiedVersion | v6_0_0.apiShortVersion => v6_0_0
case v7_0_0.fullyQualifiedVersion | v7_0_0.apiShortVersion => v7_0_0
case `dynamic-endpoint`.fullyQualifiedVersion | `dynamic-endpoint`.apiShortVersion => `dynamic-endpoint`
case `dynamic-entity`.fullyQualifiedVersion | `dynamic-entity`.apiShortVersion => `dynamic-entity`
case version if(scannedApis.map(_.fullyQualifiedVersion).contains(version))

View File

@ -2,18 +2,25 @@ package code.api.v7_0_0
import cats.data.{Kleisli, OptionT}
import cats.effect._
import cats.implicits._
import code.api.util.{APIUtil, CustomJsonFormats}
import code.api.Constant._
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
import code.api.util.APIUtil.{EmptyBody, _}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.{ApiVersionUtils, CustomJsonFormats, NewStyle}
import code.api.v1_4_0.JSONFactory1_4_0
import code.api.v4_0_0.JSONFactory400
import code.bankconnectors.Connector
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.json.Formats
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion}
import net.liftweb.json.JsonAST.prettyRender
import net.liftweb.json.Extraction
import net.liftweb.json.{Extraction, Formats}
import org.http4s._
import org.http4s.dsl.io._
import org.typelevel.vault.Key
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
import scala.language.{higherKinds, implicitConversions}
@ -24,12 +31,13 @@ object Http4s700 {
implicit val formats: Formats = CustomJsonFormats.formats
implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any))
val apiVersion: ScannedApiVersion = ApiVersion.v7_0_0
val apiVersionString: String = apiVersion.toString
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v7_0_0
val versionStatus = ApiVersionStatus.STABLE.toString
val resourceDocs = ArrayBuffer[ResourceDoc]()
case class CallContext(userId: String, requestId: String)
import cats.effect.unsafe.implicits.global
val callContextKey: Key[CallContext] = Key.newKey[IO, CallContext].unsafeRunSync()
val callContextKey: Key[CallContext] =
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
object CallContextMiddleware {
@ -42,31 +50,108 @@ object Http4s700 {
}
}
val v700Services: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> Root / "obp" / `apiVersionString` / "root" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext]
Ok(IO.fromFuture(IO(
for {
_ <- Future() // Just start async call
} yield {
convertAnyToJsonString(
JSONFactory700.getApiInfoJSON(apiVersion, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.")
)
}
)))
object Implementations7_0_0 {
case req @ GET -> Root / "obp" / `apiVersionString` / "banks" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
Ok(IO.fromFuture(IO(
for {
(banks, callContext) <- code.api.util.NewStyle.function.getBanks(None)
} yield {
convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
}
)))
// Common prefix: /obp/v7.0.0
val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString
resourceDocs += ResourceDoc(
null,
implementedInApiVersion,
nameOf(root),
"GET",
"/root",
"Get API Info (root)",
s"""Returns information about:
|
|* API version
|* Hosted by information
|* Git Commit
|${userAuthenticationMessage(false)}""",
EmptyBody,
apiInfoJSON,
List(UnknownError, "no connector set"),
apiTagApi :: Nil,
http4sPartialFunction = Some(root)
)
// Route: GET /obp/v7.0.0/root
val root: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "root" =>
val callContext = req.attributes.lookup(callContextKey).get.asInstanceOf[CallContext]
Ok(IO.fromFuture(IO(
for {
_ <- Future() // Just start async call
} yield {
convertAnyToJsonString(
JSONFactory700.getApiInfoJSON(implementedInApiVersion, s"Hello, ${callContext.userId}! Your request ID is ${callContext.requestId}.")
)
}
)))
}
resourceDocs += ResourceDoc(
null,
implementedInApiVersion,
nameOf(getBanks),
"GET",
"/banks",
"Get Banks",
s"""Get banks on this API instance
|Returns a list of banks supported on this server:
|
|* ID used as parameter in URLs
|* Short and full name of bank
|* Logo URL
|* Website
|${userAuthenticationMessage(false)}""",
EmptyBody,
banksJSON,
List(UnknownError),
apiTagBank :: Nil,
http4sPartialFunction = Some(getBanks)
)
// Route: GET /obp/v7.0.0/banks
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
Ok(IO.fromFuture(IO(
for {
(banks, callContext) <- NewStyle.function.getBanks(None)
} yield {
convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
}
)))
}
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
val logic = for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
tagsParam = httpParams.filter(_.name == "tags").map(_.values).headOption
functionsParam = httpParams.filter(_.name == "functions").map(_.values).headOption
localeParam = httpParams.filter(param => param.name == "locale" || param.name == "language").map(_.values).flatten.headOption
contentParam = httpParams.filter(_.name == "content").map(_.values).flatten.flatMap(ResourceDocsAPIMethodsUtil.stringToContentParam).headOption
apiCollectionIdParam = httpParams.filter(_.name == "api-collection-id").map(_.values).flatten.headOption
tags = tagsParam.map(_.map(ResourceDocTag(_)))
functions = functionsParam.map(_.toList)
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
} yield convertAnyToJsonString(resourceDocsJson)
Ok(IO.fromFuture(IO(logic)))
}
// All routes combined
val allRoutes: HttpRoutes[IO] =
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
root(req).orElse(getBanks(req)).orElse(getResourceDocsObpV700(req))
}
}
val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(v700Services)
val wrappedRoutesV700Services: HttpRoutes[IO] = CallContextMiddleware.withCallContext(Implementations7_0_0.allRoutes)
}

View File

@ -20,6 +20,6 @@ class ApiVersionUtilsTest extends V400ServerSetup {
versions.map(version => ApiVersionUtils.valueOf(version.fullyQualifiedVersion))
//NOTE, when we added the new version, better fix this number manually. and also check the versions
versions.length shouldBe(24)
versions.length shouldBe(25)
}}
}