Introduction
FerroTERM is a pure-Rust FHIR terminology server for SNOMED CT, LOINC, and other clinical code systems, SNOMED CT first. It serves the HL7 FHIR terminology API from a single binary, backed by a memory-mapped index built once per code system release. There is no JVM, no Elasticsearch, and no external database.
Note
The name: Ferro for the Rust family FerroTERM shares with FerroEHR, TERM for terminology. The site is https://ferroterm.eu.
Status: early design
This repository is in discovery. The team is scoping the design before writing
product code, so this book describes intended behaviour, marked as planned where
the server does not exist yet. Read a sentence in the future tense (“the server
returns”, “you run”) as a design commitment, not a claim that you can run it
today. The design authority, with citations, is
docs/architecture.md
in the repository.
Who each part of this book is for
The book is organized by what you want to do.
- Evaluate is for anyone deciding whether FerroTERM fits. It covers what the server is, why it exists, the architecture at a glance, and how it compares to Snowstorm, Ontoserver, and Hermes.
- Operate is for operators and deployers. It covers installing and running the server, configuration, loading a licensed SNOMED CT edition, hardware sizing, and verifying a release before you run it.
- Integrate is for API consumers. It covers the FHIR terminology operations, implicit value sets and ECL, the supported FHIR versions, and worked request and response examples.
- Contribute is for contributors. It covers building and testing, the FHIR code generation model, and where the deeper design lives.
Two things that are always true
You bring your own code system content. The software is open source under the MIT license. SNOMED CT is licensed by SNOMED International, LOINC by the Regenstrief Institute, and this repository ships no code system content. You load a licensed release, and the server serves it. See Loading a SNOMED CT edition.
What FerroTERM is
Architecture at a glance
This page is a short tour of the design. The full design authority, with
citations to the terminology-server and graph-reachability literature, is
docs/architecture.md
in the repository.
- SNOMED CT is two things at once
- Index-materialized graph, not a graph database
- Persistence is a pure-Rust embedded engine
- FHIR support is machine-generated
- The SNOMED semantics are hand-written
- Text search is a separate index
SNOMED CT is two things at once
SNOMED CT is a formal ontology in the OWL 2 EL profile, and it is a polyhierarchical graph. The is-a hierarchy a client queries is the inferred hierarchy, produced by a description-logic reasoner, not the stated one.
That fact splits the system into two problems that have different engineering answers:
graph TD
R["SNOMED CT RF2 release"] --> OFF["Offline: classification, once per release"]
OFF --> IDX["Materialized index: CSR adjacency + roaring closure + text index"]
IDX --> ON["Online: serving, from precomputed structures"]
ON --> API["FHIR terminology API (R4 / R4B / R5 / R6)"]
- Offline: classification, once per release. The hierarchy FerroTERM serves is a reasoner output. FerroTERM computes the transitive closure from the shipped inferred relationship file once per release and persists it. It never re-derives inference at query time.
- Online: serving, from precomputed structures. Each operation is answered from the structure shaped for it: a subsumption test is a bitmap membership test, an ECL descendant set is a bitmap returned directly, and a point read is a lookup in a columnar store.
Index-materialized graph, not a graph database
SNOMED is stored as a graph, natively: integer-keyed compressed sparse-row (CSR) adjacency arrays, plus roaring bitmaps holding the transitive closure. This is a graph model with an index-materialized store.
A general-purpose graph database is rejected on the evidence. No production terminology server uses one: Ontoserver is Postgres plus Lucene, Snowstorm is Elasticsearch, and Hermes is a memory-mapped store plus Lucene. Graph databases degrade on the deep multi-hop neighborhoods that ECL descendant expansion produces, which is exactly the query a terminology server is stressed by. At SNOMED’s size the full transitive closure fits in memory and gives an O(1) subsumption test with no traversal fallback.
Persistence is a pure-Rust embedded engine
The built structures persist in redb, a pure-Rust, memory-mapped, ACID
embedded key-value engine. The build tool writes these artifacts once per
edition, and the server opens them read-only. redb is the storage format. At
startup the server loads the transitive closure into a resident, ordinal-indexed
structure so a subsumption test is a membership check against a resident bitmap,
which is how the design reaches microsecond-scale subsumption in pure Rust.
FHIR support is machine-generated
HL7 publishes the whole type system and every operation as machine-readable
StructureDefinition and OperationDefinition resources, in versioned packages.
FerroTERM vendors and pins those packages and generates per-version Rust modules from
them, so R5’s extra $expand parameters appear where the spec has them and are
absent where it does not. Every generated file is marked // @generated, a drift
check regenerates it in CI, and one runtime wrapper answers R4, R4B, R5, and R6
callers at once. This mirrors how the sibling project FerroEHR generates its
openEHR model.
The SNOMED semantics are hand-written
RF2 loading, the materialized ontology, ECL evaluation, and $expand paging are
the product, and no existing Rust project provides them. ECL is the hard part:
every expression constraint returns a set of codes, so the evaluator compiles ECL
to set algebra over the descendant bitmaps and the attribute adjacency, using
bitmap AND and OR rather than pointer-chasing. Correctness is measured against
Snowstorm as the reference server.
Text search is a separate index
Description search is its own concern, held in its own index. A word inverted
index maps each word to a roaring-bitmap postings list, and an fst dictionary
answers per-word prefix and fuzzy lookup. A query intersects the matching words’
postings, applies the language-reference-set and active-status filters, and sorts
by matched-term length.
How it compares
FerroTERM serves the same FHIR terminology API as the established servers. The difference is the runtime footprint and the stack it needs to operate. This page sets FerroTERM beside Snowstorm, Ontoserver, and Hermes.
Note
The numbers for the other servers come from their own documentation and published work, cited in
docs/architecture.md. FerroTERM’s own figures are design targets, since the server is in design.
The servers
- Snowstorm (SNOMED International): Java on Elasticsearch. The reference server and FerroTERM’s correctness oracle. It is full-featured, and a full International edition deployment wants 16 to 32 GB of RAM plus a search cluster.
- Snowstorm Lite (SNOMED International): drops Elasticsearch for a single Lucene index and runs the full International edition in about 500 MB. It is the closest point of comparison for footprint.
- Ontoserver (CSIRO): Postgres plus Lucene. A production FHIR terminology server, index-materialized rather than a graph database.
- Hermes (Mark Wardle): Clojure, a memory-mapped store plus Lucene. It serves subsumption in tens of microseconds from a materialized structure, which is the design point FerroTERM follows in pure Rust.
Side by side
| Server | Language / runtime | Store | Full-edition memory | Deploys as |
|---|---|---|---|---|
| Snowstorm | Java (JVM) | Elasticsearch | 16 to 32 GB | JVM plus a search cluster |
| Snowstorm Lite | Java (JVM) | Lucene | about 500 MB | JVM service |
| Ontoserver | Java (JVM) | Postgres plus Lucene | server plus database | JVM plus Postgres |
| Hermes | Clojure (JVM) | memory-mapped store plus Lucene | modest | JVM service |
| FerroTERM (planned) | Rust | redb, memory-mapped | a few hundred MB target | single static binary |
What FerroTERM trades
FerroTERM aims for a small footprint and a single binary, and it starts as a read-oriented server over a loaded edition. The mature servers carry features that come later in FerroTERM’s build order, such as full MRCM validation of post-coordinated expressions and closure maintenance. See what is not built yet in What FerroTERM is.
Every production server in this list converges on a materialized index rather than live graph traversal. FerroTERM takes the same shape and implements it in pure Rust with a machine-generated FHIR layer across four versions, which no existing Rust project provides.
Install and run
This page describes how you install and start the server. The distribution is in design, so treat the steps as the planned shape.
Warning
FerroTERM has no released binary or image yet. The commands below are the intended interface, not something you can run today. Follow the repository for release announcements.
Planned distribution
FerroTERM ships as a single static binary and as a container image. Both carry the whole server: there is no JVM to install, no Elasticsearch to run, and no external database to provision. You provide a built SNOMED CT index (see Loading a SNOMED CT edition) and point the server at it.
Run the binary (planned)
Download the binary for your platform and its attestation, verify the attestation (see Verifying releases), then run it:
$ ferroterm --index /path/to/ferroterm-index
The server starts, opens the index read-only, and listens for FHIR requests. You configure the listen address, the index path, and the served FHIR versions through the settings described in Configuration.
Run the container (planned)
$ docker run --rm -p 8080:8080 \
-v /path/to/ferroterm-index:/data/index:ro \
ghcr.io/rubentalstra/FerroTERM:<tag> \
--index /data/index
Mount the index read-only. The server needs no writable volume for serving, because the index is built offline by a separate tool.
Check that it is serving
Once the server is up, a FHIR client can call the terminology operations. The CapabilityStatement tells a client which operations and FHIR versions this deployment serves:
$ curl http://localhost:8080/metadata
See The FHIR terminology API for the operations themselves.
Configuration
This page describes how you configure the server. The configuration surface is in design, so the setting names below are the planned shape and may change before the first release.
Note
No configuration schema is frozen yet. This page lists the settings the design calls for. Check the release notes for the exact names when a build ships.
What you configure
A deployment configures a small set of things:
- The index path. Where the built SNOMED CT index lives. The server opens it read-only at startup.
- The listen address and port. Where the server accepts FHIR requests.
- The served FHIR versions. Which of R4, R4B, R5, and R6 this deployment answers. See FHIR versions.
- Expansion limits. The default and maximum page size for
ValueSet/$expand, so a broad expansion cannot return an unbounded result in one response. See Implicit value sets and ECL. - Logging. The log level and format for the
tracingoutput.
How you set it
The design follows the common Rust server pattern: command-line flags for the essentials, environment variables for a container, and a file for the rest, with flags taking precedence over the environment and the environment over the file. The exact grammar is settled with the first build.
What you do not configure
You do not configure a database connection, a search cluster, or a JVM heap. FerroTERM has none of these. The one input the server needs is the built index, and you build that offline with the release-loading tool described in Loading a SNOMED CT edition.
Loading a SNOMED CT edition
FerroTERM serves the SNOMED CT edition you load. This page explains the licensing rules you must satisfy first, and the planned build step that turns an RF2 release into the index the server reads.
You bring your own content
Warning
This repository ships no SNOMED CT content, and no build of FerroTERM contains any. SNOMED CT is licensed separately by SNOMED International. You must hold a valid SNOMED CT licence for the edition you load.
The split is firm, and you must keep the two apart:
- The software in this repository is open source under the MIT license.
- SNOMED CT content is the property of SNOMED International and is not distributed here.
A SNOMED CT licence is free within member countries, the Netherlands among them, and available under the affiliate licence elsewhere. You obtain the RF2 release for your edition from your national release centre or from SNOMED International, under your licence, and you load it into FerroTERM yourself.
The offline build (planned)
The running server never parses RF2 and never classifies the ontology. A separate tool turns an RF2 release into the memory-mapped index once per edition, and the server opens that index read-only.
graph LR
RF2["Licensed RF2 release<br/>(you provide)"] --> BUILD["ferroterm-build<br/>(offline)"]
BUILD --> IDX["ferroterm-index<br/>(graph + store + text)"]
IDX --> SRV["ferroterm<br/>(read-only)"]
The planned command takes the unpacked RF2 release and writes the index:
$ ferroterm-build --rf2 /path/to/SnomedCT_Release --out /path/to/ferroterm-index
The build computes the transitive closure from the shipped inferred relationship file, builds the CSR adjacency and the roaring closure bitmaps, writes the columnar concept and description store, and builds the text index. It runs once per release. When a new edition arrives, you rebuild the index and restart the server against it.
Note
The build tool is in design. The command name and flags above are the intended shape.
Test content
FerroTERM’s own tests use shaped, synthetic content only. They never contain real SNOMED CT concepts extracted from a release, which keeps the licence line clean in the repository itself.
Hardware sizing
FerroTERM targets a small footprint so you can run the full International edition on ordinary hardware. This page gives the design targets and where the memory goes.
Note
These are design targets grounded in the reference servers and in roaring-bitmap compression behaviour, not measurements of a shipped build. See
docs/architecture.mdfor the reasoning.
The target
The design target is to serve the full SNOMED CT International edition in a few hundred megabytes of resident memory, so the server fits on a 2 to 4 GB box with room to spare. For comparison, SNOMED International’s Snowstorm Lite runs the same edition in about 500 MB, and the Java-plus-Elasticsearch Snowstorm wants 16 to 32 GB.
Where the memory goes
At startup the server loads the reachability closure into resident memory and leaves the rest on the memory-mapped index. The expected split:
| Structure | Resident | Rough size |
|---|---|---|
| Transitive closure (ancestor and descendant bitmaps) | yes | 100 to 300 MB |
| CSR adjacency (is-a and per-attribute) | yes | tens of MB |
fst text dictionary | yes | tens of MB |
| Columnar concept and description store | memory-mapped, paged on demand | on disk |
Both directions of the closure are stored on purpose: subsumption needs one direction, and ECL returns each set directly, so keeping both trades roughly 2x the closure space for direct answers. Roaring compresses SNOMED-shaped sets heavily, which is why the resident closure lands in the hundreds of megabytes rather than the gigabytes a naive bitset would need.
Disk and CPU
The index on disk is the memory-mapped redb file the build tool writes. Size it
for the edition you load. The offline build is the CPU-heavy step, and it runs
once per release in the build tool, not on the server. Serving is a point read or
a bitmap operation, so a modest CPU handles it. A heavy $expand that
materializes a large set, and any cold read that page-faults from disk, run on a
blocking pool so they never stall the request runtime.
Verifying releases
Every FerroTERM release artifact carries a signed provenance attestation. Verify it before you run a binary, so you know the artifact was built by the project’s own release workflow and not tampered with.
Note
The release pipeline is stood up and activates on the first tag. Until a release exists, the recipe below is the interface you will use, not a check you can run yet. The design and rationale are in
docs/ci-cd.md.
What a release carries
Each release artifact ships with:
- An embedded dependency list, written into the binary’s
.dep-v0section bycargo auditable. - A CycloneDX SBOM.
- A
.sha256checksum. - A keyless Sigstore provenance attestation and a signed SBOM, both bound to the artifact digest and signed by the release workflow’s own identity.
The signing is keyless through Sigstore, so there is no long-lived key to manage or leak.
Verify the provenance
Use the GitHub CLI to verify an artifact against the workflow that is allowed to
sign it. Replace <tag> and <target> with the release you downloaded:
$ gh attestation verify ferroterm-<tag>-<target>.tar.gz -R rubentalstra/FerroTERM \
--signer-workflow rubentalstra/FerroTERM/.github/workflows/release-build.yml
The --signer-workflow flag is the point of the check. It requires that the
attestation was produced by that exact reusable workflow in this repository, so a
signature from any other workflow or repository fails. FerroTERM builds its releases
in a reusable workflow to reach SLSA Build Level 3, where the signing identity is
not reachable by user build steps.
Check the checksum
Confirm the download matches its published checksum:
$ sha256sum -c ferroterm-<tag>-<target>.tar.gz.sha256
Run both checks. The checksum tells you the bytes are intact, and the attestation tells you where the bytes came from.
The FHIR terminology API
FerroTERM speaks the HL7 FHIR terminology API. If your client already talks to a FHIR terminology server, it talks to FerroTERM. This page lists the operations and what each one answers.
Note
The server is in design. The operations below are the terminology surface FerroTERM implements, and their FHIR shapes are fixed by the specification, but you cannot call a running server yet. Requests and responses are marked as planned on the Worked examples page.
The operations
| Operation | Answers | Structure that serves it |
|---|---|---|
CodeSystem/$lookup | a concept’s properties and designations | columnar store plus adjacency |
CodeSystem/$subsumes | does concept A subsume concept B | roaring-bitmap membership |
CodeSystem/$validate-code | is this a valid code and display | columnar store |
ValueSet/$validate-code | is this code a member of the value set | store plus expansion |
ValueSet/$expand | the members of a value set, ECL-driven | precomputed bitmaps and set algebra |
ConceptMap/$translate | the targets a code maps to | map-refset lookup |
The FHIR specification defines the parameters and the response shape for each operation. FerroTERM follows the specification for the FHIR version in the request.
How a request is shaped
Every operation is a FHIR operation invocation, called with GET for simple
parameters or POST with a Parameters resource for complex input. Results come
back as a Parameters resource, or the operation’s defined resource for
$expand (an expanded ValueSet). Errors come back as an OperationOutcome
with a diagnostic issue.
For example, $lookup takes a system and a code and returns the concept’s
name, its designations, and its requested properties. $subsumes takes two codes
and a system and returns whether the first subsumes the second, the reverse, is
equivalent, or is unrelated.
SNOMED CT as the code system
For SNOMED CT the system is the SNOMED CT URI, http://snomed.info/sct. An
edition and version are addressed with the SNOMED CT URI standard’s version
form. The implicit value sets and ECL that drive $expand are covered on
Implicit value sets and ECL.
Metadata
A client discovers what a deployment serves from its CapabilityStatement at
/metadata, and from the TerminologyCapabilities resource. These report the FHIR
versions this server answers and the terminology operations it supports.
Implicit value sets and ECL
SNOMED CT defines implicit value sets: value sets you name by a URL convention rather than by storing a ValueSet resource. FerroTERM expands them with SNOMED’s Expression Constraint Language (ECL). This page explains the URL convention and how ECL drives an expansion.
Note
ECL is the hard part of the server and its main risk, so it is built and tested as its own layer against the published ECL grammar before the value-set surface depends on it. Correctness is measured against Snowstorm.
- The implicit value set convention
- What ECL expresses
- How FerroTERM evaluates it
- Paging a large expansion
The implicit value set convention
SNOMED CT on FHIR lets you name a value set by an ECL expression in the value
set’s URL, using the fhir_vs convention:
http://snomed.info/sct?fhir_vs=ecl/<expression>
You pass that URL as the value set to ValueSet/$expand, and the server expands
the ECL expression against the loaded edition. The plain ?fhir_vs (with no
ecl/) names the implicit value set of all SNOMED CT concepts, and ?fhir_vs=isa/<code>
names the value set of a concept and its descendants.
What ECL expresses
Every ECL expression returns a set of concepts. The core operators over the is-a hierarchy:
| ECL | Meaning |
|---|---|
<< X | X and all its descendants |
< X | the descendants of X, not X itself |
>> X | X and all its ancestors |
> X | the ancestors of X, not X itself |
Refinement narrows a set by attribute values, for example < 404684003 : 363698007 = << 39057004
reads as the descendants of one concept that have a given attribute pointing into
a given subtree. ECL also supports conjunction, disjunction, exclusion, attribute
groups, and cardinality.
How FerroTERM evaluates it
FerroTERM compiles an ECL expression to set algebra over the precomputed structures.
A << or >> set is a precomputed bitmap returned directly, a refinement is a
bitmap intersection or union over the per-attribute adjacency, and conjunction
and disjunction are bitmap AND and OR. There is no live graph traversal on this
path, which is what keeps a descendant expansion of a high-level concept fast.
Paging a large expansion
A broad expansion can name a large set, so $expand pages its results with the
FHIR count and offset parameters, bounded by the server’s configured maximum
page size (see Configuration). Request a page at a
time rather than an unbounded expansion.
FHIR versions
FerroTERM serves the FHIR terminology API across four versions from one running server: R4, R4B, R5, and R6. A client picks the version, and the server answers in that version’s shapes.
Why four versions from one server
HL7 publishes the whole type system and every operation as machine-readable resources, in versioned packages. FerroTERM vendors and pins those packages and generates per-version Rust modules from them, so each version’s operation surface is correct by construction. An operation parameter that R5 adds appears in the R5 module and is absent from R4B, because the generator emits what each package declares. A runtime wrapper routes a request to the module for its version, so one server answers all four callers at once.
Version status
| Version | Package | Status in FerroTERM |
|---|---|---|
| R4B | hl7.fhir.r4b.core 4.3.0 | first generation implemented |
| R5 | hl7.fhir.r5.core 5.0.0 | follows R4B |
| R4 | hl7.fhir.r4.core 4.0.1 | follows R4B |
| R6 | hl7.fhir.r6.core 6.0.0-ballot | ballot-tracking, follows the others |
Note
R4B is the first version implemented. It is the current stable release of the R4 line and a near-superset of R4, so an R4B-first build already serves the R4-family terminology surface. R5, R4, and R6 follow as further generations. R6 tracks the ballot, with publication expected around late 2026.
What this means for you
Send your request in the FHIR version your client uses. The operation names,
parameters, and response shapes are the ones the FHIR specification defines for
that version, so you do not adapt your client to FerroTERM. Implementation starts
with R4B, and the other versions become available as they are implemented. The
CapabilityStatement at /metadata reports which versions a given deployment
serves.
Worked examples
This page shows request and response shapes for the common operations. The FHIR shapes are fixed by the specification. The values are illustrative.
Warning
These examples are planned. FerroTERM has no running server yet, so you cannot send these requests today. The request and response shapes follow the FHIR terminology specification, and the concrete codes and displays are placeholders.
- $lookup a concept
- $subsumes two concepts
- $validate-code against a value set
- $expand an ECL value set
- An error
$lookup a concept
Read a concept’s display and properties:
GET /CodeSystem/$lookup?system=http://snomed.info/sct&code=73211009
A Parameters response carries the name, the display, and the requested
properties:
{
"resourceType": "Parameters",
"parameter": [
{ "name": "name", "valueString": "SNOMED CT" },
{ "name": "display", "valueString": "Diabetes mellitus (disorder)" }
]
}
$subsumes two concepts
Test whether one concept subsumes another:
GET /CodeSystem/$subsumes?system=http://snomed.info/sct&codeA=73211009&codeB=44054006
The response reports the relationship, one of subsumes, subsumed-by,
equivalent, or not-subsumed:
{
"resourceType": "Parameters",
"parameter": [
{ "name": "outcome", "valueCode": "subsumes" }
]
}
$validate-code against a value set
Check that a code is a member of a value set:
GET /ValueSet/$validate-code?url=http://snomed.info/sct?fhir_vs=isa/73211009&system=http://snomed.info/sct&code=44054006
{
"resourceType": "Parameters",
"parameter": [
{ "name": "result", "valueBoolean": true }
]
}
$expand an ECL value set
Expand an implicit value set named by ECL, one page at a time:
GET /ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=ecl/<<73211009&count=20&offset=0
The response is an expanded ValueSet whose expansion.contains holds the page
of members, with total reporting the full set size:
{
"resourceType": "ValueSet",
"expansion": {
"total": 137,
"offset": 0,
"contains": [
{ "system": "http://snomed.info/sct", "code": "73211009", "display": "Diabetes mellitus (disorder)" }
]
}
}
An error
An invalid or unknown input returns an OperationOutcome:
{
"resourceType": "OperationOutcome",
"issue": [
{ "severity": "error", "code": "code-invalid", "diagnostics": "Unknown code '00000' in system 'http://snomed.info/sct'." }
]
}
See The FHIR terminology API for the operation list and Implicit value sets and ECL for the ECL URL convention.
Build and test
This page is a short pointer for contributors. The deeper design and the working discipline live in the repository, and this book keeps to what you need to get building.
- Read these first
- The local gates
- The two layers
- No SNOMED CT content in the repository
- Correctness oracle
Read these first
docs/architecture.md: the design authority, with citations to the terminology-server and graph-reachability literature.CONTRIBUTING.md: the contribution rules, branches, commit signing, and pull-request checklist.
The local gates
Once the Cargo workspace exists, the local gates mirror CI exactly. Run them before you open a pull request:
$ cargo fmt --all --check
$ cargo clippy --workspace --all-targets --all-features -- -D warnings
$ cargo nextest run --workspace --locked
$ cargo test --doc --workspace --locked
$ RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features --document-private-items
$ cargo deny check
Scope any command to a crate with -p <crate> while you iterate, then run the
full --workspace gates before you push. Every cargo invocation uses --locked,
and Cargo.lock is committed.
The workflow lanes that check shell and YAML (actionlint, zizmor,
shellcheck) run on every change, including before any Rust exists. If you edit a
shell script, keep it clean at shellcheck --severity=style.
The two layers
crates/ferroterm-fhiris generated from the vendored FHIR specs. Never hand-edit a// @generatedfile. See The codegen model.- The SNOMED engine and the server are hand-written, idiomatic Rust, with the FHIR and SNOMED specifications as the authority.
No SNOMED CT content in the repository
Tests use shaped, synthetic content only. Never commit real SNOMED CT concepts from a release, and never commit an RF2 file. See Loading a SNOMED CT edition for the licensing rule.
Correctness oracle
Terminology answers are checked against Snowstorm as the reference server over the same edition, and the ECL evaluator is tested against the published ECL grammar as its own layer before the value-set surface depends on it.
The codegen model
The FHIR layer of FerroTERM is generated, not hand-written. This page explains the model at the level a contributor needs. The mechanics live in the generator crate and its rules in the repository.
Why generate the FHIR layer
HL7 publishes the whole FHIR type system and every operation as machine-readable
StructureDefinition and OperationDefinition resources, in versioned packages.
Rather than transcribe those by hand for four versions, FerroTERM vendors the
packages and generates per-version Rust modules from them. Each version’s
operation surface is then correct by construction: a parameter that R5 adds
appears in the R5 module because the R5 package declares it.
The pinned inputs
The generator reads vendored, pinned FHIR packages:
| Package | Version |
|---|---|
hl7.fhir.r4.core | 4.0.1 |
hl7.fhir.r4b.core | 4.3.0 |
hl7.fhir.r5.core | 5.0.0 |
hl7.fhir.r6.core | 6.0.0-ballot |
hl7.terminology | THO |
The packages are vendored verbatim under tools/ferroterm-fhir-codegen/vendor/, each
with a PROVENANCE.md, and fetched by a script. You never hand-edit a vendored
package. Change the fetcher and re-run it.
The rules
- Never hand-edit a
// @generatedfile. To change the output, change the generator (tools/ferroterm-fhir-codegen) or its override map, then regenerate. - The generator emits the complete model within its declared closure. A
terminology server touches a small root set of resources, so the generator’s
root set is the terminology surface (
CodeSystem,ValueSet,ConceptMap,Parameters,OperationOutcome,CapabilityStatement,TerminologyCapabilities,Bundle, and the terminology operations), and it emits the complete transitive closure of the datatypes those roots reference. It never trims inside that closure to quiet a diff, and it never adds a hand-written shape outside it. - A drift check regenerates in CI and fails on any diff, so the generated layer stays in step with the vendored inputs.
Regenerate
$ cargo run -p ferroterm-fhir-codegen -- emit
Then run the drift check. If consuming code needs a shape the generated crate lacks, fix the emitter rather than shadowing it with a hand-written type.
The generator design follows the sibling project FerroEHR, which generates its openEHR model from vendored machine-readable specs the same way.