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_manifest::profile::{Profile, ProfileError};
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
36impl ProfileBootstrap {
37    pub fn init() -> ConfigResult<&'static Profile> {
38        if PROFILE.get().is_some() {
39            return Err(ProfileBootstrapError::AlreadyInitialized.into());
40        }
41
42        let path_str = std::env::var("SYSTEMPROMPT_PROFILE")
43            .map_err(|_e| ProfileBootstrapError::PathNotSet)?;
44        let path = std::path::PathBuf::from(path_str);
45
46        let profile = Self::load_from_path_and_validate(&path)?;
47
48        PROFILE_PATH
49            .set(path.to_string_lossy().to_string())
50            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
51
52        PROFILE
53            .set(profile)
54            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
55
56        PROFILE
57            .get()
58            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
59    }
60
61    pub fn get() -> Result<&'static Profile, ProfileBootstrapError> {
62        PROFILE.get().ok_or(ProfileBootstrapError::NotInitialized)
63    }
64
65    pub fn get_path() -> Result<&'static str, ProfileBootstrapError> {
66        PROFILE_PATH
67            .get()
68            .map(String::as_str)
69            .ok_or(ProfileBootstrapError::NotInitialized)
70    }
71
72    #[must_use]
73    pub fn is_initialized() -> bool {
74        PROFILE.get().is_some()
75    }
76
77    pub fn try_init() -> ConfigResult<&'static Profile> {
78        if let Some(profile) = PROFILE.get() {
79            return Ok(profile);
80        }
81        Self::init()
82    }
83
84    pub fn init_from_path(path: &Path) -> ConfigResult<&'static Profile> {
85        if PROFILE.get().is_some() {
86            return Err(ProfileBootstrapError::AlreadyInitialized.into());
87        }
88
89        let profile = Self::load_from_path_and_validate(path)?;
90
91        PROFILE_PATH
92            .set(path.to_string_lossy().to_string())
93            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
94
95        PROFILE
96            .set(profile)
97            .map_err(|_e| ProfileBootstrapError::AlreadyInitialized)?;
98
99        PROFILE
100            .get()
101            .ok_or_else(|| ProfileBootstrapError::NotInitialized.into())
102    }
103
104    fn load_from_path_and_validate(path: &Path) -> ConfigResult<Profile> {
105        let content = std::fs::read_to_string(path).map_err(|source| ProfileError::ReadFile {
106            path: path.to_path_buf(),
107            source,
108        })?;
109        let profile = Profile::from_yaml(&content, path)?;
110        profile.validate()?;
111        Ok(profile)
112    }
113}