From 11e4a71cc4bc0e7969240719b7cbe0008cf64f06 Mon Sep 17 00:00:00 2001 From: hongwei Date: Thu, 22 Jan 2026 15:43:49 +0100 Subject: [PATCH] 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 --- .../code/api/util/http4s/Http4sSupport.scala | 131 ++++++++++++++++++ .../scala/code/api/v7_0_0/Http4s700.scala | 51 +++---- 2 files changed, 149 insertions(+), 33 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 1ba91aecd..60fd2680f 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 @@ -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 + } + } } /** 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 24c900420..55da729fc 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,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(