openehr

openEHR Reference Model types, validation, paths, AQL parsing, and change-control security primitives — in Rust.

openEHR specifies clinical information as a small, stable Reference Model of about ninety classes, plus archetypes that constrain it into clinical content. This crate implements the Reference Model and the machinery around it, so a Rust program can read, build, check, address, and safely disclose openEHR data without inventing its own idea of what a health record is.

[dependencies]
openehr = "0.1"

What it does#

use openehr::path::Pathable;
use openehr::rm::ehr::Composition;
use openehr::validation::Validate;

// Read a composition another openEHR implementation wrote.
let composition: Composition = serde_json::from_str(json)?;

// Check the Reference Model invariants. Deserialization never calls a
// constructor, so this is the only gate on data that arrived from elsewhere.
composition.validate_ok()?;

// Address a node by openEHR path.
let systolic = composition.item_at_path(
    "/content[openEHR-EHR-OBSERVATION.blood_pressure.v2]\
     /data/events[at0006]/data/items[at0004]/value/magnitude",
)?;
Module openEHR component
base BASE: identifiers, references, intervals, ISO 8601
rm::data_types RM: Data Types — every DV_* class
rm::data_structures RM: Data Structures — ITEM_*, CLUSTER, ELEMENT, HISTORY
rm::common RM: Common — archetyping, parties, audit, change control
rm::ehr RM: EHR — COMPOSITION, the five entry classes, EHR_STATUS, FOLDER
rm::demographic RM: Demographic — PERSON, ROLE, ORGANISATION, AGENT
terminology TERM: the openEHR support terminology, sixteen groups
path openEHR path parsing and navigation
aql QUERY: AQL lexing, parsing, and static checking
validation Reference Model invariant checking
security EHR_ACCESS, tamper-evident audit chaining, redaction

Everything serializes to and from openEHR canonical JSON (ITS-JSON).

What it does not do#

Stating this plainly is part of the design. A clinical library that implies coverage it does not have is worse than a small one.

Not implemented Why
Archetypes and templates (AM, ADL, AOM2) a parser and a constraint engine, each larger than this crate
AQL execution needs a repository; aql parses and checks, and returns no rows
Terminology lookup beyond openEHR's own needs a terminology server; external codes are carried opaquely
UCUM unit conversion a wrong conversion is a thousand-fold dosing error
REST service, persistence, EHR Extract out of scope — see spec/01-scope.md
HL7 GTS / PIVL timing evaluation returns Unsupported rather than a guess
OpenPGP verification, encryption key management belongs to the deployment

Where openEHR defines an operation this crate does not implement, the operation returns an explicit Unsupported error naming the specification section that records the exclusion. It never returns a plausible default.

Three design commitments#

Refuse rather than guess. Comparison is partial throughout. A month-precision date is not ordered against a day inside that month; 5 mg is not comparable with 5 mL; a path matching three elements fails rather than returning the first. Each has a plausible wrong answer that no downstream reader could detect.

let may: Date = "2024-05".parse()?;
let may_17: Date = "2024-05-17".parse()?;
assert_eq!(may.partial_cmp(&may_17), None);   // May which day?

let mg = DvQuantity::new(5.0, "mg")?;
let ml = DvQuantity::new(5.0, "mL")?;
assert_eq!(mg.partial_cmp(&ml), None);        // not the same dose of anything

Absence is structured. openEHR's four null flavours are four different clinical facts, and this crate will not let them collapse:

Flavour Means
271|no information| nobody looked
253|unknown| somebody looked and could not find out
272|masked| the value exists and is withheld
273|not applicable| the question does not arise

"No allergy history recorded" is the first and "no known allergies" is the fourth. Prescribing software that treats them alike will eventually give a penicillin-allergic patient penicillin.

Nothing prints protected health information. No Display renders an identifier or a media blob; no error echoes a submitted value; a validation report names paths and invariants and never content; redaction masks rather than deletes, and reports how much it withheld rather than what.

Two gates, not one#

Constructors enforce invariants on data the program builds. validation enforces them on data the program receivesserde writes fields directly and never calls a constructor. A service that deserializes and stores without validating has no invariant checking at all, whatever its constructors do.

// No constructor in this crate produces this. A sender can still send it.
let element: Element = serde_json::from_str(
    r#"{"name":{"value":"Systolic"},"archetype_node_id":"at0004",
        "value":{"_type":"DV_COUNT","magnitude":1},
        "null_flavour":{"value":"unknown","defining_code":
          {"terminology_id":{"value":"openehr"},"code_string":"253"}}}"#,
)?;
assert_eq!(element.validate().violations()[0].invariant, "Null_flavour_indicated");

Security#

security supplies what a library can supply, and says what it cannot.

  • EHR_ACCESS with a default-deny decision, a documented reference scheme, and lossless carriage of schemes it cannot evaluate. Dispatch is by declared scheme name and never by object shape, so a foreign policy is never reinterpreted as an empty local one.
  • A tamper-evident chain over committed versions, unkeyed or with an HMAC-SHA-256 tag. The documentation states plainly what an unkeyed chain buys — it detects careless modification and supports an external witness, and it does not stop an informed attacker with write access. Only a tag mismatch is a tampering finding; an unheld key is reported as an unheld key.
  • Redaction that masks as 272|masked|, keeps the document valid, and counts rather than names what it withheld.

What the deployment must still provide: authentication, group membership, transport security, key storage, consent capture, and log retention. See spec/11-security.md for the whole boundary.

Examples#

cargo run --example 01_build_composition     # build a blood pressure, emit canonical JSON
cargo run --example 02_validate_incoming     # four defects a JSON schema would not catch
cargo run --example 03_paths_and_aql         # path navigation and AQL parsing side by side
cargo run --example 04_versioning_and_audit  # commits, concurrent-write refusal, chain verification
cargo run --example 05_access_and_redaction  # default-deny decisions and consent filtering

Specification-driven#

spec/ is normative. Every requirement has a permanent identifier cited from the code, the tests, and the documentation, so a claim about this crate is traceable back to a decision.

Read For
spec/index.md the map, and what this spec adds to openEHR's
spec/01-scope.md what is excluded and why
spec/conformance-matrix.md what is verified today
spec/audit.md every known gap, with evidence

Two numbers from those files, because they are the ones worth knowing before depending on this crate: of 291 requirements, 237 are verified by a named test and 3 are implemented with no test at all. A further 13 are marked type — enforced by the compiler, where a runtime test could not fail — rather than counted as verified, so the first number means what it says.

Status#

Version 0.1.0. First release. The Reference Model surface is complete for the packages listed above and the open findings are in spec/audit.md — twelve of them, seven already fixed, none a false claim in the documentation.

Every code fragment above is compiled and run as a test (tests/readme.rs). A documented example that does not compile is worse than none, because it costs the reader the time to find out.

Building#

cargo build
cargo test                      # unit, integration, and doctests
cargo clippy --all-targets      # pedantic, with missing_docs/errors/panics denied
cargo fmt --all -- --check

MSRV is rust-version in Cargo.toml (currently 1.90), Rust edition 2024.

Licence#

MIT OR Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.

openEHR specifications are published by the openEHR Foundation under CC-BY-SA; this crate is an independent implementation and is not endorsed by or affiliated with the openEHR Foundation.

View this page's source on GitHub — the crates are the source of truth; this site renders them.