Skip to content

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

Merged
snyaggarwal merged 3 commits into
ocl_online/451-fhir-svcm-prfrom
ocl_online/451-fhir-swagger
Oct 23, 2026
Merged

snyaggarwal merged 3 commits into
ocl_online/451-fhir-svcm-prfrom
ocl_online/451-fhir-swagger

Conversation

@paynejd

@paynejd paynejd commented Oct 23, 2026

Copy link
Copy Markdown
Member

A Swagger page for the FHIR terminology service

Follows #956 and is stacked on its branch; it gets retargeted to master once #956 merges. Tracking: OpenConceptLab/ocl_online#451.

Why: when FHIR has its own host (FHIR_SUBDOMAIN), the API host answers FHIR paths with a 404. Today's Swagger page still lists them, each up to four times (with and without a trailing slash, plus malformed forms such as /fhir/CodeSystem$lookup), with write methods FHIR doesn't serve and internal docstrings. "Try it out" on them fails.

What changes

  • The FHIR host serves its own Swagger page: /swagger/, /swagger.json and /swagger.yaml. It describes the FHIR terminology service alone, and "Try it out" calls that host.
    • What it lists: search and read of CodeSystem, ValueSet and ConceptMap; $lookup; CodeSystem and ValueSet $validate-code; $expand; $translate; metadata; and $versions. Each is listed at /fhir and at the owner bases /orgs/{org} and /users/{user}.
    • How it lists them: each operation appears once, with no trailing slash, tagged CodeSystem, ValueSet, ConceptMap or Capabilities. Each has its query parameters, a Parameters body for POST, and OperationOutcome errors.
    • What it leaves out: PUT, PATCH, DELETE, create by POST, and the duplicate path forms.
    • The parameter descriptions follow the code. For example, they say which separate inputs a coding replaces.
  • The API's Swagger page:
    • When FHIR has its own host, the API's page leaves the FHIR paths out and links to the FHIR page.
    • When FHIR shares the host, the API's page lists the same curated operations instead of the generated ones.
    • Every other API path and operation is unchanged; a test compares them with the generator's previous output.
  • FhirMiddleware: it opens only the three documentation paths on the FHIR host. The Swagger UI's static files are served ahead of it, as they are today, and the middleware order is unchanged.
  • Authorize: operations inherit the existing Token and Basic definitions, so Swagger's Authorize button works at owner bases, where a token shows private content.

Testing

  • Full suite on the final commit: 3,463 tests OK, pylint 10.00/10, coverage 96% (99% in core/fhir).
  • New tests in core/fhir/tests_swagger.py:
    • the exact (method, path) set, each resolved against the URL conf;
    • search parameters taken from the search parser;
    • operation parameters and POST bodies;
    • which inputs a coding replaces;
    • the FHIR host's paths, and the order of static files ahead of the host check;
    • the API page with FHIR on its own host and on the same host;
    • the unchanged non-FHIR operations.
  • Not changed here: python manage.py generate_swagger already fails on master, in a bulk-import view, so this PR leaves it alone. The HTTP schema endpoints work.

Review

Codex reviewed it in three passes. Passes 1 and 2 each found a description that over-promised what an input does alongside a coding, and both are fixed; pass 3 was clean. A simplification review proposed one cut, which was declined.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R8hw7w8753DDSCezrmrDA8

paynejd and others added 3 commits October 22, 2026 20:40
…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
…ng 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
…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
@paynejd

paynejd commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Review loop (Codex passes 1–3, simplification)

  • Codex pass 1 (codex-cli 0.163.0, commit 9d754f7): one P1, fixed in e06ba67. The schema implied that a separate display, systemVersion or version still applies when coding is given, which the operations don't do. Each operation's coding now names the inputs it replaces, and those inputs say they aren't read alongside a coding.
  • Codex pass 2 (commit e06ba67): one P1, fixed in 6bf3ea6. $lookup's system is still checked at an instance, so it now says that it must name that code system there.
  • Codex pass 3 (commit 6bf3ea6): clean, with both findings resolved.
  • Simplification review: one cut proposed (a deepcopy of parameter lists) and declined. It saves no lines, and the copy keeps the shared parameter objects unmutated.
  • Full gate on 6bf3ea6: 3,463 tests OK, pylint 10.00, coverage 96%.

The full findings are on the tracking ticket.

@snyaggarwal
snyaggarwal merged commit f88a62b into ocl_online/451-fhir-svcm-pr Oct 23, 2026
3 checks passed
@snyaggarwal
snyaggarwal deleted the ocl_online/451-fhir-swagger branch October 23, 2026 03:47
snyaggarwal pushed a commit that referenced this pull request Oct 26, 2026
…VCM transactions (#956)

* OpenConceptLab/ocl_online#451 | FHIR requests and errors use native FHIR 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

* OpenConceptLab/ocl_online#451 | Canonical lookups preserve registry entries 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

* OpenConceptLab/ocl_online#451 | FHIR search and read return the releases 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

* OpenConceptLab/ocl_online#451 | Missing FHIR operation inputs get consistent, 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

* OpenConceptLab/ocl_online#451 | FHIR lookup and validation answer for 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

* OpenConceptLab/ocl_online#451 | FHIR translation returns the requested 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

* OpenConceptLab/ocl_online#451 | FHIR expansion pages the requested value 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

* OpenConceptLab/ocl_online#451 | FHIR metadata describes the operations 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

* OpenConceptLab/ocl_online#451 | ConceptMap system searches use indexed 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

* OpenConceptLab/ocl_online#451 | CodeSystem and ValueSet titles fall back 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

* OpenConceptLab/ocl_online#451 | A named FHIR version resolves to a visible 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

* OpenConceptLab/ocl_online#451 | Value set members stored as root concept 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

* OpenConceptLab/ocl_online#451 | FHIR operations reject parameters with 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

* OpenConceptLab/ocl_online#451 | Root concept members report the newest 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

* OpenConceptLab/ocl_online#451 | Filtered expansions match members once 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

* OpenConceptLab/ocl_online#451 | Instance operations serve named unreleased 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

* OpenConceptLab/ocl_online#451 | Only /fhir/ and owner bases are FHIR 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

* OpenConceptLab/ocl_online#451 | The FHIR host serves its own Swagger 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>

* OpenConceptLab/ocl_online#451 | Tests: non-FHIR paths keep Django's behaviour, a released HEAD can be excluded

- FhirErrorMiddleware re-raises an exception on a path that isn't FHIR.
- The production request class reads a malformed Content-Type, or a non-text charset, off a FHIR path as Django does.
- get_latest_released_version(exclude_head=True) skips a released HEAD and retired versions.
- The convert_ocl_uri_to_fhir_url tests move to IdentifierSerializerTest.

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

* OpenConceptLab/ocl_online#451 | A server error answered as a 422 is still logged at ERROR and sent to Errbit

FHIR answers every error with an OperationOutcome and no 5xx (design.md's Protocol row). Each place that turns a
5xx into a 422 now goes through report_server_error: an exception's traceback is logged at ERROR, which Errbit
gets; a 5xx a view or the stack returned without one is logged at ERROR with a notice to Errbit of its own. A
response Django has already logged isn't reported twice. The log line, "FHIR answered a <status> server error as
a 422", is the same everywhere, so monitoring can count it.

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

* OpenConceptLab/ocl_online#451 | The FHIR API throttles every caller at one rate

Every FHIR view (search, read, operations, metadata) takes its throttle from FhirViewMixin: FhirMinuteThrottle, the
same rate for every caller whatever their groups, set by FHIR_THROTTLE_RATE (1000/minute by default) when throttling
is on. It protects the server; groups play no part in it.

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

* OpenConceptLab/ocl_online#451 | FHIR limits are settings with their own names; one too-costly error; no import-time state

- The page sizes, read limits and time bound are settings: FHIR_SEARCH_MAX_COUNT (100), FHIR_SEARCH_TIMEOUT_MS
  (5000), FHIR_CODE_SYSTEM_CONCEPT_LIMIT, FHIR_VALUE_SET_REFERENCE_LIMIT, FHIR_CONCEPT_MAP_MAPPING_LIMIT,
  FHIR_EXPAND_MAX_COUNT and FHIR_TRANSLATE_MATCH_LIMIT (1000 each), read when used.
- An answer that would take too long raises one TooCostlyError with its own message (core.fhir.constants);
  within_search_timeout takes the message, and the mappings index raises the search's.
- The CapabilityStatement's date is computed when first asked, not at import, and the XML's log-once of what it
  leaves out is a cached function rather than a module-level set.
- The URL registry toggle's name is a constant (core.url_registry.constants), and ServedHead sits with resolve.py's
  other definitions.

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

* OpenConceptLab/ocl_online#451 | One reader per input type: typed accessors on FhirParameters

FhirParameters reads every operation input: text, required_text, boolean, whole_number, coding and
codeable_concept_coding, and Coding carries path and require. The per-module parsers are gone, and with them
the differences between operations:
- A GET's system|code token splits at its first |, as a token search value does (search's _count and token
  parameters read the same way). $lookup and $validate-code split at the last one before.
- A codeableConcept is read with one coding everywhere. $translate took the first of several and left the
  others unread; now, as in $lookup and $validate-code, several are a 400 not-supported, and none is a 400
  invalid.
- Booleans and whole numbers take no padding (" true", "5 "), as no FHIR boolean or integer has any; $expand's
  stripped it, $translate's didn't. The error messages are the same everywhere.
- An empty _count is the default page size, as an empty search value is no criterion.

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

* OpenConceptLab/ocl_online#451 | FHIR's access checks use the models' own rules

- BaseModel.is_public_access is the rule public_can_view reads, and PUBLIC_ACCESS_VALUES its stored values for
  querysets; FHIR uses both rather than a copy of its own.
- Organization.has_edit_access and UserProfile.has_edit_access say who may edit an owner's repositories, and so
  view its private ones: staff, an org's members, the user. FHIR's owner_access asks the owner, as a repository's
  has_parent_edit_access does.

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

* OpenConceptLab/ocl_online#451 | One task keeps the URL registry's links, queued once a change commits

A repository row created with, or given, a canonical URL no longer resolves registry entries in the request. Its
save, in Source and Collection alike, calls ConceptContainerModel.resolve_url_registry_entries_later: one query
asks whether any active entry is linked to the row or names its URL in its namespace, and only then is the
existing resolve_url_registry_entries task queued, on commit, so it reads the committed row. The task re-resolves
those entries (url_registry_entries_to_resolve), and skips a row deleted meanwhile.

A delete re-resolves only its own namespace's entries naming a deleted row's canonical URL, besides those that
were linked to the rows: an entry resolves in its own namespace, so no other could have pointed at them.

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

* OpenConceptLab/ocl_online#451 | One set of OCL path and FHIR path helpers; one place for a base's metadata routes

- core.common.fhir_helpers holds the conversions between an OCL repository path and its FHIR path: to_ocl_path (by
  the path's own resource type), to_fhir_path, and repository_prefixes (a source's path in any of its forms as the
  OCL prefixes to match). IdentifierSerializer's converters, the resolver, $translate and ConceptMap search use
  them instead of their own. convert_fhir_url_to_ocl_uri now renames only the resource type's segment, so an owner
  or repository named CodeSystem, ValueSet or ConceptMap keeps its name.
- core.fhir.urls.metadata_routes makes a base's metadata and $versions routes, for /fhir/ and both owner bases.

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

* OpenConceptLab/ocl_online#451 | Test: a ConceptMap read and written back keeps its map types

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

* OpenConceptLab/ocl_online#451 | Pylint: line lengths, a shadowed name, an import made once

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

* OpenConceptLab/ocl_online#451 | Review fixes: FHIR's remaining-requests header counts FHIR's throttle; the queued task is the saved row's

- On a FHIR path, ThrottleHeadersMiddleware reports X-LimitRemaining-Minute from FhirMinuteThrottle, the counter FHIR
  views throttle by, and no daily header; it used the plan throttles' counters, which FHIR views no longer use.
- The registry task queued on commit takes the saved row's id and type, so an instance reused before the commit (as
  a new version is made from one) doesn't queue another row's.
- The test that a response Django already logged isn't reported again patches the middleware's own reference.

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

* OpenConceptLab/ocl_online#451 | Simplification cuts: one log-once function, to_ocl_path reused, a dead property and a repeated test gone

- log_dropped is the cached function itself.
- convert_fhir_url_to_ocl_uri is to_ocl_path with a kind.
- ConceptContainerModel.active_url_registry_entries had no caller left.
- The Errbit-notice unit test repeated the view test; the delete-scope test uses the mock's own call record.

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

* OpenConceptLab/ocl_online#451 | core/fhir/base.py split by concern; core.common no longer imports core.fhir

- base.py's parts are modules of their own: constants (media types, FHIR versions, XML names), utils
  (is_fhir_path, fhir_base_url, namespace_of), xml (JSON and XML conversion), negotiation (media types, versioning,
  parsers, renderers, the middleware's responses), parameters (FhirParameters, Coding) and mixins (FhirViewMixin,
  FhirOperationView, and the search and read mixins from search.py).
- The serializer fields and mixin shared by the CodeSystem, ValueSet and ConceptMap serializers move from search.py
  to core/fhir/serializers.py.
- core/common/swagger.py keeps the generic generator; the FHIR-aware one is core.fhir.swagger.ApiSchemaGenerator,
  which core/urls.py uses, so core.common no longer imports core.fhir.

Behaviour is unchanged.

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

* OpenConceptLab/ocl_online#451 | One FhirResolver per request; the shared resolver takes a resolution policy instead of FHIR's flags

- ConceptContainerModel.resolve_reference_expression and resolve_repo take a `resolution`, a ReferenceResolution:
  which repository each step finds (a canonical match, an owner's repository, a registry entry's) and the version it
  answers with. Its defaults are $resolveReference's rules, so the shared path holds no FHIR rule, and the repo_type,
  allow_head, heads and head_shown parameters are gone, as are repo_type on find_repo_by_canonical_url,
  Repository.get and URLRegistry.find_repo.
- core.fhir.resolve.FhirResolution is FHIR's policy: one resource type, a canonical URL on a repository's active
  HEAD, no HEAD by default, only the HEADs the base shows, and a HEAD the caller isn't shown found before any version
  is read. It records the HEAD it found, where a closure collected it before.
- core.fhir.resolve.FhirResolver is one request's base and caller: resolve, is_shown, is_visible, has_release,
  shown_q, viewable_rows, visible_versions and served_repositories are its methods rather than functions passed
  (namespace, user) or (kwargs, user). It asks about the caller's rights over an owner's private repositories once
  per owner, where the answer was stamped on each version and copied between them. FhirViewMixin builds one per
  request, and the operations, search, read, metadata and the ConceptMap serializer take it.

Behaviour is unchanged.

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

* OpenConceptLab/ocl_online#451 | A version's repository access and canonical URL are declared on the model, not stamped on it

- ConceptContainerModel declares repository_public_access and repository_canonical_url as cached properties (the
  repository's active HEAD's, read once through repository_head), which a query may annotate and the resolver may
  fill in, and is_fqdn and resolution_url as class attributes. FHIR's repository_access helper and its getattr
  fallbacks are gone.
- A CodeSystem read's listed concepts are kept in the serializer's context, not on the version (fhir_concepts).

Behaviour is unchanged.

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

* OpenConceptLab/ocl_online#451 | A mapping's side is a MappingSide; ConceptMap groups are built by core.fhir.ops.translate

- MappingSide names a mapping's from or to fields once (source_id, source_url, concept_code, the shown and
  canonical annotations) and holds the queries and answers that read them: of_version, shown_as, with_shown,
  fields and system. FROM and TO replace the f'{side}_...' strings $translate built its queries from.
- concept_map_groups (core.fhir.ops.translate) builds a ConceptMap version's groups, version-neutral, with each
  target's relationship; r4.concept_map_groups writes them in R4. ConceptMapGroupField only asks for them, where it
  read the request, the view's kwargs and four of translate's helpers itself.

Behaviour is unchanged.

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

* OpenConceptLab/ocl_online#451 | Test: the resolver asks about an owner's private repositories once

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

* OpenConceptLab/ocl_online#451 | Test: a release without a canonical URL of its own shows its repository's

CanonicalUrlField now reads the version's declared repository_canonical_url wherever the serializer is used, so a
directly serialized release (a write's response) applies the rule reads and searches already applied.

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

* OpenConceptLab/ocl_online#451 | Simplification cuts: forwarding wrappers gone, fhir_base_url built from namespace_of

Translation.resolve, Search.viewable_sources and FhirViewMixin.get_fhir_base_url only forwarded to the resolver or
fhir_base_url; their callers call those directly. fhir_base_url builds the base from namespace_of.

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

* OpenConceptLab/ocl_online#451 | Comment: finish the sentence above is_fqdn

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

* OpenConceptLab/ocl_online#451 | Test: releases without a canonical URL of their own add no query to a search

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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