refactor(http4s): simplify CallContext access with implicit RequestOps extension

- Replace withCallContext helper method with implicit RequestOps extension class
- Add `req.callContext` syntax for cleaner CallContext extraction in endpoints
- Enhance Http4sRequestAttributes documentation with usage examples
- Update Http4s700 endpoints to use new implicit CallContext accessor pattern
- Remove nested callback pattern in favor of direct implicit CallContext availability
- Improve code readability by eliminating withCallContext wrapper boilerplate
- Add RequestOps import to Http4s700 for implicit extension method support
This commit is contained in:
hongwei 2026-01-22 15:26:59 +01:00
parent dbd046bf7c
commit df54e60fd0
2 changed files with 74 additions and 78 deletions

View File

@ -24,7 +24,7 @@ import scala.language.higherKinds
*
* This file contains:
* - Http4sCallContextBuilder: Builds shared CallContext from http4s Request[IO]
* - Http4sRequestAttributes: Request attribute key for storing CallContext
* - Http4sRequestAttributes: Provides CallContext access from http4s requests
* - ResourceDocMatcher: Matches http4s requests to ResourceDoc entries
*
* Validated entities (User, Bank, BankAccount, View, Counterparty) are stored
@ -32,50 +32,51 @@ import scala.language.higherKinds
*/
/**
* Request attribute keys for storing CallContext in http4s requests.
* Request attribute keys and helpers for accessing CallContext in http4s requests.
*
* Note: Uses http4s Vault (org.typelevel.vault.Key) for type-safe request attributes.
* CallContext is stored in http4s request attributes using Vault (type-safe key-value store).
* Validated entities (bank, bankAccount, view, counterparty) are stored within CallContext itself.
*
* Usage in endpoints:
* {{{
* import Http4sRequestAttributes.RequestOps
*
* val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] {
* case req @ GET -> Root / "banks" =>
* implicit val cc: CallContext = req.callContext
* for {
* result <- yourBusinessLogic // cc is implicitly available
* response <- Ok(result)
* } yield response
* }
* }}}
*/
object Http4sRequestAttributes {
import org.typelevel.vault.Key
// CallContext contains all request data and validated entities
/**
* Vault key for storing CallContext in http4s request attributes.
* CallContext contains all request data and validated entities.
*/
val callContextKey: Key[CallContext] =
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
/**
* Get CallContext from request attributes.
* CallContext contains validated entities: bank, bankAccount, view, counterparty
* Implicit class that adds CallContext accessor to Request[IO].
* Import RequestOps to enable `req.callContext` syntax.
*/
def getCallContext(req: Request[IO]): Option[CallContext] =
req.attributes.lookup(callContextKey)
/**
* Helper method to extract CallContext from http4s Request and execute business logic.
* Simplifies endpoint code by handling the common pattern of extracting CallContext.
*
* Usage example:
* {{{
* val myEndpoint: HttpRoutes[IO] = HttpRoutes.of[IO] {
* case req @ GET -> Root / "banks" =>
* withCallContext(req) { cc =>
* for {
* result <- yourBusinessLogic(cc)
* response <- Ok(result)
* } yield response
* }
* }
* }}}
*
* @param req The http4s request
* @param f Function that takes CallContext and returns IO[Response]
* @return IO[Response[IO]]
*/
def withCallContext(req: Request[IO])(f: CallContext => IO[Response[IO]]): IO[Response[IO]] = {
IO.fromOption(req.attributes.lookup(callContextKey))(
new RuntimeException("CallContext not found in request attributes")
).flatMap(f)
implicit class RequestOps(val req: Request[IO]) extends AnyVal {
/**
* Extract CallContext from request attributes.
* Throws RuntimeException if CallContext is not found (should never happen with ResourceDocMiddleware).
*
* @return CallContext containing validated user, bank, account, view, counterparty
*/
def callContext: CallContext = {
req.attributes.lookup(callContextKey).getOrElse(
throw new RuntimeException("CallContext not found in request attributes")
)
}
}
}

View File

@ -10,7 +10,8 @@ import code.api.util.ApiRole.{canGetCardsForBank, canReadResourceDoc}
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
import code.api.util.{ApiRole, ApiVersionUtils, CustomJsonFormats, NewStyle}
import code.api.util.http4s.Http4sRequestAttributes.RequestOps
import code.api.util.{ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
import code.api.v1_3_0.JSONFactory1_3_0
import code.api.v1_4_0.JSONFactory1_4_0
import code.api.v4_0_0.JSONFactory400
@ -75,7 +76,6 @@ object Http4s700 {
val responseJson = convertAnyToJsonString(
JSONFactory700.getApiInfoJSON(implementedInApiVersion, versionStatus)
)
Ok(responseJson)
}
@ -105,16 +105,15 @@ object Http4s700 {
// Route: GET /obp/v7.0.0/banks
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" =>
Http4sRequestAttributes.withCallContext(req) { cc =>
for {
result <- IO.fromFuture(IO {
for {
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
})
response <- Ok(result)
} yield response
}
implicit val cc: CallContext = req.callContext
for {
result <- IO.fromFuture(IO {
for {
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
})
response <- Ok(result)
} yield response
}
resourceDocs += ResourceDoc(
@ -136,17 +135,16 @@ object Http4s700 {
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "cards" =>
Http4sRequestAttributes.withCallContext(req) { cc =>
for {
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
result <- IO.fromFuture(IO {
for {
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
response <- Ok(result)
} yield response
}
implicit val cc: CallContext = req.callContext
for {
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
result <- IO.fromFuture(IO {
for {
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
response <- Ok(result)
} yield response
}
resourceDocs += ResourceDoc(
@ -169,20 +167,19 @@ object Http4s700 {
// Authentication and bank validation handled by ResourceDocMiddleware
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
Http4sRequestAttributes.withCallContext(req) { cc =>
for {
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
result <- IO.fromFuture(IO {
for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
response <- Ok(result)
} yield response
}
implicit val cc: CallContext = req.callContext
for {
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
result <- IO.fromFuture(IO {
for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
})
response <- Ok(result)
} yield response
}
resourceDocs += ResourceDoc(
@ -219,12 +216,11 @@ object Http4s700 {
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
import com.openbankproject.commons.ExecutionContext.Implicits.global
Http4sRequestAttributes.withCallContext(req) { cc =>
for {
result <- IO.fromFuture(IO {
// Check resource_docs_requires_role property
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
implicit val cc: CallContext = req.callContext
for {
result <- IO.fromFuture(IO {
// Check resource_docs_requires_role property
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
for {
// Authentication based on property
@ -258,7 +254,6 @@ object Http4s700 {
})
response <- Ok(result)
} yield response
}
}