1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use std::time::Duration;
7use tokio::fs;
8
9use super::{CheckType, PipelineStage};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SafetyConfig {
14 pub enabled: bool,
16 pub strict_mode: bool,
18 pub show_progress: bool,
20 pub parallel_checks: bool,
22 pub pre_commit: StageConfig,
24 pub pre_push: StageConfig,
26 pub publish: StageConfig,
28 pub bypass: BypassConfig,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct StageConfig {
35 pub enabled: bool,
37 pub timeout_seconds: u64,
39 pub checks: Vec<CheckType>,
41 pub continue_on_warning: bool,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BypassConfig {
48 pub enabled: bool,
50 pub require_reason: bool,
52 pub require_confirmation: bool,
54 pub log_bypasses: bool,
56 pub max_bypasses_per_day: u32,
58}
59
60impl Default for BypassConfig {
61 fn default() -> Self {
62 Self {
63 enabled: true,
64 require_reason: true,
65 require_confirmation: true,
66 log_bypasses: true,
67 max_bypasses_per_day: 3,
68 }
69 }
70}
71
72impl BypassConfig {
73 pub async fn load_or_default() -> Result<Self> {
82 match SafetyConfig::load().await {
84 Ok(config) => Ok(config.bypass),
85 Err(_) => Ok(Self::default()),
86 }
87 }
88}
89
90impl Default for SafetyConfig {
91 fn default() -> Self {
92 Self {
93 enabled: true,
94 strict_mode: true,
95 show_progress: true,
96 parallel_checks: true,
97 pre_commit: StageConfig {
98 enabled: true,
99 timeout_seconds: 300, checks: CheckType::for_stage(PipelineStage::PreCommit),
101 continue_on_warning: false,
102 },
103 pre_push: StageConfig {
104 enabled: true,
105 timeout_seconds: 600, checks: CheckType::for_stage(PipelineStage::PrePush),
107 continue_on_warning: false,
108 },
109 publish: StageConfig {
110 enabled: true,
111 timeout_seconds: 900, checks: CheckType::for_stage(PipelineStage::Publish),
113 continue_on_warning: false,
114 },
115 bypass: BypassConfig {
116 enabled: true, require_reason: true,
118 require_confirmation: true,
119 log_bypasses: true,
120 max_bypasses_per_day: 3,
121 },
122 }
123 }
124}
125
126impl SafetyConfig {
127 pub async fn load_or_default() -> Result<Self> {
134 match Self::load().await {
135 Ok(config) => Ok(config),
136 Err(_) => Ok(Self::default()),
137 }
138 }
139
140 pub async fn load() -> Result<Self> {
146 let config_path = Self::config_file_path()?;
147 let contents = fs::read_to_string(&config_path)
148 .await
149 .map_err(|e| Error::config(format!("Failed to read safety config: {}", e)))?;
150
151 let config: Self = toml::from_str(&contents)
152 .map_err(|e| Error::config(format!("Failed to parse safety config: {}", e)))?;
153
154 Ok(config)
155 }
156
157 pub async fn save(&self) -> Result<()> {
164 let config_path = Self::config_file_path()?;
165
166 if let Some(parent) = config_path.parent() {
168 fs::create_dir_all(parent).await?;
169 }
170
171 let contents = toml::to_string_pretty(self)
172 .map_err(|e| Error::config(format!("Failed to serialize safety config: {}", e)))?;
173
174 fs::write(&config_path, contents)
175 .await
176 .map_err(|e| Error::config(format!("Failed to write safety config: {}", e)))?;
177
178 Ok(())
179 }
180
181 pub fn config_file_path() -> Result<PathBuf> {
187 let config_dir = crate::config::Config::config_dir_path()?;
188 Ok(config_dir.join("safety.toml"))
189 }
190
191 pub fn get_stage_config(&self, stage: PipelineStage) -> &StageConfig {
193 match stage {
194 PipelineStage::PreCommit => &self.pre_commit,
195 PipelineStage::PrePush => &self.pre_push,
196 PipelineStage::Publish => &self.publish,
197 }
198 }
199
200 pub fn get_stage_config_mut(&mut self, stage: PipelineStage) -> &mut StageConfig {
202 match stage {
203 PipelineStage::PreCommit => &mut self.pre_commit,
204 PipelineStage::PrePush => &mut self.pre_push,
205 PipelineStage::Publish => &mut self.publish,
206 }
207 }
208
209 pub fn is_check_enabled(&self, stage: PipelineStage, check: CheckType) -> bool {
211 let stage_config = self.get_stage_config(stage);
212 stage_config.enabled && stage_config.checks.contains(&check)
213 }
214
215 pub fn get_timeout(&self, stage: PipelineStage) -> Duration {
217 Duration::from_secs(self.get_stage_config(stage).timeout_seconds)
218 }
219
220 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
227 match key {
228 key if key.starts_with("bypass.") => self.set_bypass_config(key, value),
229 key if key.contains('.') => self.set_stage_config(key, value),
230 _ => self.set_main_config(key, value),
231 }
232 }
233
234 fn set_main_config(&mut self, key: &str, value: &str) -> Result<()> {
236 match key {
237 "enabled" => {
238 self.enabled = self.parse_bool(value, "enabled")?;
239 }
240 "strict_mode" => {
241 self.strict_mode = self.parse_bool(value, "strict_mode")?;
242 }
243 "show_progress" => {
244 self.show_progress = self.parse_bool(value, "show_progress")?;
245 }
246 "parallel_checks" => {
247 self.parallel_checks = self.parse_bool(value, "parallel_checks")?;
248 }
249 _ => return Err(Error::config(format!("Unknown safety config key: {}", key))),
250 }
251 Ok(())
252 }
253
254 fn set_stage_config(&mut self, key: &str, value: &str) -> Result<()> {
256 let (stage, field) = key
257 .split_once('.')
258 .ok_or_else(|| Error::config(format!("Invalid config key format: {}", key)))?;
259
260 match (stage, field) {
261 ("pre_commit", "enabled") => {
262 self.pre_commit.enabled = self.parse_bool(value, "pre_commit.enabled")?;
263 }
264 ("pre_commit", "timeout_seconds") => {
265 self.pre_commit.timeout_seconds =
266 self.parse_u64(value, "pre_commit.timeout_seconds")?;
267 }
268 ("pre_push", "enabled") => {
269 self.pre_push.enabled = self.parse_bool(value, "pre_push.enabled")?;
270 }
271 ("pre_push", "timeout_seconds") => {
272 self.pre_push.timeout_seconds =
273 self.parse_u64(value, "pre_push.timeout_seconds")?;
274 }
275 ("publish", "enabled") => {
276 self.publish.enabled = self.parse_bool(value, "publish.enabled")?;
277 }
278 ("publish", "timeout_seconds") => {
279 self.publish.timeout_seconds = self.parse_u64(value, "publish.timeout_seconds")?;
280 }
281 _ => return Err(Error::config(format!("Unknown safety config key: {}", key))),
282 }
283 Ok(())
284 }
285
286 fn set_bypass_config(&mut self, key: &str, value: &str) -> Result<()> {
288 match key {
289 "bypass.enabled" => {
290 self.bypass.enabled = self.parse_bool(value, "bypass.enabled")?;
291 }
292 _ => return Err(Error::config(format!("Unknown safety config key: {}", key))),
293 }
294 Ok(())
295 }
296
297 fn parse_bool(&self, value: &str, field: &str) -> Result<bool> {
299 value
300 .parse()
301 .map_err(|_| Error::config(format!("Invalid boolean value for {}", field)))
302 }
303
304 fn parse_u64(&self, value: &str, field: &str) -> Result<u64> {
306 value
307 .parse()
308 .map_err(|_| Error::config(format!("Invalid number for {}", field)))
309 }
310
311 pub fn get(&self, key: &str) -> Option<String> {
313 match key {
314 "enabled" => Some(self.enabled.to_string()),
315 "strict_mode" => Some(self.strict_mode.to_string()),
316 "show_progress" => Some(self.show_progress.to_string()),
317 "parallel_checks" => Some(self.parallel_checks.to_string()),
318 "pre_commit.enabled" => Some(self.pre_commit.enabled.to_string()),
319 "pre_commit.timeout_seconds" => Some(self.pre_commit.timeout_seconds.to_string()),
320 "pre_push.enabled" => Some(self.pre_push.enabled.to_string()),
321 "pre_push.timeout_seconds" => Some(self.pre_push.timeout_seconds.to_string()),
322 "publish.enabled" => Some(self.publish.enabled.to_string()),
323 "publish.timeout_seconds" => Some(self.publish.timeout_seconds.to_string()),
324 "bypass.enabled" => Some(self.bypass.enabled.to_string()),
325 _ => None,
326 }
327 }
328}