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}