1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Severity {
7 Error,
8 Warning,
9}
10
11#[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#[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}