Skip to content

Commit c4abbd0

Browse files
authored
Merge pull request #2785 from hongwei1/develop-obp
v6.0.0: retire Lift APIMethods600, wire OBPAPI6_0_0 to Http4s600 (+ cascade fixes)
2 parents 145b5e9 + 80b737c commit c4abbd0

12 files changed

Lines changed: 17312 additions & 17563 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
.settings
99
.metals
1010
.vscode
11+
.claude/settings.local.json
1112
*.code-workspace
1213
.zed
1314
.cursor

obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
177177
case ApiVersion.v7_0_0 => resourceDocs
178178
case ConstantsBG.`berlinGroupVersion2` => resourceDocs
179179
case ApiVersion.v1_2_1 => resourceDocs
180+
case ApiVersion.v6_0_0 => resourceDocs // fully on http4s — no Lift route filter
180181
case _ => resourceDocs.filter(rd => versionRoutesClasses.contains(rd.partialFunction.getClass))
181182
}
182183

obp-api/src/main/scala/code/api/util/RateLimitingUtil.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ object RateLimitingUtil extends MdcLoggable {
265265
}
266266

267267
private def incrementConsumerCounters(consumerKey: String, period: LimitCallPeriod, limit: Long): (Long, Long) = {
268-
if (useConsumerLimits) {
268+
if (useConsumerLimits && limit > 0) {
269269
incrementCounter(createUniqueKey(consumerKey, period), period)
270270
} else {
271271
(-1, -1)

obp-api/src/main/scala/code/api/v1_4_0/JSONFactory1_4_0.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ object JSONFactory1_4_0 extends MdcLoggable{
539539
jsonRequestBodyFieldsI18n:String,
540540
jsonResponseBodyFieldsI18n:String
541541
): ResourceDocJson = {
542-
val cacheKey = LOCALISED_RESOURCE_DOC_PREFIX + s"operationId:${operationId}-locale:$locale- isVersion4OrHigher:$isVersion4OrHigher- includeTechnology:$includeTechnology".intern()
542+
val cacheKey = LOCALISED_RESOURCE_DOC_PREFIX + s"operationId:${operationId}-locale:$locale- isVersion4OrHigher:$isVersion4OrHigher- includeTechnology:$includeTechnology-specifiedUrl:${resourceDocUpdatedTags.specifiedUrl.getOrElse("")}".intern()
543543
Caching.memoizeSyncWithImMemory(Some(cacheKey))(CREATE_LOCALISED_RESOURCE_DOC_JSON_TTL.seconds) {
544544
val fieldsDescription =
545545
if (resourceDocUpdatedTags.tags.toString.contains("Dynamic-Entity")

obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala

Lines changed: 17170 additions & 17156 deletions
Large diffs are not rendered by default.

obp-api/src/main/scala/code/api/v6_0_0/Http4s600.scala

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import code.api.util.http4s.{ErrorResponseConverter, RequestScopeConnection, Res
1313
import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
1414
import code.api.util.newstyle.ViewNewStyle
1515
import code.api.v2_0_0.JSONFactory200
16+
import code.api.v5_0_0.Http4s500
1617
import code.api.v5_1_0.{Http4s510, JSONFactory510}
1718
import code.api.v6_0_0.JSONFactory600.ScannedApiVersionJsonV600
1819
import 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
*/
9090
object 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
}

obp-api/src/main/scala/code/api/v6_0_0/OBPAPI6_0_0.scala

Lines changed: 29 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ package code.api.v6_0_0
2929

3030
import scala.language.reflectiveCalls
3131
import code.api.OBPRestHelper
32-
import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints}
32+
import code.api.util.APIUtil.OBPEndpoint
3333
import code.api.util.VersionedOBPApis
3434
import code.api.v1_3_0.APIMethods130
3535
import code.api.v1_4_0.APIMethods140
@@ -45,118 +45,62 @@ import code.api.v5_1_0.{APIMethods510, OBPAPI5_1_0}
4545
import code.util.Helper.MdcLoggable
4646
import com.github.dwickern.macros.NameOf.nameOf
4747
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
48-
import net.liftweb.common.{Box, Full}
49-
import net.liftweb.http.{LiftResponse, PlainTextResponse}
50-
import org.apache.http.HttpStatus
5148

5249
/*
53-
This file defines which endpoints from all the versions are available in v5.0.0
50+
This file defines which endpoints from all the versions are available in v6.0.0.
51+
All v6.0.0 endpoints have been migrated to Http4s600 — this object is retained
52+
only for resource-doc aggregation and the Lift dispatch registry.
5453
*/
55-
object OBPAPI6_0_0 extends OBPRestHelper
56-
with APIMethods130
57-
with APIMethods140
58-
with APIMethods200
59-
with APIMethods210
60-
with APIMethods220
61-
with APIMethods300
62-
with CustomAPIMethods300
63-
with APIMethods310
64-
with APIMethods400
65-
with APIMethods500
66-
with APIMethods510
67-
with APIMethods600
54+
object OBPAPI6_0_0 extends OBPRestHelper
55+
with APIMethods130
56+
with APIMethods140
57+
with APIMethods200
58+
with APIMethods210
59+
with APIMethods220
60+
with APIMethods300
61+
with CustomAPIMethods300
62+
with APIMethods310
63+
with APIMethods400
64+
with APIMethods500
65+
with APIMethods510
6866
with MdcLoggable
6967
with VersionedOBPApis{
7068

7169
val version : ApiVersion = ApiVersion.v6_0_0
7270

7371
val versionStatus = ApiVersionStatus.BLEEDING_EDGE.toString
7472

75-
// Possible Endpoints from 5.1.0, exclude one endpoint use - method,exclude multiple endpoints use -- method,
76-
// e.g getEndpoints(Implementations5_0_0) -- List(Implementations5_0_0.genericEndpoint, Implementations5_0_0.root)
77-
lazy val endpointsOf6_0_0 = getEndpoints(Implementations6_0_0)
78-
79-
// Exclude v5.1.0 root endpoint since v6.0.0 has its own
80-
lazy val endpointsOf5_1_0_without_root = OBPAPI5_1_0.routes.filterNot(_ == Implementations5_1_0.root)
81-
82-
/*
83-
* IMPORTANT: Endpoint Exclusion Pattern
84-
*
85-
* excludeEndpoints is used to filter out old endpoints when v6.0.0 has a DIFFERENT URL pattern.
86-
*
87-
* WHEN TO EXCLUDE:
88-
* - Old and new endpoints have DIFFERENT URLs (e.g., v4.0.0: /users/:username vs v6.0.0: /providers/:provider/users/:username)
89-
* - The old endpoint should not be accessible via v6.0.0 at all
90-
*
91-
* WHEN NOT TO EXCLUDE:
92-
* - Old and new endpoints have the SAME URL and HTTP method (e.g., GET /api/versions)
93-
* - In this case, collectResourceDocs() automatically deduplicates by (URL, method) and keeps newest version
94-
* - Excluding by function name would remove BOTH versions since they share the same name!
95-
*
96-
* Why? The routing works as follows:
97-
* 1. endpoints list = endpointsOf6_0_0 ++ endpointsOf5_1_0_without_root (contains BOTH old and new)
98-
* 2. allResourceDocs = collectResourceDocs() deduplicates docs by (URL, method), keeps newest
99-
* 3. excludeEndpoints filters ResourceDocs by partialFunctionName (removes by name, not by version)
100-
* 4. getAllowedEndpoints() filters endpoints to only those with matching ResourceDocs
101-
*
102-
* Pattern: Add nameOf(Implementations{version}.endpointName) :: with a comment explaining why
103-
*/
104-
lazy val excludeEndpoints =
105-
nameOf(Implementations3_0_0.getUserByUsername) :: // following 4 endpoints miss Provider parameter in the URL, we introduce new ones in V600.
73+
// Re-export so tests that import OBPAPI6_0_0.Implementations6_0_0 still compile.
74+
val Implementations6_0_0 = Http4s600.Implementations6_0_0
75+
76+
lazy val excludeEndpoints =
77+
nameOf(Implementations3_0_0.getUserByUsername) ::
10678
nameOf(Implementations3_1_0.getBadLoginStatus) ::
10779
nameOf(Implementations3_1_0.unlockUser) ::
10880
nameOf(Implementations4_0_0.lockUser) ::
109-
// NOTE: getScannedApiVersions is NOT excluded here because it has the same URL in both v4.0.0 and v6.0.0
110-
// collectResourceDocs() automatically deduplicates by (URL, HTTP method) and keeps the newest version (v6.0.0)
111-
// Excluding by function name would incorrectly filter out BOTH versions since they share the same function name
112-
nameOf(Implementations4_0_0.createUserWithAccountAccess) :: // following 3 endpoints miss ViewId parameter in the URL, we introduce new ones in V600.
81+
nameOf(Implementations4_0_0.createUserWithAccountAccess) ::
11382
nameOf(Implementations4_0_0.grantUserAccessToView) ::
11483
nameOf(Implementations4_0_0.revokeUserAccessToView) ::
115-
nameOf(Implementations4_0_0.revokeGrantUserAccessToViews) ::// this endpoint is forbidden in V600, we do not support multi views in one endpoint from V600.
116-
// v4.0.0 personal user attribute endpoints replaced by /my/personal-data-fields in v6.0.0
84+
nameOf(Implementations4_0_0.revokeGrantUserAccessToViews) ::
11785
nameOf(Implementations4_0_0.getMyPersonalUserAttributes) ::
11886
nameOf(Implementations4_0_0.createMyPersonalUserAttribute) ::
11987
nameOf(Implementations4_0_0.updateMyPersonalUserAttribute) ::
120-
// v5.1.0 non-personal user attribute endpoints replaced by /users/USER_ID/attributes in v6.0.0
12188
nameOf(Implementations5_1_0.createNonPersonalUserAttribute) ::
12289
nameOf(Implementations5_1_0.getNonPersonalUserAttributes) ::
12390
nameOf(Implementations5_1_0.deleteNonPersonalUserAttribute) ::
12491
Nil
125-
126-
// if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc.
92+
93+
// All v6.0.0 endpoints live in Http4s600 — aggregate Http4s600.resourceDocs on top of v5.1.0.
12794
def allResourceDocs = collectResourceDocs(
12895
OBPAPI5_1_0.allResourceDocs,
129-
Implementations6_0_0.resourceDocs
96+
Http4s600.resourceDocs
13097
).filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|")))
13198

132-
// all endpoints - v6.0.0 endpoints first so they take precedence over v5.1.0
133-
private val endpoints: List[OBPEndpoint] = endpointsOf6_0_0.toList ++ endpointsOf5_1_0_without_root
134-
135-
// Filter the possible endpoints by the disabled / enabled Props settings and add them together
136-
// Make root endpoint mandatory (prepend it)
137-
val routes : List[OBPEndpoint] = Implementations6_0_0.root ::
138-
getAllowedEndpoints(endpoints, allResourceDocs)
99+
// No Lift routes — all v6.0.0 endpoints are served by Http4s600.
100+
val routes: List[OBPEndpoint] = Nil
139101

140102
registerRoutes(routes, allResourceDocs, apiPrefix, true)
141103

142-
143104
logger.info(s"version $version has been run! There are ${routes.length} routes, ${allResourceDocs.length} allResourceDocs.")
144-
145-
// specified response for OPTIONS request.
146-
private val corsResponse: Box[LiftResponse] = Full{
147-
val corsHeaders = List(
148-
"Access-Control-Allow-Origin" -> "*",
149-
"Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, PUT, PATCH, DELETE",
150-
"Access-Control-Allow-Headers" -> "*",
151-
"Access-Control-Allow-Credentials" -> "true",
152-
"Access-Control-Max-Age" -> "1728000" //Tell client that this pre-flight info is valid for 20 days
153-
)
154-
PlainTextResponse("", corsHeaders, HttpStatus.SC_NO_CONTENT)
155-
}
156-
/*
157-
* process OPTIONS http request, just return no content and status is 204
158-
*/
159-
this.serve({
160-
case req if req.requestType.method == "OPTIONS" => corsResponse
161-
})
105+
// CORS for OPTIONS is handled by the http4s corsHandler layer — no Lift serve needed here.
162106
}

0 commit comments

Comments
 (0)