1use std::path::{Path, PathBuf};
2
3use directories::ProjectDirs;
4
5pub 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 #[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 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 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 let second = ConfigInitializer::init_project_config(project_dir, false);
112 expect_that!(
113 second,
114 err(matches_pattern!(ConfigInitError::AlreadyExists(_)))
115 );
116
117 let forced = ConfigInitializer::init_project_config(project_dir, true)?;
119 expect_that!(forced, eq(&project_dir.join("fence.jsonc")));
120 Ok(())
121 }
122}