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:
hongwei 2026-01-22 15:43:49 +01:00
parent df54e60fd0
commit 11e4a71cc4
2 changed files with 149 additions and 33 deletions

View File

@ -11,12 +11,14 @@ import net.liftweb.http.provider.HTTPParam
import net.liftweb.json.{Extraction, compactRender}
import net.liftweb.json.JsonDSL._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.headers.`Content-Type`
import org.typelevel.ci.CIString
import org.typelevel.vault.Key
import java.util.{Date, UUID}
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
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
}
}
}
/**

View File

@ -10,7 +10,7 @@ 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.http4s.Http4sRequestAttributes.RequestOps
import code.api.util.http4s.Http4sRequestAttributes.{RequestOps, EndpointHelpers}
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
@ -105,15 +105,11 @@ object Http4s700 {
// Route: GET /obp/v7.0.0/banks
val getBanks: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" =>
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
EndpointHelpers.executeAndRespond(req) { implicit cc =>
for {
(banks, callContext) <- NewStyle.function.getBanks(Some(cc))
} yield JSONFactory400.createBanksJson(banks)
}
}
resourceDocs += ResourceDoc(
@ -135,16 +131,11 @@ object Http4s700 {
// Authentication handled by ResourceDocMiddleware based on AuthenticatedUserIsRequired
val getCards: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "cards" =>
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
EndpointHelpers.withUser(req) { (user, cc) =>
for {
(cards, callContext) <- NewStyle.function.getPhysicalCardsForUser(user, Some(cc))
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
}
}
resourceDocs += ResourceDoc(
@ -167,19 +158,13 @@ object Http4s700 {
// Authentication and bank validation handled by ResourceDocMiddleware
val getCardsForBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" / bankId / "cards" =>
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
EndpointHelpers.withUserAndBank(req) { (user, bank, cc) =>
for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, Some(cc))
(cards, callContext) <- NewStyle.function.getPhysicalCardsForBank(bank, user, obpQueryParams, callContext)
} yield JSONFactory1_3_0.createPhysicalCardsJSON(cards, user)
}
}
resourceDocs += ResourceDoc(