Skip to main content

souk_core/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Severity of a validation diagnostic.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Severity {
7    Error,
8    Warning,
9}
10
11/// A single validation finding.
12#[derive(Debug, Clone)]
13pub struct ValidationDiagnostic {
14    pub severity: Severity,
15    pub message: String,
16    pub path: Option<PathBuf>,
17    pub field: Option<String>,
18}
19
20impl ValidationDiagnostic {
21    pub fn error(message: impl Into<String>) -> Self {
22        Self {
23            severity: Severity::Error,
24            message: message.into(),
25            path: None,
26            field: None,
27        }
28    }
29
30    pub fn warning(message: impl Into<String>) -> Self {
31        Self {
32            severity: Severity::Warning,
33            message: message.into(),
34            path: None,
35            field: None,
36        }
37    }
38
39    pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
40        self.path = Some(path.into());
41        self
42    }
43
44    pub fn with_field(mut self, field: impl Into<String>) -> Self {
45        self.field = Some(field.into());
46        self
47    }
48
49    pub fn is_error(&self) -> bool {
50        self.severity == Severity::Error
51    }
52}
53
54/// The result of validating a plugin or marketplace.
55#[derive(Debug)]
56pub struct ValidationResult {
57    pub diagnostics: Vec<ValidationDiagnostic>,
58}
59
60impl ValidationResult {
61    pub fn new() -> Self {
62        Self {
63            diagnostics: Vec::new(),
64        }
65    }
66
67    pub fn push(&mut self, diagnostic: ValidationDiagnostic) {
68        self.diagnostics.push(diagnostic);
69    }
70
71    pub fn has_errors(&self) -> bool {
72        self.diagnostics.iter().any(|d| d.is_error())
73    }
74
75    pub fn error_count(&self) -> usize {
76        self.diagnostics.iter().filter(|d| d.is_error()).count()
77    }
78
79    pub fn warning_count(&self) -> usize {
80        self.diagnostics
81            .iter()
82            .filter(|d| d.severity == Severity::Warning)
83            .count()
84    }
85
86    pub fn merge(&mut self, other: ValidationResult) {
87        self.diagnostics.extend(other.diagnostics);
88    }
89}
90
91impl Default for ValidationResult {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[derive(Debug, Error)]
98pub enum SoukError {
99    #[error("Plugin not found: {0}")]
100    PluginNotFound(String),
101
102    #[error("Skill not found: {skill} in plugin {plugin}")]
103    SkillNotFound { plugin: String, skill: String },
104
105    #[error("Marketplace not found: searched upward from {0}")]
106    MarketplaceNotFound(PathBuf),
107
108    #[error("Marketplace already exists at {0}")]
109    MarketplaceAlreadyExists(PathBuf),
110
111    #[error("Plugin already exists in marketplace: {0}")]
112    PluginAlreadyExists(String),
113
114    #[error("Validation failed with {0} error(s)")]
115    ValidationFailed(usize),
116
117    #[error("Atomic operation failed, backup restored: {0}")]
118    AtomicRollback(String),
119
120    #[error("No LLM API key found. Set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY")]
121    NoApiKey,
122
123    #[error("LLM API error: {0}")]
124    LlmApiError(String),
125
126    #[error("IO error: {0}")]
127    Io(#[from] std::io::Error),
128
129    #[error("JSON error: {0}")]
130    Json(#[from] serde_json::Error),
131
132    #[error("Semver error: {0}")]
133    Semver(#[from] semver::Error),
134
135    #[error("{0}")]
136    Other(String),
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn validation_result_tracks_errors_and_warnings() {
145        let mut result = ValidationResult::new();
146        result.push(ValidationDiagnostic::error("bad thing"));
147        result.push(ValidationDiagnostic::warning("meh thing"));
148        result.push(ValidationDiagnostic::error("another bad"));
149
150        assert!(result.has_errors());
151        assert_eq!(result.error_count(), 2);
152        assert_eq!(result.warning_count(), 1);
153    }
154
155    #[test]
156    fn validation_result_merge() {
157        let mut a = ValidationResult::new();
158        a.push(ValidationDiagnostic::error("a"));
159
160        let mut b = ValidationResult::new();
161        b.push(ValidationDiagnostic::warning("b"));
162
163        a.merge(b);
164        assert_eq!(a.diagnostics.len(), 2);
165    }
166
167    #[test]
168    fn diagnostic_builder_pattern() {
169        let d = ValidationDiagnostic::error("missing name")
170            .with_path("/tmp/plugin")
171            .with_field("name");
172
173        assert!(d.is_error());
174        assert_eq!(d.path.unwrap().to_str().unwrap(), "/tmp/plugin");
175        assert_eq!(d.field.unwrap(), "name");
176    }
177}