mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
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:
parent
dbd046bf7c
commit
df54e60fd0
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user