FHIR Integration
Overview
The coordination repository uses FHIR-aligned wire formats for interoperability without implementing FHIR JSON, REST endpoints, or transport semantics.
This approach:
- Preserves FHIR semantic meaning
- Uses repository-based storage model
- Enables future FHIR projections
- Maintains human-readable formats
- Provides strict schema validation
Coordination Status
Overview
The fhir::CoordinationStatus module handles parsing and rendering of COORDINATION_STATUS.yaml files.
API
#![allow(unused)]
fn main() {
// Parse from YAML
let status_data = fhir::CoordinationStatus::parse(yaml_text)?;
// Render to YAML
let yaml_text = fhir::CoordinationStatus::render(&status_data)?;
}
Domain Types
-
CoordinationStatusData- Top-level status structurecoordination_id: Uuid- Coordination repository identifierclinical_id: Uuid- Linked clinical record identifierstatus: StatusInfo- Status information
-
StatusInfo- Status detailslifecycle_state: LifecycleState- Current lifecycle staterecord_open: bool- Whether accepting new entriesrecord_queryable: bool- Whether queries are permittedrecord_modifiable: bool- Whether modifications are permitted
-
LifecycleState- EnumerationActive- Operational and accepting updatesSuspended- Temporarily inactiveClosed- Permanently closed
Validation
- UUID validation for
coordination_idandclinical_id - Enum validation for
lifecycle_state - Boolean validation for permission flags
- Strict schema with
deny_unknown_fields
Wire Format
Internal wire types use string UUIDs, translated to proper Uuid types at boundaries:
#![allow(unused)]
fn main() {
// Wire format (internal)
struct CoordinationStatusWire {
coordination_id: String,
clinical_id: String,
status: StatusWire,
}
// Domain format (public)
struct CoordinationStatusData {
coordination_id: Uuid,
clinical_id: Uuid,
status: StatusInfo,
}
}
Thread Ledgers
Overview
The fhir::Messaging module handles parsing and rendering of messaging thread ledger.yaml files.
This implementation uses FHIR Communication resource semantics without FHIR JSON transport.
API
#![allow(unused)]
fn main() {
// Parse from YAML
let ledger_data = fhir::Messaging::ledger_parse(yaml_text)?;
// Render to YAML
let yaml_text = fhir::Messaging::ledger_render(&ledger_data)?;
}
Domain Types
-
LedgerData- Top-level ledger structurethread_id: TimestampId- Thread identifierstatus: ThreadStatus- Thread statuscreated_at: DateTime<Utc>- Creation timestamplast_updated_at: DateTime<Utc>- Last update timestampparticipants: Vec<LedgerParticipant>- Participant listvisibility: LedgerVisibility- Visibility settingspolicies: LedgerPolicies- Participation policiesaudit: LedgerAudit- Change audit trail
-
ThreadStatus- EnumerationOpen- Active, accepting messagesClosed- Closed to new messagesArchived- Archived (hidden from default views)
-
LedgerParticipant- Participant informationparticipant_id: Uuid- Participant identifierrole: ParticipantRole- Participant roledisplay_name: String- Human-readable nameorganisation: Option<String>- Organization affiliation
-
ParticipantRole- EnumerationClinician- Clinical staff memberPatient- Patient participantCareTeam- Care team member or healthcare professionalSystem- System-generated participant
-
LedgerVisibility- Visibility settingssensitivity: String- Sensitivity level (standard, confidential, restricted)restricted: bool- Whether access is restricted beyond normal rules
-
LedgerPolicies- Participation policiesallow_patient_participation: bool- Patient participation permittedallow_external_organisations: bool- External organizations permitted
-
LedgerAudit- Audit trailcreated_by: String- Creator identifierchange_log: Vec<AuditChangeLog>- Chronological change log
-
AuditChangeLog- Single audit entrychanged_at: DateTime<Utc>- Change timestampchanged_by: String- Actor identifierdescription: String- Human-readable description
Validation
- UUID validation for
thread_id(asTimestampId) - UUID validation for all
participant_idfields - DateTime parsing with timezone handling
- Enum validation for
statusandrolefields - Strict schema with
deny_unknown_fields
Wire Format
Internal wire types separate concerns:
#![allow(unused)]
fn main() {
// Wire format (internal)
struct Ledger {
thread_id: String,
status: ThreadStatus,
created_at: DateTime<Utc>,
// ... string UUIDs, raw timestamps
}
// Domain format (public)
struct LedgerData {
thread_id: TimestampId,
status: ThreadStatus,
created_at: DateTime<Utc>,
// ... proper UUID types, validated timestamps
}
}
Translation happens at parse/render boundaries using internal helper functions.
Wire Format Principles
Separation of Concerns
- Wire types are internal implementation details
- Domain types are public API surface
- Translation happens at boundaries only
- Consumers work with domain types exclusively
Strict Validation
All wire formats use #[serde(deny_unknown_fields)]:
- Unknown fields are rejected
- Prevents silent schema drift
- Ensures forward compatibility is explicit
- Catches typos and configuration errors
Type Safety
- String identifiers validated and converted to proper types
- UUIDs parsed and validated at boundaries
- Timestamps validated and converted to
DateTime<Utc> - Enumerations validated against allowed values
Human-Readable Formats
YAML is used for all wire formats:
- Git-friendly diffs
- Human-readable without tooling
- Suitable for manual review
- Easy to debug and inspect
Error Handling
Parse errors use serde_path_to_error for detailed diagnostics:
Thread ledger schema mismatch at participants[0].role:
unknown variant `doctor`, expected one of
`clinician`, `patient`, `careteam`, `system`
This provides:
- Precise error location in document
- Clear error description
- Expected values for enumerations
- Actionable feedback for corrections
FHIR Alignment
Conceptual Model
VPR coordination uses FHIR resource semantics:
- COORDINATION_STATUS.yaml ≈ FHIR operational status tracking
- Thread ledger.yaml ≈ FHIR Communication metadata
- messages.md ≈ FHIR Communication content
This is conceptual alignment, not implementation:
- No FHIR JSON format
- No FHIR REST endpoints
- No FHIR server behavior
- No FHIR Bundle/Transaction semantics
Future Projections
FHIR-aligned wire formats enable future projections to:
- FHIR Communication resources - For messaging threads
- FHIR Task resources - For coordination tasks
- FHIR DocumentReference - For compositions
- FHIR RESTful APIs - For external integrations
Projection can happen:
- At API boundaries (gRPC/REST to FHIR)
- Via export tools (VPR to FHIR Bundle)
- Through ETL pipelines (VPR to FHIR data warehouse)
Semantic Preservation
Key FHIR concepts preserved:
- Communication.status →
ThreadStatus(open, closed, archived) - Communication.recipient →
participantswith roles - Communication.sender → author metadata in messages
- Communication.sent →
created_attimestamp - Communication.payload → message content in messages.md
This ensures:
- No semantic loss in translation
- Clear mapping to FHIR when needed
- Compatibility with FHIR-based systems
- Standards-based interoperability
Implementation Details
Module Structure
crates/fhir/src/
lib.rs # Public exports and error types
coordination_status.rs # COORDINATION_STATUS.yaml handling
messaging.rs # Thread ledger.yaml handling
Error Types
#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum FhirError {
InvalidInput(String),
InvalidYaml(serde_yaml::Error),
Translation(String),
InvalidUuid(String),
// ...
}
}
Errors are converted to PatientError at boundaries via From trait.
Testing
Each module includes comprehensive tests:
- Round-trip parsing (parse → render → parse)
- Schema validation (reject unknown fields)
- Type validation (reject wrong types)
- UUID validation (reject malformed UUIDs)
- Enum validation (reject unknown variants)
- Edge cases (minimal valid documents, optional fields)
Dependencies
serdeandserde_yaml- Serializationserde_path_to_error- Detailed error pathschrono- Timestamp handlinguuid- UUID typesvpr_uuid- TimestampId type
Usage Examples
Coordination Status
#![allow(unused)]
fn main() {
use fhir::{CoordinationStatus, CoordinationStatusData, StatusInfo, LifecycleState};
// Create new status
let status_data = CoordinationStatusData {
coordination_id: Uuid::new_v4(),
clinical_id: existing_clinical_uuid,
status: StatusInfo {
lifecycle_state: LifecycleState::Active,
record_open: true,
record_queryable: true,
record_modifiable: true,
},
};
// Render to YAML
let yaml = CoordinationStatus::render(&status_data)?;
// Write to file
fs::write("COORDINATION_STATUS.yaml", yaml)?;
// Later, parse back
let yaml_text = fs::read_to_string("COORDINATION_STATUS.yaml")?;
let parsed = CoordinationStatus::parse(&yaml_text)?;
assert_eq!(status_data, parsed);
}
Thread Ledger
#![allow(unused)]
fn main() {
use fhir::{Messaging, LedgerData, ThreadStatus, LedgerParticipant, ParticipantRole};
// Create ledger
let ledger_data = LedgerData {
thread_id: thread_id,
status: ThreadStatus::Open,
created_at: Utc::now(),
last_updated_at: Utc::now(),
participants: vec![
LedgerParticipant {
participant_id: clinician_uuid,
role: ParticipantRole::Clinician,
display_name: "Dr Jane Smith".to_string(),
organisation: Some("Example NHS Trust".to_string()),
},
],
// ... visibility, policies, audit
};
// Render to YAML
let yaml = Messaging::ledger_render(&ledger_data)?;
// Write to file
fs::write("ledger.yaml", yaml)?;
// Later, parse back
let yaml_text = fs::read_to_string("ledger.yaml")?;
let parsed = Messaging::ledger_parse(&yaml_text)?;
}