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