1use std::fmt::{Display, Formatter};
4
5use camino::Utf8PathBuf;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "kebab-case")]
12pub enum Severity {
13 Info,
15 Warning,
17 Error,
19}
20
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct SourceSpan {
25 pub line: usize,
27 pub column: usize,
29}
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct DiagnosticSource {
35 pub path: Box<str>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub span: Option<SourceSpan>,
40}
41
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct Diagnostic {
46 pub code: Box<str>,
48 pub severity: Severity,
50 pub message: Box<str>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub source: Option<Box<DiagnosticSource>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub project: Option<Box<str>>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub workspace: Option<Box<str>>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub help: Option<Box<str>>,
64}
65
66impl Diagnostic {
67 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
69 Self {
70 code: code.into().into_boxed_str(),
71 severity: Severity::Error,
72 message: message.into().into_boxed_str(),
73 source: None,
74 project: None,
75 workspace: None,
76 help: None,
77 }
78 }
79
80 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
82 Self {
83 code: code.into().into_boxed_str(),
84 severity: Severity::Warning,
85 message: message.into().into_boxed_str(),
86 source: None,
87 project: None,
88 workspace: None,
89 help: None,
90 }
91 }
92
93 #[must_use]
95 pub fn with_path(mut self, path: impl Into<String>) -> Self {
96 self.source = Some(Box::new(DiagnosticSource {
97 path: path.into().into_boxed_str(),
98 span: None,
99 }));
100 self
101 }
102
103 #[must_use]
105 pub fn with_project(mut self, project: impl Into<String>) -> Self {
106 self.project = Some(project.into().into_boxed_str());
107 self
108 }
109
110 #[must_use]
112 pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
113 self.workspace = Some(workspace.into().into_boxed_str());
114 self
115 }
116
117 #[must_use]
119 pub fn with_help(mut self, help: impl Into<String>) -> Self {
120 self.help = Some(help.into().into_boxed_str());
121 self
122 }
123}
124
125impl Display for Diagnostic {
126 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127 if let Some(source) = &self.source {
128 write!(
129 f,
130 "{} [{}] {}: {}",
131 self.code,
132 severity_label(&self.severity),
133 source.path,
134 self.message
135 )
136 } else {
137 write!(
138 f,
139 "{} [{}] {}",
140 self.code,
141 severity_label(&self.severity),
142 self.message
143 )
144 }
145 }
146}
147
148#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct ValidationReport {
152 pub diagnostics: Vec<Diagnostic>,
154}
155
156impl ValidationReport {
157 pub fn new(diagnostics: Vec<Diagnostic>) -> Self {
159 Self { diagnostics }
160 }
161
162 pub fn is_success(&self) -> bool {
164 !self
165 .diagnostics
166 .iter()
167 .any(|diagnostic| diagnostic.severity == Severity::Error)
168 }
169
170 pub fn push(&mut self, diagnostic: Diagnostic) {
172 self.diagnostics.push(diagnostic);
173 }
174}
175
176#[derive(Debug, Error)]
178pub enum RepoctlError {
179 #[error("{diagnostic}")]
181 Diagnostic {
182 diagnostic: Box<Diagnostic>,
184 },
185 #[error("repoctl produced {} diagnostics", diagnostics.len())]
187 Diagnostics {
188 diagnostics: Vec<Diagnostic>,
190 },
191 #[error("I/O error at {path}: {source}")]
193 Io {
194 path: Utf8PathBuf,
196 #[source]
198 source: std::io::Error,
199 },
200 #[error("environment error: {0}")]
202 Environment(String),
203 #[error("internal error: {0}")]
205 Internal(String),
206}
207
208impl RepoctlError {
209 pub fn diagnostic(diagnostic: Diagnostic) -> Self {
211 Self::Diagnostic {
212 diagnostic: Box::new(diagnostic),
213 }
214 }
215
216 pub fn io(path: impl Into<Utf8PathBuf>, source: std::io::Error) -> Self {
218 Self::Io {
219 path: path.into(),
220 source,
221 }
222 }
223
224 pub fn diagnostics(&self) -> Vec<Diagnostic> {
226 match self {
227 Self::Diagnostic { diagnostic } => vec![(**diagnostic).clone()],
228 Self::Diagnostics { diagnostics } => diagnostics.clone(),
229 Self::Io { path, source } => vec![Diagnostic::error(
230 "repoctl.io",
231 format!("I/O error at {path}: {source}"),
232 )],
233 Self::Environment(message) => {
234 vec![Diagnostic::error("repoctl.environment", message.clone())]
235 }
236 Self::Internal(message) => vec![Diagnostic::error("repoctl.internal", message.clone())],
237 }
238 }
239}
240
241fn severity_label(severity: &Severity) -> &'static str {
242 match severity {
243 Severity::Info => "info",
244 Severity::Warning => "warning",
245 Severity::Error => "error",
246 }
247}