Skip to content

OpenConceptLab/ocl_online#451 | FHIR R4 terminology service for the SVCM transactions - #956

Open
paynejd wants to merge 18 commits into
masterfrom
ocl_online/451-fhir-svcm-pr
Open

paynejd wants to merge 18 commits into
masterfrom
ocl_online/451-fhir-svcm-pr

Conversation

@paynejd

@paynejd paynejd commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

FHIR R4 terminology service (SVCM transactions)

This series makes oclapi2's FHIR endpoints a FHIR R4 terminology service for the IHE SVCM transactions: CodeSystem, ValueSet and ConceptMap search and read, $lookup, $validate-code, $expand, $translate, and the CapabilityStatement and TerminologyCapabilities. It serves them at the global base (/fhir/) and at owner bases (/orgs/{org}/, /users/{user}/), in JSON and XML. Tracking: OpenConceptLab/ocl_online#451.

How to review it: 17 commits on master addb3d5e: one per work package, each with a message describing its behaviour, then a fix from this PR's review. Only the final tree has been gated (the full test suite, pylint and coverage). Every intermediate tree compiles but hasn't been run on its own.

Commits

  1. FHIR requests and errors use native FHIR JSON and XML
  2. Canonical lookups preserve registry entries and support FHIR version rules
  3. FHIR search and read return the releases a caller may view
  4. Missing FHIR operation inputs get consistent, typed error outcomes
  5. FHIR lookup and validation answer for the requested version and value set
  6. FHIR translation returns the requested map's live mappings and R4 equivalences
  7. FHIR expansion pages the requested value set's stored members
  8. FHIR metadata describes the operations and code-system versions each base serves
  9. ConceptMap system searches use indexed candidates and check each release's mappings
  10. CodeSystem and ValueSet titles fall back to the name when no full name is set
  11. A named FHIR version resolves to a visible repository that holds it
  12. Value set members stored as root concept rows report a holding release
  13. FHIR operations reject parameters with competing values and explain ConceptMap URI searches
  14. Root concept members report the newest visible release that holds the concept
  15. Filtered expansions match members once for both the total and the page
  16. Instance operations serve named unreleased versions; capped reads say they're partial
  17. Only /fhir/ and owner bases are FHIR paths (review fix)

Outside core/fhir/

These are worth a closer look, because they change code that non-FHIR paths share:

  • The shared canonical resolver (ConceptContainerModel.resolve_reference_expression / resolve_repo in core/common/models.py, core/repos/models.py, core/url_registry/models.py, core/common/mixins.py). FHIR resolves through the same resolver as $resolveReference, with flags that only FHIR passes: no HEAD by default, the HEADs a base shows, and a visibility check before any version is read. Registry reads no longer save or reindex entries. With the URL registry on, $resolveReference accepts namespace for expressions that name none; an expression's own namespace wins.
  • Middleware and settings: FHIR errors are OperationOutcomes from an outermost middleware, and a server error on a FHIR path becomes a 422. A FHIR path is one under /fhir/ or under an owner base's FHIR routes; other routes keep their own errors. The FHIR validator setting is gone, because XML is converted locally.
  • Dependencies: fhir.resources[xml] goes from 7.1.0 to 8.3.0, and the CodeSystem converter used by imports moves to the R4B models.
  • Routing: the FHIR routes at the owner bases and the global base.

There are no migrations. The URL registry toggle stays as it is: FHIR works with it on or off.

Testing

  • Full suite on the final tree: 3,452 tests OK, pylint 10.00/10, coverage 96% (99% in core/fhir).
  • Parity: FHIR answers were compared with FHIRsmith v0.15.0 as an oracle, on a local copy of the corpus and on a staging deployment of this tree's line. Every difference is either fixed or recorded with its reason. The report is on the tracking ticket.

Known limits and follow-ups

  • reference searches: OpenConceptLab/ocl_online#547.
  • Computed expansions, hierarchy and subsumption, and later FHIR versions: OpenConceptLab/ocl_online#452.
  • Expansion member versions for root-row members: OpenConceptLab/ocl_online#549.
  • $expand with filter on the largest stored expansions (about 38,000 members) can exceed the database time limit and answer 422: OpenConceptLab/ocl_online#564.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8

paynejd and others added 17 commits October 22, 2026 12:23
…HIR JSON and XML

FHIR views now accept and return FHIR JSON and XML through the shared
core/fhir/base.py layer. _format overrides Accept, media aliases and quality
values are honoured, and unsupported FHIR versions are refused. XML is
converted locally with fhir.resources' R4B models (fhir.resources[xml]
8.3.0, up from 7.1.0), so XML conversion no longer calls the validator
service and the FHIR_VALIDATOR_URL application setting is removed. The
shared CodeSystem converter
(core/code_systems/converter.py) moves to the R4B model and model_dump().

XML follows FHIR element order and preserves the values JSON carries.
Unsupported elements and shapes are omitted, a single-valued element keeps
its first item, and characters XML cannot represent become replacement
characters.

Errors from views, routing and middleware return OperationOutcome with
terminology issue coding; server errors become 422 responses.
FhirErrorMiddleware runs outermost, and core/wsgi.py installs
FhirWSGIRequest, so a malformed Content-Type parameter or a non-text charset
on a FHIR path is a FHIR JSON 400 rather than an error page; other paths are
unchanged. metadata also answers with a trailing slash, and names the
request's own FHIR base. Tests cover negotiation, JSON and XML input and
output, malformed headers and queries, serializer outcomes and middleware
failures.

Work package WP1 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…ntries and support FHIR version rules

Canonical resolution can select a repository type and require a released
default version. The new core/fhir/resolve.py applies the FHIR base's
namespace and access rules, preserves the resource type of OCL paths, and
uses HEAD only when named. Repositories without a release are absent.
Unavailable repositories receive the same not-found answer as missing ones.

$resolveReference accepts namespace for expressions that name none when the
URL registry is enabled; an expression's own namespace wins. Existing
shared-resolver callers keep their default resolution rules.

Registry reads no longer save or reindex entries. Repository writes refresh
cached links, and ConceptContainerModel.delete in core/common/models.py
preserves entries and resolves their links again after deletion. Link
updates save only their own fields, preserving concurrent edits.

Tests cover both registry settings, resource types, namespaces, named
versions and query counts, plus link maintenance, concurrent edits and
retries of interrupted repository or version deletes.

Work package WP2 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…ses a caller may view

CodeSystem, ValueSet and ConceptMap search and read share
core/fhir/search.py. Search lists released versions with real totals, paging
links and SUBSETTED summaries. Global reads use an opaque id per release;
owner reads use the mnemonic and a named version or the newest visible
release. meta.versionId distinguishes releases at both bases.

Search supports SVCM parameters, token systems, date comparisons, comma
alternatives and repeated criteria. String searches ignore case and accents
except with :exact; identifiers and canonical URLs match the values
resources show. CodeSystem reads include all concepts up to 1,000, then
report not-present.

A release is visible only with a viewable active HEAD. The global base shows
public content; owner bases apply the caller's rights. Source and reference
filters can match any owner's source the caller may view. ConceptMap
creation by PUT or PATCH checks ownership, and deletion of a missing map
returns 404.

Search evaluates its criteria once, batches canonical lookups, accepts at
most 20 values per parameter and 50 overall, and bounds database work to
five seconds. Tests cover search semantics, paging, read content, access,
write ownership, query counts and too-costly outcomes.

Work package WP3 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…sistent, typed error outcomes

Required-input errors in $validate-code and $translate now use the shared
FHIR InvalidError. A missing required input is a 400 OperationOutcome naming
it, with terminology issue coding. CodeSystem $validate-code for an unknown
or unavailable system still answers result=false, with or without a code.

core/fhir/tests_operation_bounds.py checks FHIR JSON POSTs and the issue
coding, alongside bounded concept and mapping reads and unchanged answers
for list-only options.

It replaces master's copy of these bounds tests
(core/code_systems/tests_operation_bounds.py, which came from this work)
with core/fhir/tests_operation_bounds.py. Its $translate cases run by GET
here; the POST cases come back with the $translate work later in the series.

The operation input checks of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
… the requested version and value set

$lookup and CodeSystem and ValueSet $validate-code now use shared operations
in core/fhir/ops and an R4 response adapter. GET and POST accept separate
inputs or a Coding, with instance routes for lookup and validation.
CodeableConcept validation accepts one coding.

Operations resolve the newest release the caller may view unless a version
is named. Without the URL registry, a canonical at the global base resolves
only among repositories the caller is shown, so a private copy never hides a
public one; the shared resolver in core/common/models.py gains optional
heads and head_shown arguments for this, and other callers keep their
defaults. Owner instance routes accept HEAD when explicitly named.
Conflicting systems or versions are reported, deleted concepts are absent,
and unavailable repositories receive the same answers as missing ones.

$lookup returns the resolved version, display, designations and inactive
property. Validation returns the code, system, version, display and issues;
display checks accept any concept name regardless of case, and retired codes
remain valid with a warning. ValueSet membership comes from its default
expansion, reporting a visible holding release: the explicit pin, then the
evaluated pin, then the newest.

Tests cover GET and POST, instance routes, encoded codes, display languages,
version conflicts, expansion membership and access with both registry
settings. Query checks keep reads focused on the named code as repositories
grow.

Work package WP4 + WP5 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…d map's live mappings and R4 equivalences

$translate now reads the resolved ConceptMap version's own live mappings
through core/fhir/ops/translate.py. GET takes code and system, or a
system|code coding token; a POSTed Parameters also takes a Coding or a
CodeableConcept. url and conceptMapVersion select the map. Without url, a
forward translation uses the input system's own map, and a reverse one uses
targetsystem's (with no targetsystem there is no map).

Map types and stored relationship spellings become R4 equivalences. Reverse
translation swaps direction and wider or narrower meaning; code-system
versions and targetsystem narrow the matches. The input code must belong to
its source scope, or target scope in reverse, with ValueSet paths resolved
as value sets.

Translation, ConceptMap reads and system searches name mapping sides
consistently: a visible source's canonical URL, otherwise the mapping's
stored URL. Deleted or retired mappings are omitted, each match names its
map, and results list at most 1,000 matches within the search time bound.

Tests cover versions, aliases, scopes, reverse equivalences, GET and POST,
JSON and XML, access rules and bounded queries. ConceptMap reads group
repeated source codes together, and writes store equivalences as the
corresponding map types.

Work package WP6 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…lue set's stored members

ValueSet $expand now serves the resolved version's default stored expansion
at type and instance routes. GET and POST only read it; neither computes nor
saves an expansion. Missing, unfinished or unresolved expansions return a
422 OperationOutcome.

Responses carry the ValueSet, expansion identifier and timestamp, total,
offset, echoed inputs and used code systems. Each visible member has its
system, code and display, with a holding release when available, inactive
status and optional designations. count and offset page up to 1,000 members;
activeOnly, word-prefix filter and displayLanguage shape the answer.

Members report their release's own canonical URL when present, and ValueSet
validation accepts that system too. Filters match decoded codes and names.
Inputs requiring a newly computed expansion are refused explicitly, and
members whose code systems the caller cannot view contribute neither content
nor totals.

Tests cover paging, filtering, languages, designations, release selection,
access and unsupported inputs with both registry settings. They also check
that POST writes nothing, queries stay bounded and timeouts return too-
costly outcomes.

Work package WP7 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…s and code-system versions each base serves

CapabilityStatement is built from the routes and search definitions in
core/fhir/capabilities.py. It advertises FHIR R4, the available interactions
and operations, SVCM terminology support, and the request's own base URL.
metadata and GET $versions work at global, organization and user bases, with
either trailing-slash form.

TerminologyCapabilities lists the code systems the caller may view and their
released versions, each under its canonical URL or its OCL CodeSystem path.
The default release is marked, and expansion and translation capabilities
follow the routed operations.

Business versions are listed as stored and resolve without a second decoding
changing their meaning. OCL-to-FHIR path conversion replaces only the
repository-type segment, preserving owner and repository names.

Tests compare advertised routes, parameters and versions with the
implementation, check JSON and XML at every base, and cover registry
choices, shared canonicals, access, query counts and timeouts.

Work package WP8 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…d candidates and check each release's mappings

ConceptMap source-system and target-system searches use
core/fhir/mapping_index.py to find candidate repositories and versions. The
database checks each candidate through its own live mappings, avoiding a
join over every mapping of a heavily mapped source.

Index requests are scoped by the versions the database shows the caller, use
one bounded request per parameter and require a complete answer. Time or
aggregation limits return too-costly; unavailable or partial index responses
return a 422 OperationOutcome.

Recent releases of repositories found in the index are checked for seven
days even if the release is not indexed yet. Matches through a source's
canonical URL need the index: repositories with no indexed mappings, and
older unindexed releases, are absent until indexed. Matches on the URL a
mapping stores, external URLs and OCL paths alike, don't need the mappings
to be indexed, but a search that also names a visible source's canonical
URL needs a complete answer from the index.

Tests cover candidate verification, case-distinct owners, current access
rules, recent releases, index lag, incomplete responses, timeouts and fixed
database query counts with both registry settings.

Work package WP3b of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…ack to the name when no full name is set

CodeSystem and ValueSet reads and search entries use the repository name as
the title when the full name is null, empty or only whitespace. A nonblank
full name remains the title, and title writes still update the full name.

Title searches match that same displayed value for prefix, :exact and
:contains searches. Tests cover both resource types at global and owner
bases, including Unicode whitespace and names that differ from a nonblank
full name.

Work package WP3c of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…sible repository that holds it

When the first repository selected for a canonical URL cannot serve the
named release, FHIR resolution can select the lowest-id visible repository
with that canonical that holds it. A named unreleased version or HEAD
remains limited to the first eligible repository; unavailable holders are
not served.

This applies where repositories sharing the canonical determine resolution.
An explicit registry entry still decides, owner bases stay within their
owner, and requests without a version keep their default.
TerminologyCapabilities also lists the additional releases served by name
under the canonical, preserving the default.

Tests cover lookup, validation, expansion, translation, search and
capabilities across callers, bases and both registry settings, including
retired, deleted and unavailable versions. Query-count tests retain the
shared resolver's existing differences and account for the HEAD read used by
$resolveReference's view-access check.

Work package WP2b of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…ept rows report a holding release

$expand and ValueSet $validate-code can report a release for a member stored
as its concept's versioned-object row, which only HEAD holds. When no
visible live release holds the member's exact row, a release holding another
active version of that concept supplies the version.

A release holding the exact member takes precedence. Within each choice, the
expansion's explicit pin wins, then its evaluated pin, then the newest
release. Validation keeps the stored member's own display, so the coding
returned by expansion validates with that display.

Tests cover root and version rows, pins, named system versions, deleted
concept versions, unavailable releases and competing members with the same
code. They check expansion and validation together and preserve the query
count.

Work package WP10 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…h competing values and explain ConceptMap URI searches

A POSTed Parameters entry with more than one value[x], resource or part is
now a 400 OperationOutcome naming the parameter, including nested parts. A
primitive's _value[x] counts as its value[x], so its own extensions remain
valid while another value choice is refused.

CapabilityStatement documents that ConceptMap source-uri and target-uri
match the groups' systems, since ConceptMaps store no source or target value
set. Tests cover value order, nested and extension-only values, and the
search documentation at every base.

Work package WP11 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…t visible release that holds the concept

$expand and ValueSet $validate-code now report a versioned-object member in
the newest visible live release holding an active version of its concept,
regardless of expansion pins. Such a row carries the concept's newest
content. Members whose exact rows belong to releases still follow the
explicit pin, then the evaluated pin, then the newest.

Expansion reads the links of the relevant repositories' newest releases
together and looks up older releases only for unresolved members. The page
and used-codesystem use the same rules for selecting releases. used-
codesystem reads every visible member's id and release (count=0 included);
full concepts and names are read only for the page.

Tests cover pins, several repositories, unavailable or deleted versions,
older holding releases and members with no holding release. They verify
expansion and validation agree, query counts do not grow with member count,
and members held in the newest releases need no individual fallback.

Work package WP12 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…e for both the total and the page

$expand with a filter reads matching member ids once in id order. Their
number supplies total and a slice supplies the requested page, so selecting
the page no longer repeats the name filter. Unfiltered expansions keep their
existing queries.

Tests check a page larger than the matches, paging with an offset, count=0,
an offset beyond the matches and no matches. Each filtered request matches
names once, and an empty page reads no page concepts or names.

Work package WP13 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…eased versions; capped reads say they're partial

At an owner base, the instance-level operations (CodeSystem $lookup and
$validate-code, ValueSet $validate-code and $expand) resolve a named version
the read doesn't find as their type-level operations do, with the same
visibility rule: a named, public, unreleased version is served, and one the
caller may not see answers exactly as a missing one. Released versions the
read finds keep their path and statement count, and reads still return
released versions only.

A version the resolver returns carries its repository's canonical URL, from
the HEAD it already reads. A resolved version accepts that URL as a system or
url even when its own canonical URL differs; answers name the version's own
canonical URL when it has one, else its repository's, so a version named by
an OCL path, or served unreleased at an owner base, no longer answers with
the path.

A ValueSet read builds compose from at most 1,000 concept references in id
order, and a ConceptMap read builds group from at most 1,000 live mappings;
each reads one more to know when it was cut and then tags meta SUBSETTED,
keeping the tags meta already has. The FHIR answer to disabled anonymous
access carries only its detail text. Unused parameter serializers,
FhirParameters.names() and MultipleMatchesError are removed.

Work package WP14 of OpenConceptLab/ocl_online#451.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
…paths

A path is a FHIR path under /fhir/, or under an owner base (/orgs/{org}/,
/users/{user}/) when it names a FHIR resource type, metadata or $versions.
Other paths whose third segment is a resource type, such as a current user's
source named CodeSystem (/user/sources/CodeSystem/), keep their own error
bodies and statuses, and the API host serves them when FHIR has its own host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8
@paynejd

paynejd commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Review loop (Codex pass 1, simplification, Codex check)

  • Codex pass 1 (codex-cli 0.163.0, commit 77bd094): no P0 or P1. One P2, fixed in ebe370c4. The FHIR path check treated any path whose third segment is a FHIR resource type as FHIR, such as a current user's source with the id CodeSystem (/user/sources/CodeSystem/), so its errors were turned into OperationOutcomes. It now applies only to /fhir/ and the owner bases /orgs/… and /users/…, the only places FHIR is routed.
  • Simplification review: one test trim applied; two small cuts to already-tested code declined.
  • Codex check of 77bd094..ebe370c: clean, and the P2 is resolved.
  • Tests on the fix: core.fhir, core.middlewares, core.users and core.orgs pass, 843 tests; pylint 10.00.

The full findings are on the tracking ticket.

@paynejd

paynejd commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Follow-up stacked on this branch: #957 gives the FHIR host its own Swagger page, listing only the operations this PR serves. It goes to master after this one.

…page of the terminology operations (#957)

* OpenConceptLab/ocl_online#451 | The FHIR host serves its own Swagger page of the terminology operations

On the FHIR host, /swagger/ (and swagger.json, swagger.yaml) describes the
FHIR terminology service alone: search and read of CodeSystem, ValueSet and
ConceptMap, $lookup, $validate-code, $expand, $translate, metadata and
$versions, at /fhir and the owner bases. Each operation is listed once, tagged
by resource type, with its query parameters, a Parameters body for POST, and
OperationOutcome errors. Writes and the slash and no-slash duplicates are left
out, and Try it out calls the FHIR host itself.

When FHIR has its own host, the API's Swagger leaves the FHIR paths out (they
answer 404 there) and links to the FHIR page. When FHIR shares the host, the
API's Swagger lists the same curated operations instead of the generated ones.
The other API operations are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8

* OpenConceptLab/ocl_online#451 | FHIR Swagger: say which inputs a coding replaces

When a coding or codeableConcept is given, the operations don't read the
separate inputs it stands for: code and system for $lookup (version still
selects the code system version), code and display for CodeSystem
$validate-code (url and version still name the code system), code, system,
systemVersion and display for ValueSet $validate-code, and code, system and
version for $translate. Each operation's coding parameter now names them, and
each of those inputs says it isn't read alongside a coding. A GET coding token
carries no version or display; a POSTed valueCoding does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8

* OpenConceptLab/ocl_online#451 | FHIR Swagger: $lookup's system still names an instance alongside a coding

At the type level, $lookup takes the code system from a coding, so a separate
system has no effect; at an instance, a separate system must still name that
code system, as without a coding. Its description now says so instead of
saying it isn't read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@snyaggarwal snyaggarwal left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review focused on: non-FHIR behaviour changes, missing tests, module-function vs OOP design, duplication, complexity and feature envy. Line numbers are from the PR head.

1. Non-FHIR behaviour changes

  • ConceptMap import map types (core/concept_maps/serializers.py:46, also used by core/importers/importer.py): R4 equivalences now become OCL map types on import (wider→NARROWER-THAN, narrower→BROADER-THAN). This is the reverse mapping OpenConceptLab/ocl_issues#2924 asks for, so it's intended. Please link that ticket in the description so it closes on merge. The PR also maps unmatched/disjoint→NOT-SAME-AS, which goes beyond the ticket but looks reasonable.
  • Synchronous registry relink on every repo/version save. Source.save / Collection.save now call link_url_registry_entries() whenever canonical_url is dirty, which includes every new version row: an extra URLRegistry query plus lookup_entry() per create, inside the request or import, next to the existing async resolve_url_registry_entries task. Two mechanisms now maintain the same link (one sync, one async). The block is also duplicated between core/sources/models.py:702 and core/collections/models.py:170; it could live once on ConceptContainerModel.
  • Delete relinks entries in every namespace. relink_url_registry_entries (core/common/models.py:976) re-resolves every active entry with the deleted canonical URL, whatever its namespace, so a widely registered URL means N lookups inside the delete.
  • URLRegistry._set_repo (core/url_registry/models.py:140) now clears the link when nothing resolves, and reads no longer write. Intended and tested, but URL-registry API responses change for entries whose URL no longer resolves; worth calling out in the description.
  • $resolveReference gains ?namespace= (toggle-gated, tested), and resolve_expression_to_version now applies it to bare-string expressions too.
  • Looks fine: middleware reorder, FhirWSGIRequest (FHIR paths only), the BundleMetaSerializer.lastUpdated default fix (datetime.now() → datetime.now, a real bug fix), and the convert_ocl_uri_to_fhir_url segment-only rename.

2. Missing tests

  • FhirErrorMiddleware (core/middlewares/middlewares.py:278): no test that an exception on a non-FHIR path is still re-raised (test_answers_left_alone covers responses only).
  • FhirWSGIRequest: no test that a malformed Content-Type on a non-FHIR path behaves as before; every wsgi test uses a FHIR path.
  • Optional: an export → import round-trip test for map types, the round trip OpenConceptLab/ocl_issues#2924 describes. test_group_equivalence_is_the_map_type_relationship covers the mapping at the serializer level.
  • No query-count test for creating a source/version with a canonical URL, now that save does a synchronous registry lookup.
  • get_latest_released_version(exclude_head=True) is only tested indirectly.
  • convert_ocl_uri_to_fhir_url tests live in core/fhir/tests_capabilities.py:302 rather than with IdentifierSerializerTest in core/common/tests.py.

3. Module-level functions vs OOP

  • core/fhir/resolve.py is ~25 free functions threading (namespace, user) / (kwargs, user) through every call (is_shown, is_visible, has_release, may_view_private, owner_access, shown_q, viewable_rows, visible_versions, held_version, ...). That data clump wants to be one per-request access/resolver object.
  • Parameter parsing is scattered free functions taking parameters: text, require, coding_of (ops/concepts.py), required_text, boolean, coding_of (ops/translate.py:185,196), whole_number, flag (ops/expand.py:106,118). They belong as typed accessors on the existing FhirParameters (core/fhir/base.py:473).
  • Import-time module state: capabilities.STARTED = timezone.now() (capabilities.py:38) and the mutable LOGGED_DROPPED_ELEMENTS = set() (base.py:73).
  • REGISTRY_TOGGLE / ServedHead are declared mid-module (resolve.py:388).
  • base.py (601 lines) mixes content negotiation, XML conversion, parameter parsing and view base classes; worth splitting.

4. Duplication

  • Same parsers, different behaviour:
    • coding_of splits with rpartition('|') in ops/concepts.py:40 but partition('|') in ops/translate.py:196, so the same system|code token can parse differently in $lookup and $translate.
    • CodeableConcept: $translate silently takes the first coding; lookup/validate reject anything but exactly one.
    • flag strips whitespace and boolean doesn't; error messages differ.
  • Three too-costly factories: too_costly() in expand.py:101 and translate.py:152, search_too_costly() in search.py:597. One factory taking a message would do.
  • Access rules re-implemented: owner_access (resolve.py:320) and may_view_private (resolve.py:283) each re-derive ConceptContainerModel.has_parent_edit_access, differently within the same module; is_public (resolve.py:262) duplicates public_can_view. That's a second permission model that can drift from the main one.
  • A 1000 limit is defined five times (CONCEPT_LIMIT, MATCH_LIMIT, MAPPING_LIMIT, REFERENCE_LIMIT, expand.MAX_COUNT), while search.MAX_COUNT reuses the same name for 100. Suggest settings with distinct names.
  • Several OCL path ↔ FHIR URL converters: IdentifierSerializer.convert_*, resolve.to_ocl_path, resolve.ocl_path, translate.OCL_PATH + repository_paths.
  • CapabilityStatementView / VersionsView routes copy-pasted across orgs/urls.py, users/urls.py and fhir/urls.py.

5. Complex logic

  • resolve_reference_expression / resolve_repo (core/common/models.py:1542,1624) gain four FHIR-only parameters (repo_type, allow_head, heads, head_shown) with too-many-arguments,too-many-locals suppressed, and find_repo_by_canonical_url(repo_type=...) switches to different semantics under the flag. A resolution-policy object, or a FHIR subclass of the resolver, would keep the shared $resolveReference path free of FHIR rules.
  • shared_resolution's head_shown closure appends to an outer heads list to pass the picked HEAD back out (resolve.py:122).
  • resolve() (resolve.py:83) layers a pipe/path version conflict, a newest-public-release fallback, a holder's pick / held version, and cached rights copied between instances. Hard to follow.
  • with_side / shown_as in translate.py build **{f'{side}_…'} dynamic Q and annotate expressions that are hard to review.

6. Feature envy

  • repository_access, is_visible, has_release, may_view_private, latest_public_release reach into and write to model instances, stamping ad-hoc attributes (repository_public_access, repository_canonical_url, fhir_may_view_private, is_fqdn, resolution_url; plus fhir_concepts, concepts_over_limit in search.py, and core/common/models.py:1637). That logic belongs on ConceptContainerModel or a small wrapper class, not monkey-patched attributes.
  • ConceptMapGroupField.to_representation pulls request and view.kwargs from the serializer context and calls with_side, side_system, relationship and namespace_of from the ops/resolve modules.
  • Inverted layering: core/common/swagger.py, core/middlewares and the domain serializers now import from core.fhir, which imports them back. cyclic-import is disabled in .pylintrc, so pylint won't flag it.

Other

  • FhirOperationView.get_throttles (core/fhir/base.py:593) and CapabilityStatementView.get_throttles (core/fhir/views.py:19) copy a throttling pattern tracked as OpenConceptLab/ocl_online#532 debt. Please use a throttle that's the same for everyone (server protection) or wait for the #532 checkpoint.
  • Turning every 5xx on a FHIR path into a 422 hides real server errors from anything that counts 5xx; monitoring/alerting may need adjusting.

This branch has not been deployed

No deployments
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