Skip to content

refactor: single source of truth for resource-doc registry (closes BG v1.3 alias gap) - #2902

Merged
simonredfern merged 12 commits into
OpenBankProject:developfrom
hongwei1:feature/bg-v13-alias-resource-doc-registry
Sep 1, 2026
Merged

refactor: single source of truth for resource-doc registry (closes BG v1.3 alias gap)#2902
simonredfern merged 12 commits into
OpenBankProject:developfrom
hongwei1:feature/bg-v13-alias-resource-doc-registry

Conversation

@hongwei1

@hongwei1 hongwei1 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

A bug report proposed a ResourceDocRegistry refactor for a production bug: adding BGv2-getAccountDetails to an API Collection failed with an invalid-operation-id error, because OBP-API kept two hand-maintained resource-doc registries (the per-version dispatcher used by /resource-docs/{VERSION}/... and API Explorer, and the union in APIUtil.allStaticResourceDocs/getAllResourceDocs used wherever an operation id must be resolved) and they had drifted.

Two corrections to that report, verified directly against develop at the time this branch was cut:

  • The error code cited (OBP-30051) is wrong — the actual failure is OBP-40048: Invalid operation_id.
  • The report's "interim fix, uncommitted in my working tree" was already merged (commits a1231e3f5 / 60b9f75d1) before this branch was cut. That merged fix closed the BGv2 gap and the v6-vs-v7 aggregation gap, and already added ResourceDocRegistryParityTest.

This PR verifies that merged fix end-to-end, closes the drift instance it left behind (the Berlin Group v1.3 alias), and — following up on the repo owner's answers to the three open questions the original report raised — implements the proposed refactor: a single ResourceDocRegistry object that both the dispatcher and the global union now derive from, so this class of drift becomes structurally impossible rather than something that has to be remembered by hand each time a standard is added.

Changes

  1. fix: add OBP_BERLIN_GROUP_1_3_Alias.allResourceDocs to APIUtil.allStaticResourceDocs (superseded by commit 4, kept as its own step for bisectability). Reproduced the failure first (OBP-40048 on a valid alias operation id against an instance with berlin_group_v1_3_alias_path set), then confirmed the fix resolves it.
  2. test: extend ResourceDocRegistryParityTest to cover the BG v1.3 alias surface.
  3. refactor: remove ResourceDocsAPIMethods's activeResourceDocs match block — all ~19 arms were confirmed pure identity functions, a leftover from the pre-http4s Lift route-filter era.
  4. refactor: introduce code.api.util.ResourceDocRegistry as the single source of truth for "which resource docs does version X serve". Http4sBGv2 becomes a ScannedApis registrant (fully convention-driven, like the other Berlin Group / UK Open Banking standards — no more hand-maintained entry or special case in ApiVersionUtils.valueOf). The global union is now deduped by operationId. ResourceDocRegistryParityTest is rewritten to iterate the registry itself rather than a hand-typed list of standards, so a newly added standard is covered by construction.
  5. test: add an HTTP-level regression test pinning BGv2-getAccountDetails as a resolvable api-collection-endpoint operation id (the exact request from the sandbox bug report) — previously only covered by a unit-level membership check.
  6. test: enable the Berlin Group v1.3 alias by default in the test environment (berlin_group_v1_3_alias_path=0.6/v1 in test.default.props and both CI workflows' generated copy of it), so its operation ids get the same HTTP-level regression coverage as commit 5, not just a unit-level check. This couldn't be done by toggling the prop per-test at runtime — the alias's ScannedApiVersion identity is captured once by a process-wide classpath scan (ScannedApis.versionMapScannedApis, a lazy val shared across the whole JVM/shard) that gets forced by the first unrelated request that falls through the route chain, almost always before any test-specific prop override could run.

Test plan

  • POST /obp/v4.0.0/my/api-collections/{name}/api-collection-endpoints with operation_id=BGv2-getAccountDetails201 (re-confirms the already-merged fix, both before and after the registry refactor)
  • Same flow exercised through API-Explorer-II's real favourite-star UI (Content.vuecreateMyAPICollectionEndpoint), not just curl
  • Reproduced OBP-40048 on a BG v1.3 alias operation id against an instance with berlin_group_v1_3_alias_path set, before the fix; 201 after
  • Same alias check re-run live after the registry refactor landed — still 201
  • ApiCollectionEndpointTest now exercises both BGv2-getAccountDetails and the alias's BGv1-getPaymentInitiationStatus as real HTTP requests, alongside existing OBPv6.0.0/UK Open Banking/Berlin Group v1.3 canonical coverage
  • ResourceDocRegistryParityTest pins both operation ids and is registry-driven end to end
  • ResourceDocsTest / SwaggerDocsTest / V7ResourceDocsAggregationTest / Http4sBGv2ResourceDocTest / RetiredApiStandardsTest / GetScannedApiVersionsTest green (123 tests) after the registry refactor
  • Full local suite: ./run_tests_parallel.sh — 3583 tests, 0 failures (final run, after all 6 commits, alias enabled by default)

…registry

The resource-docs dispatcher serves the BG v1.3 alias (active only when
berlin_group_v1_3_alias_path is set) through its ScannedApis registration,
but APIUtil.allStaticResourceDocs never included it. Its docs carry their
own operation ids, re-derived from the alias version string, so alias
operation ids failed the getAllResourceDocs membership check used by
api-collection-endpoint creation and other operation-id lookups -- the
same gap BGv2 had before it was added to this union. Reproduced against a
running instance with the alias prop set (OBP-40048 on a valid alias
operation id) and confirmed the fix resolves it.
The alias surface is gated by berlin_group_v1_3_alias_path, which is
unset in the default test environment, so its operation-id list is
legitimately empty there -- skip the non-empty assertion for it while
still running the membership check against getAllResourceDocs.
…ocs dispatcher

Every one of its ~19 arms was `case X => resourceDocs`, unchanged --
a leftover from the pre-http4s Lift route-filter era that stopped doing
any filtering once the corresponding version moved fully onto http4s.
getResourceDocsList now feeds resourceDocs directly into
activePlusLocalResourceDocs, with identical output.
…ion from one registry

Introduce ResourceDocRegistry as the single source of truth for "which
resource docs does version X serve", replacing two independently
hand-maintained registries: ResourceDocsAPIMethods.getResourceDocsList
(the per-version dispatcher used by /resource-docs/{VERSION}/... and API
Explorer) and APIUtil.allStaticResourceDocs (the union used wherever an
operation id must be resolved). These drifted three times by hand --
Berlin Group v2, v7-only operation ids, and the Berlin Group v1.3 alias
all had to be independently added to both places, and were each missed
at least once. Deriving both from one registry map makes that class of
drift structurally impossible going forward.

Http4sBGv2 becomes a ScannedApis registrant (its apiVersion is
ConstantsBG.berlinGroupVersion2), so it is now fully convention-driven
like the other Berlin Group / UK Open Banking standards and needs no
hand-maintained entry in the registry or a special case in
ApiVersionUtils.valueOf. The global union is now deduped by operationId
-- the underlying per-version buffers legitimately overlap (each
OBP-standard aggregation repeats every older version's docs), and
consumers only ever .find or build a lookup map from the result.

ResourceDocRegistryParityTest is rewritten to iterate the registry
itself rather than a hand-typed list of standards, so a future standard
reachable by the dispatcher is covered by construction and the test's
job narrows to catching an accidental regression back to two
independently maintained registries.

Verified live against a running instance, before and after: BGv2 and
Berlin Group v1.3 alias operation ids both still resolve through
POST /my/api-collections/{name}/api-collection-endpoints. Full local
suite: 3582 tests, 0 failures.
@hongwei1 hongwei1 changed the title fix: close remaining resource-doc registry gap (BG v1.3 alias) after BGv2 fix refactor: single source of truth for resource-doc registry (closes BG v1.3 alias gap) Aug 31, 2026
…int operation id

Adds an HTTP-level regression test for the sandbox bug report this
branch started from: creating an API collection endpoint with
operation_id=BGv2-getAccountDetails now returns 201, alongside the
existing coverage for OBPv6.0.0, UK Open Banking, and Berlin Group v1.3
operation ids in the same scenario. Previously the only regression
guard for this exact operation id was the unit-level membership check
in ResourceDocRegistryParityTest; this exercises the actual endpoint.
…evel coverage

berlin_group_v1_3_alias_path could not be toggled per-test at runtime:
its ScannedApiVersion identity is captured once by ScannedApis.
versionMapScannedApis' process-wide classpath scan (a lazy val, shared
across the whole JVM/shard), which gets forced by the first unrelated
request that falls through Http4sApp's route chain -- almost always
long before any test-specific setPropsValues call. The only way to
exercise a real alias operation id end to end is to have the prop
already set before the JVM boots.

Set berlin_group_v1_3_alias_path=0.6/v1 in test.default.props (local)
and both CI workflows' generated test.default.props (build_pull_request.
yml, build_container.yml). Add a regression test in
ApiCollectionEndpointTest mirroring the existing per-standard coverage
(OBPv6.0.0/UK Open Banking/Berlin Group v1.3 canonical) for the alias's
BGv1-getPaymentInitiationStatus operation id, and pin the same
operation id in ResourceDocRegistryParityTest alongside the existing
BGv2-getAccountDetails pin.
…ift instance

The global operation-id union used to be built from the v6.0.0
aggregation, so operation ids belonging to endpoints that exist only in
v7.0.0 were absent from it and could not be added to an API collection.
That drift instance had no regression test: the OBPv6.0.0-* cases in
ApiCollectionEndpointTest pass under both the old v6-based union and the
current v7-based one, so they cannot detect it.

Pin OBPv7.0.0-getMyMetrics (v7-only -- not part of
Http4sResourceDocAggregation.v600) as a real api-collection-endpoint
request, and add the matching named pin in ResourceDocRegistryParityTest
alongside the BGv2 and Berlin Group v1.3 alias ones, so all three
historical drift instances now have both HTTP-level and registry-level
coverage.
…efined order

Two defects from the registry refactor, both in how allStaticResourceDocs
was assembled.

Folding every per-version aggregation into the union added 287 operation
ids it never carried (the older aggregations are not subsets of the v7
one -- an endpoint dropped after v4 keeps its operation id there), and 234
of those collide on partialFunctionName with an entry already present.
Http4s600's top-apis and popular-apis and JSONFactory6.0.0's metrics all
build `partialFunctionName -> operationId` with `.toMap`, where the last
entry wins, so with v1.2.1 sorting last the reported operation_id flipped
to the oldest id: getBanks became OBPv1.2.1-getBanks, root became
OBPv1.2.1-root. Restrict the union to obpUnionVersion (the current OBP
aggregation) plus every non-OBP standard. Consequence, deliberate and
documented at the constant: an operation id living only in a superseded
aggregation stays unresolvable, exactly as before the refactor.

The scanned half of the registry was a plain Map, so the same `.toMap`
consumers resolved a partialFunctionName shared by two scanned standards
according to hash iteration order -- undefined, and free to shift when a
standard is added or removed. The Berlin Group v1.3 alias re-stamps the
canonical BG v1.3 docs and so collides with BG v2 on getAccountDetails
and four other names, and test.default.props now activates that alias for
every test run. Sort it by fullyQualifiedVersion into a ListMap; BG v2
then wins those names, matching the behaviour before this branch.

ResourceDocRegistryParityTest follows the narrowed union and regains the
per-surface non-empty assertion, without which a standard whose docs stop
being registered passes as a trivial subset. A new scenario pins
obpUnionVersion as the newest OBP-standard version in the registry, so
adding a v8 aggregation without moving it fails instead of silently
dropping v8-only operation ids.

Verified against a running instance: OBPv1.2.1-getBanks and
OBPv3.0.0-getAggregateMetrics are rejected with OBP-40048 again, while
BGv2-getAccountDetails, BGv1-getPaymentInitiationStatus and
OBPv7.0.0-getMyMetrics still resolve. Full local suite 3573/0.
"".split("/") returns Array(""), not an empty array, so berlinGroupV13AliasPath
was List("") on a default instance -- nonEmpty. Every downstream
`if (berlinGroupV13AliasPath.nonEmpty)` guard therefore took its ACTIVE branch
with an empty prefix: Http4sBGv13Alias published 55 docs stamped with the
degenerate ScannedApiVersion("", "", ""), whose operation ids came out as
`BG-<name>`, and its route bridge matched the prefix "/" (every request) only
to fall through again.

That was invisible while the alias sat outside the global operation-id union.
Now that this branch folds it in, those 55 junk ids became resolvable: verified
against a running default instance that api-collection-endpoint creation
accepted BG-getAccountDetails and BG-getPaymentInitiationStatus with 201,
naming endpoints no route serves. Filtering empty segments makes "unset" mean
"inactive" again -- both now return 400, while BGv1.3, BGv2, UK and OBP ids are
unaffected and /resource-docs/BGv1.3/obp still serves its 55 docs.

OBP_BERLIN_GROUP_1_3_Alias.apiVersion has to guard .head/.last against the now
genuinely empty list: the ScannedApis classpath scan catches a throwing
companion and only logs a warning, so an unguarded NoSuchElementException would
drop the alias silently. Inactive registrations keep the empty-string version,
which deliberately does not equal ConstantsBG.berlinGroupVersion1 -- colliding
there would let this doc-less object win ScannedApis' .toMap and blank out the
canonical BG v1.3 resource docs.

The alias assertions in both tests no longer depend on a prop that only exists
in a gitignored file. test.default.props is excluded by .gitignore:21, so the
CI workflows carried berlin_group_v1_3_alias_path while a fresh clone or an IDE
runner did not: deleting the line locally reproduced two failures whose
messages gave no hint a prop was missing. They now cancel with an explanatory
message when the alias is inactive, and read the expected operation id back
from the alias's own docs instead of hard-coding the BGv1- prefix, which is
derived from the configured path. Verified both ways: with the prop set 13/13
pass, without it 11 pass and 2 cancel. Full local suite 3573/0.
…passed

Two follow-ups from reviewing the registry work itself.

The scanned half was sorted by fullyQualifiedVersion, which concatenates
apiStandard.toUpperCase and apiShortVersion and can therefore collide across
distinct keys -- ("BG", "v1.3") and ("BGV", "1.3") both render "BGV1.3", and
berlin_group_v1_3_alias_path lets a deployment choose the alias's half of such
a pair. sortBy is only stable with respect to its input, and the input is the
unordered ScannedApis.versionMapScannedApis, so a tie would hand the order back
to hash iteration and with it the `.toMap` winner for a shared
partialFunctionName. Sort by (apiStandard, apiShortVersion) instead: that pair
is exactly ScannedApiVersion's equals/hashCode key, so two distinct keys of
that Map always differ in it and the order is total. The resulting sequence is
unchanged -- alias, BG v1.3, BG v2, UK 2.0/3.1/4.0.1 -- so BG v2 keeps winning
the names it shares with the alias.

The obpUnionVersion guard ranked versions with ApiVersionUtils.versions.indexOf,
which returns -1 for anything absent from that equally hand-maintained list. A
-1 loses every maxBy comparison, so adding a v8.0.0 aggregation to the registry
while forgetting ApiVersionUtils.versions left v7 as the maximum and the
scenario green -- precisely the two-places-to-edit slip it was written to catch.
Assert first that every OBP version in the registry can be ranked at all.
Verified by injecting an unregistered OBPv8.0.0: the guard now fails with
"OBP versions in the registry but missing from ApiVersionUtils.versions:
OBPv8.0.0", where before it passed. Full local suite 3573/0.
…I version

Two defects found reviewing the registry against the union it replaced.

Berlin Group and UK Open Banking both publish getBalances, getAccountList and
getAccountBalances. Http4s600's top-apis/popular-apis and JSONFactory6.0.0's
metrics resolve a partialFunctionName with `.toMap`, which keeps the LAST
matching entry, so registry order decides the operation_id they report. The
hand-written union listed UK before BG, giving Berlin Group all three; sorting
the scanned standards alphabetically put UK last and silently flipped them to
UKv4.0.1-getBalances, UKv2.0-getAccountList and UKv2.0-getAccountBalances.
Replace the alphabetical sort with an explicit standardPrecedence (UK Open
Banking, then Berlin Group) and move Berlin Group v1.3 out of the explicit
block so it is ordered by that precedence rather than pinned ahead of it. A
standard absent from the list -- including the alias, whose apiStandard is
whatever berlin_group_v1_3_alias_path names -- ranks below all of them and can
never override a first-class standard. Verified against a running instance:
the three names resolve to BGv1.3-getBalances, BGv2-getAccountList and
BGv2-getAccountBalances again, matching the values measured before this branch.

A configuration-gated standard that is switched off reports
ScannedApiVersion("", "", ""), whose fullyQualifiedVersion is "" as well. While
ScannedApis kept that registration, ApiVersionUtils.valueOf("") resolved
successfully and, because the resource-docs route tolerates an empty path
segment, GET /obp/v7.0.0/resource-docs//obp answered 200 with an empty document
list where any other unknown version string gets 400 InvalidApiVersionString.
Drop unaddressable registrations in ScannedApis.versionMapScannedApis, which
fixes ApiVersionUtils, ResourceDocRegistry and Boot's version enablement in one
place. Verified: that request now returns 400, and BG v1.3, BG v2, UK 4.0.1 and
OBP v7.0.0 still serve 55, 22, 89 and 1031 docs. With the alias no longer
registered while inactive it is not a registry surface at all, so the parity
test's now-unreachable "cancel when unconfigured" branch is removed.

Both defects reached a green CI because nothing asserted either value; two
scenarios now pin them. Full local suite 3575/0.
…red name

standardPrecedence ranked a version by its apiStandard string, and the alias
takes that string from the first segment of berlin_group_v1_3_alias_path. A
deployment may point it at a name an existing standard already uses:
configured as "BG/v9" the alias reports ScannedApiVersion("BG", "BG", "v9"),
ranks alongside Berlin Group, and -- sorting after "v2" on the tie-breaker --
comes last, so its re-stamped copies won getBalances, getAccountList and
getAccountBalances away from the canonical docs it had copied. Metrics,
top-apis and popular-apis would then report BGv9-getBalances instead of
BGv1.3-getBalances. The comment on standardPrecedence claimed the opposite,
that the alias "can never override a first-class standard no matter how a
deployment configures it".

Match the alias by identity instead and rank it below every listed standard,
which makes that claim true for any configuration. sortKey takes the derived
alias version as a curried parameter and is package-private so the guarantee
can be tested against a synthetic alias, rather than only under whichever
berlin_group_v1_3_alias_path the JVM happens to have booted with.

Verified both directions with the new scenario: reverting to the string-based
rank fails it with "(1,BG,v9) was not less than (1,BG,v2)" -- the mechanism
itself -- and it passes with the fix. Full local suite 3576/0.
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@simonredfern
simonredfern merged commit ebf085f into OpenBankProject:develop Sep 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants