vpr_core/config.rs
1//! Core runtime configuration.
2//!
3//! This module defines configuration that should be resolved once at process startup and then
4//! passed into core services. The intent is to avoid reading process-wide environment variables
5//! during request handling, which can lead to inconsistent behaviour in multi-threaded runtimes
6//! and test harnesses.
7//!
8//! # Configuration Sources
9//!
10//! Configuration is typically resolved from environment variables at startup:
11//!
12//! - `PATIENT_DATA_DIR`: Base directory for patient data storage
13//! - `RM_SYSTEM_VERSION`: OpenEHR Reference Model version (optional)
14//! - `VPR_NAMESPACE`: Namespace identifier for this VPR instance
15//!
16//! # Directory Structure
17//!
18//! The configuration establishes the following directory layout:
19//!
20//! ```text
21//! patient_data_dir/
22//! ├── clinical/ # Clinical records (Git repos per patient)
23//! └── demographics/ # Demographic data (JSON files per patient)
24//! ```
25//!
26//! # Safety and Validation
27//!
28//! Configuration values are validated at construction time:
29//!
30//! - Directory paths must exist and be accessible
31//! - Namespace cannot be empty
32//! - RM version must be supported
33//!
34//! # Usage Pattern
35//!
36//! ```rust,ignore
37//! // In main.rs or startup code
38//! let config = CoreConfig::new(
39//! patient_data_dir,
40//! rm_version,
41//! namespace,
42//! )?;
43//!
44//! // Pass to services
45//! let clinical_service = ClinicalService::new(Arc::new(config.clone()));
46//! let demographics_service = DemographicsService::new(Arc::new(config));
47//! ```
48
49use crate::constants::{CLINICAL_DIR_NAME, DEMOGRAPHICS_DIR_NAME, LATEST_RM};
50use crate::error::PatientResult;
51use crate::NonEmptyText;
52use std::path::{Path, PathBuf};
53
54/// Core configuration resolved at startup.
55///
56/// This struct holds all configuration values that are determined once at process startup
57/// and remain immutable throughout the application lifecycle. It provides access to:
58///
59/// - Patient data storage directories
60/// - OpenEHR Reference Model version
61/// - VPR instance namespace
62///
63/// All paths are validated and canonicalized during construction.
64#[derive(Clone, Debug)]
65pub struct CoreConfig {
66 patient_data_dir: PathBuf,
67 rm_system_version: openehr::RmVersion,
68 vpr_namespace: NonEmptyText,
69}
70
71impl CoreConfig {
72 /// Create a new `CoreConfig`.
73 ///
74 /// # Arguments
75 ///
76 /// * `patient_data_dir` - Base directory for patient data storage
77 /// * `rm_system_version` - OpenEHR Reference Model version
78 /// * `vpr_namespace` - Namespace identifier (cannot be empty)
79 ///
80 /// # Errors
81 ///
82 /// Returns `PatientError::InvalidInput` if `vpr_namespace` is empty or whitespace-only.
83 pub fn new(
84 patient_data_dir: PathBuf,
85 rm_system_version: openehr::RmVersion,
86 vpr_namespace: NonEmptyText,
87 ) -> PatientResult<Self> {
88 Ok(Self {
89 patient_data_dir,
90 rm_system_version,
91 vpr_namespace,
92 })
93 }
94
95 /// Get the base patient data directory.
96 ///
97 /// This is the root directory containing `clinical/` and `demographics/` subdirectories.
98 pub fn patient_data_dir(&self) -> &Path {
99 &self.patient_data_dir
100 }
101
102 /// Get the clinical records directory.
103 ///
104 /// Returns `patient_data_dir/clinical/`.
105 pub fn clinical_dir(&self) -> PathBuf {
106 self.patient_data_dir.join(CLINICAL_DIR_NAME)
107 }
108
109 /// Get the demographics directory.
110 ///
111 /// Returns `patient_data_dir/demographics/`.
112 pub fn demographics_dir(&self) -> PathBuf {
113 self.patient_data_dir.join(DEMOGRAPHICS_DIR_NAME)
114 }
115
116 /// Get the OpenEHR Reference Model version.
117 ///
118 /// This determines which RM features and constraints are enforced.
119 pub fn rm_system_version(&self) -> openehr::RmVersion {
120 self.rm_system_version
121 }
122
123 /// Get the VPR namespace identifier.
124 ///
125 /// Used to isolate different VPR instances or deployments.
126 pub fn vpr_namespace(&self) -> &str {
127 self.vpr_namespace.as_str()
128 }
129}
130
131/// Parse the RM system version from an optional string value.
132///
133/// If `value` is `None` or empty/whitespace, returns the latest supported RM.
134///
135/// # Arguments
136///
137/// * `value` - Optional string representation of an RM version (e.g., "1.0.4")
138///
139/// # Returns
140///
141/// The parsed `RmVersion` or the latest supported version if no value provided.
142///
143/// # Errors
144///
145/// Returns `PatientError` if the version string cannot be parsed.
146pub fn rm_system_version_from_env_value(
147 value: Option<NonEmptyText>,
148) -> PatientResult<openehr::RmVersion> {
149 let value = value
150 .map(|v| v.as_str().to_string())
151 .filter(|v| !v.is_empty());
152 let parsed = value.map(|v| v.parse::<openehr::RmVersion>()).transpose()?;
153
154 Ok(parsed.unwrap_or(LATEST_RM))
155}