Skip to main content

systemprompt_loader/
profile_loader.rs

1//! Reads, validates, writes and enumerates layered profiles.
2//!
3//! [`ProfileLoader`] is a thin shim over the manifest's layered loader: a
4//! profile is `<profiles_dir>/<env>/profile.yaml` over the optional shared
5//! `<profiles_dir>/base.yaml`. Writes go to the overlay only.
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 systemprompt_identifiers::ProfileName;
12use systemprompt_manifest::Profile;
13use systemprompt_manifest::profile::OVERLAY_PROFILE_FILE;
14
15use crate::error::{ProfileLoadError, ProfileLoadResult};
16
17#[derive(Debug, Clone, Copy)]
18pub struct ProfileLoader;
19
20impl ProfileLoader {
21    pub fn load_from_path(profile_path: &Path) -> ProfileLoadResult<Profile> {
22        Profile::load_from_path(profile_path).map_err(ProfileLoadError::from)
23    }
24
25    pub fn load(profiles_dir: &Path, profile_name: &ProfileName) -> ProfileLoadResult<Profile> {
26        Profile::load_layered(profiles_dir, profile_name.as_str()).map_err(ProfileLoadError::from)
27    }
28
29    pub fn load_from_path_and_validate(profile_path: &Path) -> ProfileLoadResult<Profile> {
30        let profile = Self::load_from_path(profile_path)?;
31        profile.validate().map_err(ProfileLoadError::from)?;
32        Ok(profile)
33    }
34
35    pub fn load_and_validate(
36        profiles_dir: &Path,
37        profile_name: &ProfileName,
38    ) -> ProfileLoadResult<Profile> {
39        let profile = Self::load(profiles_dir, profile_name)?;
40        profile.validate().map_err(ProfileLoadError::from)?;
41        Ok(profile)
42    }
43
44    pub fn save(profile: &Profile, profiles_dir: &Path) -> ProfileLoadResult<()> {
45        let overlay_path = profiles_dir.join(&profile.name).join(OVERLAY_PROFILE_FILE);
46        let header = format!(
47            "# systemprompt.io Profile: {}\n# Overlay over ../base.yaml: holds only what this \
48             environment changes.",
49            profile.display_name
50        );
51        profile
52            .save_overlay(&overlay_path, Some(&header))
53            .map_err(ProfileLoadError::from)
54    }
55
56    #[must_use]
57    pub fn list_available(profiles_dir: &Path) -> Vec<String> {
58        let entries = match std::fs::read_dir(profiles_dir) {
59            Ok(entries) => entries,
60            Err(e) => {
61                if profiles_dir.exists() {
62                    tracing::warn!(
63                        error = %e,
64                        path = %profiles_dir.display(),
65                        "Failed to read profiles directory"
66                    );
67                }
68                return Vec::new();
69            },
70        };
71        let mut names: Vec<String> = entries
72            .filter_map(Result::ok)
73            .filter(|e| e.path().join(OVERLAY_PROFILE_FILE).is_file())
74            .map(|e| e.file_name().to_string_lossy().to_string())
75            .collect();
76        names.sort();
77        names
78    }
79}