@@ -13,6 +13,7 @@ import code.api.util.http4s.{ErrorResponseConverter, RequestScopeConnection, Res
1313import code .api .util .http4s .Http4sRequestAttributes .{EndpointHelpers , RequestOps }
1414import code .api .util .newstyle .ViewNewStyle
1515import code .api .v2_0_0 .JSONFactory200
16+ import code .api .v5_0_0 .Http4s500
1617import code .api .v5_1_0 .{Http4s510 , JSONFactory510 }
1718import code .api .v6_0_0 .JSONFactory600 .ScannedApiVersionJsonV600
1819import code .accountattribute .AccountAttributeX
@@ -80,12 +81,11 @@ import scala.concurrent.Future
8081 * v6.0.0 http4s endpoints — Phase 1 in progress.
8182 *
8283 * Wire-in into `Http4sApp.baseServices` is performed alongside this object.
83- * The v600→v510 bridge (`v600ToV510Bridge`) is intentionally NOT appended to
84- * `allRoutes`: unmigrated v6 paths must fall through the http4s chain to the
85- * Lift fallback, which still serves the v6 Lift handlers. Adding the bridge
86- * would let v6 *overrides* be hijacked into v5.1 handlers (CLAUDE.md →
87- * "Bridge-cascade hijack"). The bridge val is kept here so it can be enabled
88- * later if the team decides to short-circuit Lift for v6 originals.
84+ * The v600→v500 bridge (`v600ToV500Bridge`) rewrites unhandled v6.0.0 paths
85+ * to v5.0.0 and delegates to Http4s500.wrappedRoutesV500Services, which has a
86+ * working cascade chain (v5.0.0 → v4.0.0 → v3.1.0 → v3.0.0). The bridge
87+ * skips v5.1.0 because Http4s510's own bridge to v5.0.0 is disabled due to
88+ * MetricTest / VRPConsentRequestTest regressions.
8989 */
9090object Http4s600 {
9191
@@ -1790,6 +1790,7 @@ object Http4s600 {
17901790 .orElse(getConnectorTraces(req))
17911791 .orElse(getDynamicEntityDiagnostics(req))
17921792 .orElse(cleanupOrphanedDynamicEntityRecords(req))
1793+ .orElse(createWebUiProps(req))
17931794 .orElse(createOrUpdateWebUiProps(req))
17941795 .orElse(deleteWebUiProps(req))
17951796 .orElse(createCustomViewManagement(req))
@@ -2249,6 +2250,36 @@ object Http4s600 {
22492250 Some (canCleanupOrphanedDynamicEntityRecords :: Nil ),
22502251 http4sPartialFunction = Some (cleanupOrphanedDynamicEntityRecords))
22512252
2253+ // POST /obp/v6.0.0/management/webui_props
2254+ lazy val createWebUiProps : HttpRoutes [IO ] = HttpRoutes .of[IO ] {
2255+ case req @ POST -> `prefixPath` / " management" / " webui_props" =>
2256+ EndpointHelpers .withUserAndBodyCreated[WebUiPropsCommons , Any ](req) { (user, postedData, cc) =>
2257+ for {
2258+ _ <- NewStyle .function.hasEntitlement(" " , user.userId, canCreateWebUiProps, Some (cc))
2259+ _ <- NewStyle .function.tryons(
2260+ s """ $InvalidWebUiProps name must be start with webui_, but current post name is: ${postedData.name} """ ,
2261+ 400 , Some (cc)) { require(postedData.name.startsWith(" webui_" )) }
2262+ webUiProps <- Future (MappedWebUiPropsProvider .createOrUpdate(postedData)) map {
2263+ unboxFullOrFail(_, Some (cc))
2264+ }
2265+ } yield (webUiProps : WebUiPropsCommons )
2266+ }
2267+ }
2268+
2269+ resourceDocs += ResourceDoc (
2270+ null , implementedInApiVersion, nameOf(createWebUiProps), " POST" ,
2271+ " /management/webui_props" ,
2272+ " Create WebUiProps" ,
2273+ s """ Create a WebUiProps.
2274+ |
2275+ | ${APIUtil .userAuthenticationMessage(true )}
2276+ | """ ,
2277+ WebUiPropsCommons (" webui_api_explorer_url" , " https://apiexplorer.openbankproject.com" ),
2278+ WebUiPropsCommons (" webui_api_explorer_url" , " https://apiexplorer.openbankproject.com" , Some (" web-ui-props-id" )),
2279+ List ($AuthenticatedUserIsRequired , UserHasMissingRoles , InvalidJsonFormat , UnknownError ),
2280+ List (apiTagWebUiProps), Some (List (canCreateWebUiProps)),
2281+ http4sPartialFunction = Some (createWebUiProps))
2282+
22522283 // PUT /obp/v6.0.0/management/webui_props/WEBUI_PROP_NAME
22532284 lazy val createOrUpdateWebUiProps : HttpRoutes [IO ] = HttpRoutes .of[IO ] {
22542285 case req @ PUT -> `prefixPath` / " management" / " webui_props" / webUiPropName =>
@@ -3179,7 +3210,11 @@ object Http4s600 {
31793210 case req @ GET -> `prefixPath` / " products" =>
31803211 EndpointHelpers .withUser(req) { (_, cc) =>
31813212 val params = req.uri.query.multiParams.toList.map { case (k, vs) => GetProductsParam (k, vs.toList) }
3182- val cacheKey = APIMethods600 .productsCacheKey(" __all__" , params)
3213+ val cacheKey = {
3214+ val canonical = params.map(p => p.name -> p.value.sorted).sortBy(_._1)
3215+ .map { case (n, vs) => s " $n= ${vs.mkString(" ," )}" }.mkString(" &" )
3216+ s " productsV600:__all__: $canonical"
3217+ }
31833218 val cacheTTL = APIUtil .getPropsAsIntValue(" getAllProductsV600.cache.ttl.seconds" , 60 )
31843219 val hit = code.api.cache.Caching .getFinancialProductsCache(cacheKey, cacheTTL)
31853220 .flatMap(s => try Some (net.liftweb.json.parse(s).extract[ProductsJsonV600 ])
@@ -8470,21 +8505,38 @@ object Http4s600 {
84708505 val allRoutesWithMiddleware : HttpRoutes [IO ] =
84718506 ResourceDocMiddleware .apply(resourceDocs)(allRoutes)
84728507
8473- // ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.1.0/… ─────────────
8508+ // ─── path-rewriting bridge: /obp/v6.0.0/… → /obp/v5.0.0/… ─────────────
8509+ // Targets v5.0.0 (not v5.1.0) because Http4s510's bridge to v5.0.0 is
8510+ // disabled (MetricTest / VRPConsentRequestTest regressions). Http4s500 has
8511+ // its own working cascade: v5.0.0 → v4.0.0 → v3.1.0 → v3.0.0.
84748512 // NOT appended to allRoutes — see object-level scaladoc.
8475- val v600ToV510Bridge : HttpRoutes [IO ] = Kleisli [HttpF , Request [IO ], Response [IO ]] { req =>
8513+ val v600ToV500Bridge : HttpRoutes [IO ] = Kleisli [HttpF , Request [IO ], Response [IO ]] { req =>
84768514 val rawPath = req.uri.path.renderString
84778515 if (rawPath.startsWith(" /obp/v6.0.0/" )) {
8478- val rewritten = rawPath.replaceFirst(" /obp/v6\\ .0\\ .0/" , " /obp/v5.1 .0/" )
8516+ val rewritten = rawPath.replaceFirst(" /obp/v6\\ .0\\ .0/" , " /obp/v5.0 .0/" )
84798517 val newUri = req.uri.withPath(Uri .Path .unsafeFromString(rewritten))
84808518 val rewrittenReq = req.withUri(newUri)
8481- Http4s510 .wrappedRoutesV510Services .run(rewrittenReq)
8519+ Http4s500 .wrappedRoutesV500Services .run(rewrittenReq)
84828520 } else {
84838521 OptionT .none[IO , Response [IO ]]
84848522 }
84858523 }
84868524 }
84878525
8488- val wrappedRoutesV600Services : HttpRoutes [IO ] =
8489- Implementations6_0_0 .allRoutesWithMiddleware
8526+ // `lazy val`, not `val`: `OBPAPI6_0_0` and `APIMethods600` reference
8527+ // `Http4s600.Implementations6_0_0` directly via getstatic. When either is loaded
8528+ // first (during Lift's Boot), the JVM triggers `Implementations6_0_0.<clinit>`
8529+ // before `Http4s600.<clinit>`. Resource-doc registrations inside Impl6.<init>
8530+ // reference `Http4s600.MODULE$`, triggering `Http4s600.<clinit>` recursively on
8531+ // the same thread. JVM allows recursive class init; the partially-initialised
8532+ // `Impl6.MODULE$` is returned. The strict-val `wrappedRoutesV600Services =
8533+ // Impl6.allRoutesWithMiddleware` then reads the not-yet-assigned
8534+ // `allRoutesWithMiddleware` field (still null) and writes null permanently.
8535+ // A `lazy val` defers the read until first access (from Http4sApp after Boot
8536+ // completes), by which time Impl6 is fully initialised.
8537+ lazy val wrappedRoutesV600Services : HttpRoutes [IO ] =
8538+ Kleisli [HttpF , Request [IO ], Response [IO ]] { req =>
8539+ Implementations6_0_0 .allRoutesWithMiddleware.run(req)
8540+ .orElse(Implementations6_0_0 .v600ToV500Bridge.run(req))
8541+ }
84908542}
0 commit comments