mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:47:18 +00:00
feature(http4s): add EndpointHelpers for simplified endpoint implementations
- Add EndpointHelpers object with reusable endpoint execution patterns - Implement executeAndRespond helper for Future-based business logic execution - Implement withUser helper to extract and validate User from CallContext - Implement withBank helper to extract and validate Bank from CallContext - Implement withUserAndBank helper for endpoints requiring both User and Bank - Add comprehensive documentation and usage examples for each helper - Import EndpointHelpers in Http4s700 for endpoint implementation - Reduce boilerplate in endpoint implementations by centralizing common patterns - Improve code consistency and maintainability across http4s endpoints
This commit is contained in:
parent
df54e60fd0
commit
11e4a71cc4
@ -11,12 +11,14 @@ import net.liftweb.http.provider.HTTPParam
|
|||||||
import net.liftweb.json.{Extraction, compactRender}
|
import net.liftweb.json.{Extraction, compactRender}
|
||||||
import net.liftweb.json.JsonDSL._
|
import net.liftweb.json.JsonDSL._
|
||||||
import org.http4s._
|
import org.http4s._
|
||||||
|
import org.http4s.dsl.io._
|
||||||
import org.http4s.headers.`Content-Type`
|
import org.http4s.headers.`Content-Type`
|
||||||
import org.typelevel.ci.CIString
|
import org.typelevel.ci.CIString
|
||||||
import org.typelevel.vault.Key
|
import org.typelevel.vault.Key
|
||||||
|
|
||||||
import java.util.{Date, UUID}
|
import java.util.{Date, UUID}
|
||||||
import scala.collection.mutable.ArrayBuffer
|
import scala.collection.mutable.ArrayBuffer
|
||||||
|
import scala.concurrent.Future
|
||||||
import scala.language.higherKinds
|
import scala.language.higherKinds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,6 +80,135 @@ object Http4sRequestAttributes {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper methods to simplify endpoint implementations.
|
||||||
|
* These eliminate boilerplate for common patterns in http4s endpoints.
|
||||||
|
*/
|
||||||
|
object EndpointHelpers {
|
||||||
|
import net.liftweb.json.{Extraction, Formats}
|
||||||
|
import net.liftweb.json.JsonAST.prettyRender
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a Future-based business logic function and return JSON response.
|
||||||
|
* Handles Future execution, JSON conversion, and Ok response creation.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* {{{
|
||||||
|
* case req @ GET -> Root / "banks" =>
|
||||||
|
* executeAndRespond(req) { implicit cc =>
|
||||||
|
* for {
|
||||||
|
* (banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||||
|
* } yield JSONFactory400.createBanksJson(banks)
|
||||||
|
* }
|
||||||
|
* }}}
|
||||||
|
*
|
||||||
|
* @param req The http4s request
|
||||||
|
* @param f Business logic function that takes CallContext and returns Future[A]
|
||||||
|
* @param formats Implicit JSON formats for serialization
|
||||||
|
* @tparam A The result type (will be converted to JSON)
|
||||||
|
* @return IO[Response[IO]] with JSON body
|
||||||
|
*/
|
||||||
|
def executeAndRespond[A](req: Request[IO])(f: CallContext => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||||
|
implicit val cc: CallContext = req.callContext
|
||||||
|
for {
|
||||||
|
result <- IO.fromFuture(IO(f(cc)))
|
||||||
|
jsonString = prettyRender(Extraction.decompose(result))
|
||||||
|
response <- Ok(jsonString)
|
||||||
|
} yield response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute business logic that requires validated User from CallContext.
|
||||||
|
* Extracts User from CallContext, executes business logic, and returns JSON response.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* {{{
|
||||||
|
* case req @ GET -> Root / "cards" =>
|
||||||
|
* withUser(req) { (user, cc) =>
|
||||||
|
* for {
|
||||||
|
* (cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||||
|
* } yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||||
|
* }
|
||||||
|
* }}}
|
||||||
|
*
|
||||||
|
* @param req The http4s request
|
||||||
|
* @param f Business logic function that takes (User, CallContext) and returns Future[A]
|
||||||
|
* @param formats Implicit JSON formats for serialization
|
||||||
|
* @tparam A The result type (will be converted to JSON)
|
||||||
|
* @return IO[Response[IO]] with JSON body
|
||||||
|
*/
|
||||||
|
def withUser[A](req: Request[IO])(f: (User, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||||
|
implicit val cc: CallContext = req.callContext
|
||||||
|
for {
|
||||||
|
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
||||||
|
result <- IO.fromFuture(IO(f(user, cc)))
|
||||||
|
jsonString = prettyRender(Extraction.decompose(result))
|
||||||
|
response <- Ok(jsonString)
|
||||||
|
} yield response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute business logic that requires validated Bank from CallContext.
|
||||||
|
* Extracts Bank from CallContext, executes business logic, and returns JSON response.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* {{{
|
||||||
|
* case req @ GET -> Root / "banks" / bankId / "accounts" =>
|
||||||
|
* withBank(req) { (bank, cc) =>
|
||||||
|
* for {
|
||||||
|
* (accounts, callContext) <- NewStyle.function.getBankAccounts(bank, Some(cc))
|
||||||
|
* } yield JSONFactory400.createAccountsJson(accounts)
|
||||||
|
* }
|
||||||
|
* }}}
|
||||||
|
*
|
||||||
|
* @param req The http4s request
|
||||||
|
* @param f Business logic function that takes (Bank, CallContext) and returns Future[A]
|
||||||
|
* @param formats Implicit JSON formats for serialization
|
||||||
|
* @tparam A The result type (will be converted to JSON)
|
||||||
|
* @return IO[Response[IO]] with JSON body
|
||||||
|
*/
|
||||||
|
def withBank[A](req: Request[IO])(f: (Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||||
|
implicit val cc: CallContext = req.callContext
|
||||||
|
for {
|
||||||
|
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
||||||
|
result <- IO.fromFuture(IO(f(bank, cc)))
|
||||||
|
jsonString = prettyRender(Extraction.decompose(result))
|
||||||
|
response <- Ok(jsonString)
|
||||||
|
} yield response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute business logic that requires both User and Bank from CallContext.
|
||||||
|
* Extracts both from CallContext, executes business logic, and returns JSON response.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* {{{
|
||||||
|
* case req @ GET -> Root / "banks" / bankId / "cards" =>
|
||||||
|
* withUserAndBank(req) { (user, bank, cc) =>
|
||||||
|
* for {
|
||||||
|
* (cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, Some(cc))
|
||||||
|
* } yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||||
|
* }
|
||||||
|
* }}}
|
||||||
|
*
|
||||||
|
* @param req The http4s request
|
||||||
|
* @param f Business logic function that takes (User, Bank, CallContext) and returns Future[A]
|
||||||
|
* @param formats Implicit JSON formats for serialization
|
||||||
|
* @tparam A The result type (will be converted to JSON)
|
||||||
|
* @return IO[Response[IO]] with JSON body
|
||||||
|
*/
|
||||||
|
def withUserAndBank[A](req: Request[IO])(f: (User, Bank, CallContext) => Future[A])(implicit formats: Formats): IO[Response[IO]] = {
|
||||||
|
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(f(user, bank, cc)))
|
||||||
|
jsonString = prettyRender(Extraction.decompose(result))
|
||||||
|
response <- Ok(jsonString)
|
||||||
|
} yield response
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import code.api.util.ApiRole.{canGetCardsForBank, canReadResourceDoc}
|
|||||||
import code.api.util.ApiTag._
|
import code.api.util.ApiTag._
|
||||||
import code.api.util.ErrorMessages._
|
import code.api.util.ErrorMessages._
|
||||||
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
|
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
|
||||||
import code.api.util.http4s.Http4sRequestAttributes.RequestOps
|
import code.api.util.http4s.Http4sRequestAttributes.{RequestOps, EndpointHelpers}
|
||||||
import code.api.util.{ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
|
import code.api.util.{ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
|
||||||
import code.api.v1_3_0.JSONFactory1_3_0
|
import code.api.v1_3_0.JSONFactory1_3_0
|
||||||
import code.api.v1_4_0.JSONFactory1_4_0
|
import code.api.v1_4_0.JSONFactory1_4_0
|
||||||
@ -105,15 +105,11 @@ object Http4s700 {
|
|||||||
// Route: GET /obp/v7.0.0/banks
|
// Route: GET /obp/v7.0.0/banks
|
||||||
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||||
case req @ GET -> `prefixPath` / "banks" =>
|
case req @ GET -> `prefixPath` / "banks" =>
|
||||||
implicit val cc: CallContext = req.callContext
|
EndpointHelpers.executeAndRespond(req) { implicit cc =>
|
||||||
for {
|
for {
|
||||||
result <- IO.fromFuture(IO {
|
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
||||||
for {
|
} yield JSONFactory400.createBanksJson(banks)
|
||||||
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
|
}
|
||||||
} yield convertAnyToJsonString(JSONFactory400.createBanksJson(banks))
|
|
||||||
})
|
|
||||||
response <- Ok(result)
|
|
||||||
} yield response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceDocs += ResourceDoc(
|
resourceDocs += ResourceDoc(
|
||||||
@ -135,16 +131,11 @@ object Http4s700 {
|
|||||||
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
|
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
|
||||||
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||||
case req @ GET -> `prefixPath` / "cards" =>
|
case req @ GET -> `prefixPath` / "cards" =>
|
||||||
implicit val cc: CallContext = req.callContext
|
EndpointHelpers.withUser(req) { (user, cc) =>
|
||||||
for {
|
for {
|
||||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
||||||
result <- IO.fromFuture(IO {
|
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||||
for {
|
}
|
||||||
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
|
|
||||||
} yield convertAnyToJsonString(JSONFactory1_3_0.createPhysicalCardsJSON(cards, user))
|
|
||||||
})
|
|
||||||
response <- Ok(result)
|
|
||||||
} yield response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceDocs += ResourceDoc(
|
resourceDocs += ResourceDoc(
|
||||||
@ -167,19 +158,13 @@ object Http4s700 {
|
|||||||
// Authentication and bank validation handled by ResourceDocMiddleware
|
// Authentication and bank validation handled by ResourceDocMiddleware
|
||||||
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||||
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
|
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
|
||||||
implicit val cc: CallContext = req.callContext
|
EndpointHelpers.withUserAndBank(req) { (user, bank, cc) =>
|
||||||
for {
|
for {
|
||||||
user <- IO.fromOption(cc.user.toOption)(new RuntimeException("User not found in CallContext"))
|
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
|
||||||
bank <- IO.fromOption(cc.bank)(new RuntimeException("Bank not found in CallContext"))
|
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
|
||||||
result <- IO.fromFuture(IO {
|
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
|
||||||
for {
|
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
|
||||||
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(
|
resourceDocs += ResourceDoc(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user