Skip to main content

systemprompt_config/bootstrap/
profile.rs

1//! Process-wide profile bootstrap.
2//!
3//! Loads the active profile YAML from `SYSTEMPROMPT_PROFILE` (or an
4//! explicit path) and stores it in a `OnceLock` so the rest of the
5//! application can access it without passing it down call stacks.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::path::Path;
11use std::sync::OnceLock;
12
13use systemprompt_models::profile::Profile;
14
15use crate::error::ConfigResult;
16
17static PROFILE: OnceLock<Profile> = OnceLock::new();
18static PROFILE_PATH: OnceLock<String> = OnceLock::new();
19
20#[derive(Debug, Clone, Copy)]
21pub struct ProfileBootstrap;
22
23#[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum ProfileBootstrapError {
26    #[error("Profile not initialized. Call ProfileBootstrap::init() at application startup")]
27    NotInitialized,
28
29    #[error("Profile already initialized")]
30    AlreadyInitialized,
31
32    #[error("Profile path not set. Set SYSTEMPROMPT_PROFILE environment variable")]
33    PathNotSet,
34
35    #[error("Profile validation failed: {0}")]
36    ValidationFailed(String),
37
38    #[error("Failed to load profile: {0}")]
39    LoadFailed(String),
40}
41
42impl ProfileBootstrap {
43    pub fn init() -> ConfigResult<&'static Profile> {
44        if PROFILE.get().is_some() {
45            return Err(ProfileBootstrapError::AlreadyInitialized.into());
46        }
47
48        let path_str = std::env::var("SYSTEMPROMPT_PROFILE")
49            .map_err(|_e| ProfileBootstrapError::PathNotSet)?;
50        let path = std::path::PathBuf::from(path_str);
51
52        let profile = Self::load_from_path_and_validate(&path)?;
53
54        PROFILE_PATH
55            .set(path.to_string_lossy().to_string())
56            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
57
58        PROFILE
59            .set(profile)
60            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
61
62        PROFILE
63            .get()
64            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
65    }
66
67    pub fn get() -> Result<&'static Profile, ProfileBootstrapError> {
68        PROFILE.get().ok_or(ProfileBootstrapError::NotInitialized)
69    }
70
71    pub fn get_path() -> Result<&'static str, ProfileBootstrapError> {
72        PROFILE_PATH
73            .get()
74            .map(String::as_str)
75            .ok_or(ProfileBootstrapError::NotInitialized)
76    }
77
78    #[must_use]
79    pub fn is_initialized() -> bool {
80        PROFILE.get().is_some()
81    }
82
83    pub fn try_init() -> ConfigResult<&'static Profile> {
84        if let Some(profile) = PROFILE.get() {
85            return Ok(profile);
86        }
87        Self::init()
88    }
89
90    pub fn init_from_path(path: &Path) -> ConfigResult<&'static Profile> {
91        if PROFILE.get().is_some() {
92            return Err(ProfileBootstrapError::AlreadyInitialized.into());
93        }
94
95        let profile = Self::load_from_path_and_validate(path)?;
96
97        PROFILE_PATH
98            .set(path.to_string_lossy().to_string())
99            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
100
101        PROFILE
102            .set(profile)
103            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
104
105        PROFILE
106            .get()
107            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
108    }
109
110    fn load_from_path_and_validate(path: &Path) -> ConfigResult<Profile> {
111        let profile = crate::profile_loader::load_profile_with_catalog(path)?;
112        profile.validate()?;
113        Ok(profile)
114    }
115}