Skip to main content

aiw/
config.rs

1use std::path::{Path, PathBuf};
2
3use directories::ProjectDirs;
4
5/// Default embedded `fence.jsonc` template content for aiw.
6pub const DEFAULT_AIW_TEMPLATE: &str = include_str!("../templates/fence.jsonc");
7
8#[derive(Debug, thiserror::Error)]
9pub enum ConfigInitError {
10    #[error("Configuration already exists: {0}")]
11    AlreadyExists(PathBuf),
12    #[error("Could not determine user configuration directory")]
13    UserConfigPathNotFound,
14    #[error("I/O error during config initialization: {0}")]
15    Io(#[from] std::io::Error),
16}
17
18pub struct ConfigInitializer;
19
20impl ConfigInitializer {
21    /// Returns the canonical user config path (e.g. ~/.config/aiw/fence.jsonc).
22    #[must_use]
23    pub fn user_config_path() -> Option<PathBuf> {
24        ProjectDirs::from("", "", "aiw").map(|proj| proj.config_dir().join("fence.jsonc"))
25    }
26
27    /// Initializes user configuration (~/.config/aiw/fence.jsonc) if it does not exist yet (or if force is true).
28    pub fn init_user_config(force: bool) -> Result<PathBuf, ConfigInitError> {
29        let path = Self::user_config_path().ok_or(ConfigInitError::UserConfigPathNotFound)?;
30
31        if path.exists() && !force {
32            return Err(ConfigInitError::AlreadyExists(path));
33        }
34
35        if let Some(parent) = path.parent() {
36            std::fs::create_dir_all(parent)?;
37        }
38        std::fs::write(&path, DEFAULT_AIW_TEMPLATE)?;
39        Ok(path)
40    }
41
42    /// Initializes project configuration (fence.jsonc) in `target_dir` if it does not exist yet (or if force is true).
43    pub fn init_project_config(target_dir: &Path, force: bool) -> Result<PathBuf, ConfigInitError> {
44        let fence_jsonc = target_dir.join("fence.jsonc");
45        let fence_json = target_dir.join("fence.json");
46
47        if !force {
48            if fence_jsonc.exists() {
49                return Err(ConfigInitError::AlreadyExists(fence_jsonc));
50            }
51            if fence_json.exists() {
52                return Err(ConfigInitError::AlreadyExists(fence_json));
53            }
54        }
55
56        let base_path_str = Self::user_config_path()
57            .map(|p| p.to_string_lossy().into_owned())
58            .unwrap_or_else(|| "~/.config/aiw/fence.jsonc".to_string());
59
60        let content = format!(
61            r#"{{
62  "$schema": "https://raw.githubusercontent.com/fencesandbox/fence/main/docs/schema/fence.schema.json",
63  "extends": "{base_path_str}",
64  "filesystem": {{
65    "allowRead": []
66  }}
67}}
68"#
69        );
70
71        std::fs::write(&fence_jsonc, content)?;
72        Ok(fence_jsonc)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use googletest::prelude::*;
80
81    #[googletest::test]
82    fn default_aiw_template_contains_schema_and_extends() {
83        expect_that!(
84            DEFAULT_AIW_TEMPLATE,
85            contains_substring("fence.schema.json")
86        );
87        expect_that!(
88            DEFAULT_AIW_TEMPLATE,
89            contains_substring("\"extends\": \"code\"")
90        );
91        expect_that!(DEFAULT_AIW_TEMPLATE, contains_substring("\"/nix\""));
92    }
93
94    #[googletest::test]
95    fn init_project_config_creates_fence_jsonc_with_extends()
96    -> Result<(), Box<dyn std::error::Error>> {
97        let temp_dir = tempfile::tempdir()?;
98        let project_dir = temp_dir.path();
99
100        let created = ConfigInitializer::init_project_config(project_dir, false)?;
101        expect_that!(created, eq(&project_dir.join("fence.jsonc")));
102
103        let fence_file = project_dir.join("fence.jsonc");
104        expect_that!(fence_file.exists(), is_true());
105
106        let content = std::fs::read_to_string(&fence_file)?;
107        expect_that!(&content, contains_substring("fence.schema.json"));
108        expect_that!(&content, contains_substring("\"extends\":"));
109
110        // Second call without force returns AlreadyExists error
111        let second = ConfigInitializer::init_project_config(project_dir, false);
112        expect_that!(
113            second,
114            err(matches_pattern!(ConfigInitError::AlreadyExists(_)))
115        );
116
117        // Call with force returns Ok(path)
118        let forced = ConfigInitializer::init_project_config(project_dir, true)?;
119        expect_that!(forced, eq(&project_dir.join("fence.jsonc")));
120        Ok(())
121    }
122}