1use crate::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11pub mod bypass;
12pub mod checks;
13pub mod config;
14pub mod pipeline;
16pub mod report;
17
18pub use config::SafetyConfig;
19pub use pipeline::SafetyPipeline;
20pub use report::{CheckResult, SafetyReport};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum PipelineStage {
25 PreCommit,
27 PrePush,
29 Publish,
31}
32
33impl PipelineStage {
34 pub fn name(&self) -> &'static str {
36 match self {
37 Self::PreCommit => "pre-commit",
38 Self::PrePush => "pre-push",
39 Self::Publish => "publish",
40 }
41 }
42
43 pub fn display_name(&self) -> &'static str {
45 match self {
46 Self::PreCommit => "Pre-Commit",
47 Self::PrePush => "Pre-Push",
48 Self::Publish => "Publish",
49 }
50 }
51
52 pub fn default_timeout(&self) -> Duration {
54 match self {
55 Self::PreCommit => Duration::from_secs(300), Self::PrePush => Duration::from_secs(600), Self::Publish => Duration::from_secs(900), }
59 }
60}
61
62impl std::str::FromStr for PipelineStage {
63 type Err = Error;
64
65 fn from_str(s: &str) -> Result<Self> {
66 match s.to_lowercase().as_str() {
67 "pre-commit" | "precommit" | "commit" => Ok(Self::PreCommit),
68 "pre-push" | "prepush" | "push" => Ok(Self::PrePush),
69 "publish" | "pub" => Ok(Self::Publish),
70 _ => Err(Error::parse(format!("Unknown pipeline stage: {}", s))),
71 }
72 }
73}
74
75impl std::fmt::Display for PipelineStage {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 write!(f, "{}", self.name())
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub enum CheckType {
84 Format,
86 Clippy,
88 Build,
90 Test,
92 Audit,
94 Doc,
96 PublishDryRun,
98 Standards,
100 DocCoverage,
102 License,
104 Semver,
106}
107
108impl CheckType {
109 pub fn name(&self) -> &'static str {
111 match self {
112 Self::Format => "format",
113 Self::Clippy => "clippy",
114 Self::Build => "build",
115 Self::Test => "test",
116 Self::Audit => "audit",
117 Self::Doc => "doc",
118 Self::PublishDryRun => "publish-dry-run",
119 Self::Standards => "standards",
120 Self::DocCoverage => "doc-coverage",
121 Self::License => "license",
122 Self::Semver => "semver",
123 }
124 }
125
126 pub fn display_name(&self) -> &'static str {
128 match self {
129 Self::Format => "Format Check",
130 Self::Clippy => "Clippy Check",
131 Self::Build => "Build Check",
132 Self::Test => "Test Check",
133 Self::Audit => "Security Audit",
134 Self::Doc => "Documentation Build",
135 Self::PublishDryRun => "Publish Dry Run",
136 Self::Standards => "Standards Check",
137 Self::DocCoverage => "Documentation Coverage",
138 Self::License => "License Check",
139 Self::Semver => "Semver Check",
140 }
141 }
142
143 pub fn for_stage(stage: PipelineStage) -> Vec<Self> {
145 match stage {
146 PipelineStage::PreCommit => {
147 vec![Self::Format, Self::Clippy, Self::Build, Self::Standards]
148 }
149 PipelineStage::PrePush => vec![
150 Self::Format,
151 Self::Clippy,
152 Self::Build,
153 Self::Standards,
154 Self::Test,
155 Self::Audit,
156 Self::Doc,
157 ],
158 PipelineStage::Publish => vec![
159 Self::Format,
160 Self::Clippy,
161 Self::Build,
162 Self::Standards,
163 Self::Test,
164 Self::Audit,
165 Self::Doc,
166 Self::PublishDryRun,
167 Self::DocCoverage,
168 Self::License,
169 Self::Semver,
170 ],
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
177pub enum SafetyResult {
178 Passed,
180 Blocked {
182 failures: Vec<String>,
184 suggestions: Vec<String>,
186 },
187 Bypassed {
189 reason: String,
191 user: String,
193 },
194}
195
196impl SafetyResult {
197 pub fn is_allowed(&self) -> bool {
199 matches!(self, Self::Passed | Self::Bypassed { .. })
200 }
201
202 pub fn message(&self) -> String {
204 match self {
205 Self::Passed => "🎉 All safety checks passed! Operation allowed.".to_string(),
206 Self::Blocked {
207 failures,
208 suggestions,
209 } => {
210 let mut msg = "🚨 Safety checks FAILED - operation blocked!\n\n".to_string();
211
212 if !failures.is_empty() {
213 msg.push_str("Failures:\n");
214 for failure in failures {
215 msg.push_str(&format!(" • {}\n", failure));
216 }
217 }
218
219 if !suggestions.is_empty() {
220 msg.push_str("\nSuggestions:\n");
221 for suggestion in suggestions {
222 msg.push_str(&format!(" • {}\n", suggestion));
223 }
224 }
225
226 msg
227 }
228 Self::Bypassed { reason, user } => {
229 format!(
230 "⚠️ Safety checks bypassed by {} - reason: {}",
231 user, reason
232 )
233 }
234 }
235 }
236}
237
238#[cfg(test)]
239#[allow(clippy::unwrap_used, clippy::expect_used)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_pipeline_stage_from_str() {
245 assert_eq!(
246 "pre-commit".parse::<PipelineStage>().unwrap(),
247 PipelineStage::PreCommit
248 );
249 assert_eq!(
250 "pre-push".parse::<PipelineStage>().unwrap(),
251 PipelineStage::PrePush
252 );
253 assert_eq!(
254 "publish".parse::<PipelineStage>().unwrap(),
255 PipelineStage::Publish
256 );
257 assert!("invalid".parse::<PipelineStage>().is_err());
258 }
259
260 #[test]
261 fn test_check_type_for_stage() {
262 let pre_commit_checks = CheckType::for_stage(PipelineStage::PreCommit);
263 assert!(pre_commit_checks.contains(&CheckType::Format));
264 assert!(pre_commit_checks.contains(&CheckType::Clippy));
265 assert!(!pre_commit_checks.contains(&CheckType::Test)); let publish_checks = CheckType::for_stage(PipelineStage::Publish);
268 assert!(publish_checks.contains(&CheckType::PublishDryRun));
269 assert!(publish_checks.contains(&CheckType::Semver));
270 }
271
272 #[test]
273 fn test_safety_result_is_allowed() {
274 assert!(SafetyResult::Passed.is_allowed());
275 assert!(SafetyResult::Bypassed {
276 reason: "test".to_string(),
277 user: "test".to_string()
278 }
279 .is_allowed());
280 assert!(!SafetyResult::Blocked {
281 failures: vec![],
282 suggestions: vec![]
283 }
284 .is_allowed());
285 }
286}