1use crate::{Error, Result};
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11pub mod bypass;
13pub mod checks;
15pub mod config;
17pub mod execution;
19pub mod pipeline;
22pub mod report;
24
25pub use config::SafetyConfig;
26pub use pipeline::SafetyPipeline;
27pub use report::{CheckResult, SafetyReport};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum PipelineStage {
32 PreCommit,
34 PrePush,
36 Publish,
38}
39
40impl PipelineStage {
41 pub fn name(&self) -> &'static str {
43 match self {
44 Self::PreCommit => "pre-commit",
45 Self::PrePush => "pre-push",
46 Self::Publish => "publish",
47 }
48 }
49
50 pub fn display_name(&self) -> &'static str {
52 match self {
53 Self::PreCommit => "Pre-Commit",
54 Self::PrePush => "Pre-Push",
55 Self::Publish => "Publish",
56 }
57 }
58
59 pub fn default_timeout(&self) -> Duration {
61 match self {
62 Self::PreCommit => Duration::from_secs(300), Self::PrePush => Duration::from_secs(600), Self::Publish => Duration::from_secs(900), }
66 }
67}
68
69impl std::str::FromStr for PipelineStage {
70 type Err = Error;
71
72 fn from_str(s: &str) -> Result<Self> {
73 match s.to_lowercase().as_str() {
74 "pre-commit" | "precommit" | "commit" => Ok(Self::PreCommit),
75 "pre-push" | "prepush" | "push" => Ok(Self::PrePush),
76 "publish" | "pub" => Ok(Self::Publish),
77 _ => Err(Error::parse(format!("Unknown pipeline stage: {}", s))),
78 }
79 }
80}
81
82impl std::fmt::Display for PipelineStage {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "{}", self.name())
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub enum CheckType {
91 Format,
93 Clippy,
95 Build,
97 Test,
99 Audit,
101 Doc,
103 PublishDryRun,
105 Standards,
107 DocCoverage,
109 License,
111 Semver,
113}
114
115impl CheckType {
116 pub fn name(&self) -> &'static str {
118 match self {
119 Self::Format => "format",
120 Self::Clippy => "clippy",
121 Self::Build => "build",
122 Self::Test => "test",
123 Self::Audit => "audit",
124 Self::Doc => "doc",
125 Self::PublishDryRun => "publish-dry-run",
126 Self::Standards => "standards",
127 Self::DocCoverage => "doc-coverage",
128 Self::License => "license",
129 Self::Semver => "semver",
130 }
131 }
132
133 pub fn display_name(&self) -> &'static str {
135 match self {
136 Self::Format => "Format Check",
137 Self::Clippy => "Clippy Check",
138 Self::Build => "Build Check",
139 Self::Test => "Test Check",
140 Self::Audit => "Security Audit",
141 Self::Doc => "Documentation Build",
142 Self::PublishDryRun => "Publish Dry Run",
143 Self::Standards => "Standards Check",
144 Self::DocCoverage => "Documentation Coverage",
145 Self::License => "License Check",
146 Self::Semver => "Semver Check",
147 }
148 }
149
150 pub fn for_stage(stage: PipelineStage) -> Vec<Self> {
152 match stage {
153 PipelineStage::PreCommit => Self::pre_commit_checks(),
154 PipelineStage::PrePush => Self::pre_push_checks(),
155 PipelineStage::Publish => Self::publish_checks(),
156 }
157 }
158
159 fn pre_commit_checks() -> Vec<Self> {
161 vec![Self::Format, Self::Clippy, Self::Build, Self::Standards]
162 }
163
164 fn pre_push_checks() -> Vec<Self> {
166 vec![
167 Self::Format,
168 Self::Clippy,
169 Self::Build,
170 Self::Standards,
171 Self::Test,
172 Self::Audit,
173 Self::Doc,
174 ]
175 }
176
177 fn publish_checks() -> Vec<Self> {
179 vec![
180 Self::Format,
181 Self::Clippy,
182 Self::Build,
183 Self::Standards,
184 Self::Test,
185 Self::Audit,
186 Self::Doc,
187 Self::PublishDryRun,
188 Self::DocCoverage,
189 Self::License,
190 Self::Semver,
191 ]
192 }
193}
194
195#[derive(Debug, Clone)]
197pub enum SafetyResult {
198 Passed,
200 Blocked {
202 failures: Vec<String>,
204 suggestions: Vec<String>,
206 },
207 Bypassed {
209 reason: String,
211 user: String,
213 },
214}
215
216impl SafetyResult {
217 pub fn is_allowed(&self) -> bool {
219 matches!(self, Self::Passed | Self::Bypassed { .. })
220 }
221
222 pub fn message(&self) -> String {
224 match self {
225 Self::Passed => "🎉 All safety checks passed! Operation allowed.".to_string(),
226 Self::Blocked {
227 failures,
228 suggestions,
229 } => {
230 let mut msg = "🚨 Safety checks FAILED - operation blocked!\n\n".to_string();
231
232 if !failures.is_empty() {
233 msg.push_str("Failures:\n");
234 for failure in failures {
235 msg.push_str(&format!(" • {}\n", failure));
236 }
237 }
238
239 if !suggestions.is_empty() {
240 msg.push_str("\nSuggestions:\n");
241 for suggestion in suggestions {
242 msg.push_str(&format!(" • {}\n", suggestion));
243 }
244 }
245
246 msg
247 }
248 Self::Bypassed { reason, user } => {
249 format!(
250 "⚠️ Safety checks bypassed by {} - reason: {}",
251 user, reason
252 )
253 }
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests;