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}