raz_config/
workspace.rs

1use crate::{
2    CommandConfig, FilterConfig, ProviderConfig, RazConfig, UiConfig,
3    error::{ConfigError, Result},
4    override_config::OverrideCollection,
5    schema::ConfigVersion,
6};
7use once_cell::sync::Lazy;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Mutex;
12
13static WORKSPACE_CONFIG_CACHE: Lazy<Mutex<HashMap<PathBuf, WorkspaceConfig>>> =
14    Lazy::new(|| Mutex::new(HashMap::new()));
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct WorkspaceConfig {
18    pub raz: Option<RazConfig>,
19    pub providers_config: Option<ProviderConfig>,
20    pub filters: Option<FilterConfig>,
21    pub ui: Option<UiConfig>,
22    pub commands: Option<Vec<CommandConfig>>,
23    pub overrides: Option<OverrideCollection>,
24    pub extends: Option<PathBuf>,
25    #[serde(skip)]
26    pub path: PathBuf,
27}
28
29impl WorkspaceConfig {
30    pub fn new(workspace_path: PathBuf) -> Self {
31        Self {
32            raz: None,
33            providers_config: None,
34            filters: None,
35            ui: None,
36            commands: None,
37            overrides: None,
38            extends: None,
39            path: workspace_path,
40        }
41    }
42
43    pub fn load(workspace_path: impl AsRef<Path>) -> Result<Option<Self>> {
44        let workspace_path = workspace_path.as_ref();
45        let config_path = Self::find_config_path(workspace_path)?;
46
47        let Some(config_path) = config_path else {
48            return Ok(None);
49        };
50
51        {
52            let cache = WORKSPACE_CONFIG_CACHE.lock().unwrap();
53            if let Some(cached) = cache.get(&config_path) {
54                return Ok(Some(cached.clone()));
55            }
56        }
57
58        let contents = std::fs::read_to_string(&config_path)?;
59        let mut config: Self = toml::from_str(&contents)?;
60        config.path = config_path.parent().unwrap().to_path_buf();
61
62        if let Some(extends_path) = &config.extends {
63            let base_path = if extends_path.is_relative() {
64                config.path.join(extends_path)
65            } else {
66                extends_path.clone()
67            };
68
69            if let Some(base_config) = Self::load(&base_path)? {
70                config = config.merge_with_base(base_config);
71            }
72        }
73
74        config.validate()?;
75
76        {
77            let mut cache = WORKSPACE_CONFIG_CACHE.lock().unwrap();
78            cache.insert(config_path, config.clone());
79        }
80
81        Ok(Some(config))
82    }
83
84    pub fn save(&self) -> Result<()> {
85        let config_path = self.path.join(crate::WORKSPACE_CONFIG_FILENAME);
86
87        if let Some(parent) = config_path.parent() {
88            std::fs::create_dir_all(parent)?;
89        }
90
91        let contents = toml::to_string_pretty(self)?;
92        std::fs::write(&config_path, contents)?;
93
94        {
95            let mut cache = WORKSPACE_CONFIG_CACHE.lock().unwrap();
96            cache.insert(config_path, self.clone());
97        }
98
99        Ok(())
100    }
101
102    fn find_config_path(start_path: &Path) -> Result<Option<PathBuf>> {
103        let mut current = start_path;
104
105        loop {
106            let config_path = current.join(crate::WORKSPACE_CONFIG_FILENAME);
107            if config_path.exists() {
108                return Ok(Some(config_path));
109            }
110
111            if let Some(parent) = current.parent() {
112                current = parent;
113            } else {
114                break;
115            }
116        }
117
118        Ok(None)
119    }
120
121    pub fn validate(&self) -> Result<()> {
122        if let Some(raz_config) = &self.raz {
123            if raz_config.version.needs_migration(&ConfigVersion::CURRENT) {
124                return Err(ConfigError::VersionMismatch {
125                    expected: ConfigVersion::CURRENT.0,
126                    found: raz_config.version.0,
127                });
128            }
129        }
130
131        if let Some(extends_path) = &self.extends {
132            if extends_path.canonicalize()? == self.path.canonicalize()? {
133                return Err(ConfigError::CyclicInheritance);
134            }
135        }
136
137        Ok(())
138    }
139
140    fn merge_with_base(mut self, base: WorkspaceConfig) -> Self {
141        if self.raz.is_none() && base.raz.is_some() {
142            self.raz = base.raz;
143        }
144
145        if self.providers_config.is_none() && base.providers_config.is_some() {
146            self.providers_config = base.providers_config;
147        }
148
149        if self.filters.is_none() && base.filters.is_some() {
150            self.filters = base.filters;
151        }
152
153        if self.ui.is_none() && base.ui.is_some() {
154            self.ui = base.ui;
155        }
156
157        if self.commands.is_none() && base.commands.is_some() {
158            self.commands = base.commands;
159        } else if let (Some(mut commands), Some(base_commands)) =
160            (self.commands.take(), base.commands)
161        {
162            let mut merged = base_commands;
163            merged.append(&mut commands);
164            self.commands = Some(merged);
165        }
166
167        if self.overrides.is_none() && base.overrides.is_some() {
168            self.overrides = base.overrides;
169        } else if let (Some(overrides), Some(base_overrides)) =
170            (&mut self.overrides, base.overrides)
171        {
172            for (key, override_config) in base_overrides.overrides {
173                overrides.overrides.entry(key).or_insert(override_config);
174            }
175        }
176
177        self
178    }
179
180    pub fn clear_cache() {
181        let mut cache = WORKSPACE_CONFIG_CACHE.lock().unwrap();
182        cache.clear();
183    }
184
185    pub fn invalidate_cache_for(path: &Path) {
186        let mut cache = WORKSPACE_CONFIG_CACHE.lock().unwrap();
187        cache.retain(|k, _| !k.starts_with(path));
188    }
189}