refactor(http4s): consolidate validated entities into CallContext

- Add bank, bankAccount, view, and counterparty fields to CallContext case class
- Remove individual Vault keys for User, Bank, BankAccount, View, and Counterparty from Http4sRequestAttributes
- Simplify Http4sRequestAttributes to store only CallContext in request attributes
- Update ResourceDocMiddleware to enrich CallContext with validated entities instead of storing them separately
- Remove ValidatedContext case class as validated entities are now part of CallContext
- Streamline request attribute management by centralizing all validated data in a single CallContext object
- Improves code maintainability and reduces complexity in the validation chain
This commit is contained in:
hongwei 2026-01-22 14:13:34 +01:00
parent baecbc110f
commit e8999ba54c
4 changed files with 27 additions and 67 deletions

View File

@ -55,7 +55,12 @@ case class CallContext(
xRateLimitRemaining : Long = -1,
xRateLimitReset : Long = -1,
paginationOffset : Option[String] = None,
paginationLimit : Option[String] = None
paginationLimit : Option[String] = None,
// Validated entities from ResourceDoc middleware (http4s)
bank: Option[Bank] = None,
bankAccount: Option[BankAccount] = None,
view: Option[View] = None,
counterparty: Option[CounterpartyTrait] = None
) extends MdcLoggable {
override def toString: String = SecureLogging.maskSensitive(
s"${this.getClass.getSimpleName}(${this.productIterator.mkString(", ")})"

View File

@ -24,63 +24,28 @@ import scala.language.higherKinds
*
* This file contains:
* - Http4sCallContextBuilder: Builds shared CallContext from http4s Request[IO]
* - Http4sRequestAttributes: Request attribute keys for storing validated objects
* - Http4sRequestAttributes: Request attribute key for storing CallContext
* - ResourceDocMatcher: Matches http4s requests to ResourceDoc entries
* - ResourceDocMiddleware: Validation chain middleware for http4s
* - ErrorResponseConverter: Converts OBP errors to http4s Response[IO]
*
* Validated entities (User, Bank, BankAccount, View, Counterparty) are stored
* directly in CallContext fields, making them available throughout the call chain.
*/
/**
* Vault keys for storing validated objects in http4s request attributes.
* These keys allow middleware to pass validated objects to endpoint handlers.
* WIP
*/
/**
* Request attribute keys for storing validated objects in http4s requests.
* These keys allow middleware to pass validated objects to endpoint handlers.
* Request attribute keys for storing CallContext in http4s requests.
*
* Note: Uses http4s Vault (org.typelevel.vault.Key) for type-safe request attributes.
*/
object Http4sRequestAttributes {
// Use shared CallContext from code.api.util.ApiSession
// CallContext contains all request data and validated entities
val callContextKey: Key[CallContext] =
Key.newKey[IO, CallContext].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
val userKey: Key[User] =
Key.newKey[IO, User].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
val bankKey: Key[Bank] =
Key.newKey[IO, Bank].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
val bankAccountKey: Key[BankAccount] =
Key.newKey[IO, BankAccount].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
val viewKey: Key[View] =
Key.newKey[IO, View].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
val counterpartyKey: Key[CounterpartyTrait] =
Key.newKey[IO, CounterpartyTrait].unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
/**
* Helper methods for accessing validated objects from request attributes
* Get CallContext from request attributes.
* CallContext contains validated entities: bank, bankAccount, view, counterparty
*/
def getCallContext(req: Request[IO]): Option[CallContext] =
req.attributes.lookup(callContextKey)
def getUser(req: Request[IO]): Option[User] =
req.attributes.lookup(userKey)
def getBank(req: Request[IO]): Option[Bank] =
req.attributes.lookup(bankKey)
def getBankAccount(req: Request[IO]): Option[BankAccount] =
req.attributes.lookup(bankAccountKey)
def getView(req: Request[IO]): Option[View] =
req.attributes.lookup(viewKey)
def getCounterparty(req: Request[IO]): Option[CounterpartyTrait] =
req.attributes.lookup(counterpartyKey)
}
/**
@ -331,16 +296,3 @@ object ResourceDocMatcher {
)
}
}
/**
* Validated context containing all validated objects from the middleware chain.
* This is passed to endpoint handlers after successful validation.
*/
case class ValidatedContext(
user: Option[User],
bank: Option[Bank],
bankAccount: Option[BankAccount],
view: Option[View],
counterparty: Option[CounterpartyTrait],
callContext: CallContext
)

View File

@ -243,13 +243,16 @@ object ResourceDocMiddleware extends MdcLoggable{
counterpartyResult.flatMap {
case Left(errorResponse) => IO.pure(errorResponse)
case Right((counterpartyOpt, finalCC)) =>
// All validations passed - store validated context and invoke route
var updatedReq = req.withAttribute(Http4sRequestAttributes.callContextKey, finalCC)
boxUser.toOption.foreach { user => updatedReq = updatedReq.withAttribute(Http4sRequestAttributes.userKey, user) }
bankOpt.foreach { bank => updatedReq = updatedReq.withAttribute(Http4sRequestAttributes.bankKey, bank) }
accountOpt.foreach { account => updatedReq = updatedReq.withAttribute(Http4sRequestAttributes.bankAccountKey, account) }
viewOpt.foreach { view => updatedReq = updatedReq.withAttribute(Http4sRequestAttributes.viewKey, view) }
counterpartyOpt.foreach { counterparty => updatedReq = updatedReq.withAttribute(Http4sRequestAttributes.counterpartyKey, counterparty) }
// All validations passed - update CallContext with validated entities
val enrichedCC = finalCC.copy(
bank = bankOpt,
bankAccount = accountOpt,
view = viewOpt,
counterparty = counterpartyOpt
)
// Store enriched CallContext in request attributes
val updatedReq = req.withAttribute(Http4sRequestAttributes.callContextKey, enrichedCC)
routes.run(updatedReq).getOrElseF(IO.pure(Response[IO](org.http4s.Status.NotFound)))
}
}

View File

@ -137,7 +137,7 @@ object Http4s700 {
case req @ GET -> `prefixPath` / "cards" =>
val response = for {
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
user <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.userKey))(new RuntimeException("User not found in request attributes"))
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))
@ -169,8 +169,8 @@ object Http4s700 {
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
val response = for {
cc <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.callContextKey))(new RuntimeException("CallContext not found in request attributes"))
user <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.userKey))(new RuntimeException("User not found in request attributes"))
bank <- IO.fromOption(req.attributes.lookup(Http4sRequestAttributes.bankKey))(new RuntimeException("Bank not found in request attributes"))
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)