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 receives — serde 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_ACCESSwith 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-256tag. 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.