From df54e60fd08ae16565c062cde31430fd1cd971a2 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 22 Jan 2026 15:26:59 +0100 Subject: [PATCH] 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 --- .../code/api/util/http4s/Http4sSupport.scala | 69 +++++++-------- .../scala/code/api/v7_0_0/Http4s700.scala | 83 +++++++++---------- 2 files changed, 74 insertions(+), 78 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala index 39d5ad0ae..1ba91aecd 100644 --- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala +++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala @@ -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") + ) + } } } diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 9811b61c7..24c900420 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -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 - } }