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
7use std::path::Path;
8use std::sync::OnceLock;
9
10use systemprompt_models::profile::Profile;
11
12use crate::error::ConfigResult;
13
14static PROFILE: OnceLock<Profile> = OnceLock::new();
15static PROFILE_PATH: OnceLock<String> = OnceLock::new();
16
17#[derive(Debug, Clone, Copy)]
18pub struct ProfileBootstrap;
19
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum ProfileBootstrapError {
23    #[error("Profile not initialized. Call ProfileBootstrap::init() at application startup")]
24    NotInitialized,
25
26    #[error("Profile already initialized")]
27    AlreadyInitialized,
28
29    #[error("Profile path not set. Set SYSTEMPROMPT_PROFILE environment variable")]
30    PathNotSet,
31
32    #[error("Profile validation failed: {0}")]
33    ValidationFailed(String),
34
35    #[error("Failed to load profile: {0}")]
36    LoadFailed(String),
37}
38
39impl ProfileBootstrap {
40    pub fn init() -> ConfigResult<&'static Profile> {
41        if PROFILE.get().is_some() {
42            return Err(ProfileBootstrapError::AlreadyInitialized.into());
43        }
44
45        let path_str =
46            std::env::var("SYSTEMPROMPT_PROFILE").map_err(|_| ProfileBootstrapError::PathNotSet)?;
47        let path = std::path::PathBuf::from(path_str);
48
49        let profile = Self::load_from_path_and_validate(&path)?;
50
51        PROFILE_PATH
52            .set(path.to_string_lossy().to_string())
53            .map_err(|_| ProfileBootstrapError::AlreadyInitialized)?;
54
55        PROFILE
56            .set(profile)
57            .map_err(|_| ProfileBootstrapError::AlreadyInitialized)?;
58
59        PROFILE
60            .get()
61            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
62    }
63
64    pub fn get() -> Result<&'static Profile, ProfileBootstrapError> {
65        PROFILE.get().ok_or(ProfileBootstrapError::NotInitialized)
66    }
67
68    pub fn get_path() -> Result<&'static str, ProfileBootstrapError> {
69        PROFILE_PATH
70            .get()
71            .map(String::as_str)
72            .ok_or(ProfileBootstrapError::NotInitialized)
73    }
74
75    #[must_use]
76    pub fn is_initialized() -> bool {
77        PROFILE.get().is_some()
78    }
79
80    pub fn try_init() -> ConfigResult<&'static Profile> {
81        if let Some(profile) = PROFILE.get() {
82            return Ok(profile);
83        }
84        Self::init()
85    }
86
87    pub fn init_from_path(path: &Path) -> ConfigResult<&'static Profile> {
88        if PROFILE.get().is_some() {
89            return Err(ProfileBootstrapError::AlreadyInitialized.into());
90        }
91
92        let profile = Self::load_from_path_and_validate(path)?;
93
94        PROFILE_PATH
95            .set(path.to_string_lossy().to_string())
96            .map_err(|_| ProfileBootstrapError::AlreadyInitialized)?;
97
98        PROFILE
99            .set(profile)
100            .map_err(|_| ProfileBootstrapError::AlreadyInitialized)?;
101
102        PROFILE
103            .get()
104            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
105    }
106
107    fn load_from_path_and_validate(path: &Path) -> ConfigResult<Profile> {
108        let profile = crate::profile_loader::load_profile_with_catalog(path)?;
109        profile.validate()?;
110        Ok(profile)
111    }
112}