1use core::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct DiagnosticLimits {
8 pub max_items: usize,
10 pub max_preview_bytes: usize,
12}
13
14impl Default for DiagnosticLimits {
15 fn default() -> Self {
16 Self {
17 max_items: 32,
18 max_preview_bytes: 256,
19 }
20 }
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct ByteSpan {
27 pub start: usize,
28 pub end: usize,
29}
30
31impl ByteSpan {
32 #[must_use]
33 pub const fn new(start: usize, end: usize) -> Self {
34 Self { start, end }
35 }
36}
37
38#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct TextPreview {
42 byte_len: usize,
43 text: String,
44 truncated: bool,
45}
46
47impl TextPreview {
48 fn new(text: &str, max_bytes: usize) -> Self {
49 let mut end = text.len().min(max_bytes);
50 while !text.is_char_boundary(end) {
51 end -= 1;
52 }
53 Self {
54 byte_len: text.len(),
55 text: text[..end].to_owned(),
56 truncated: end < text.len(),
57 }
58 }
59
60 #[must_use]
61 pub const fn byte_len(&self) -> usize {
62 self.byte_len
63 }
64
65 #[must_use]
66 pub fn text(&self) -> &str {
67 &self.text
68 }
69
70 #[must_use]
71 pub const fn is_truncated(&self) -> bool {
72 self.truncated
73 }
74}
75
76#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct MismatchDetails {
80 source_range: ByteSpan,
81 expected: TextPreview,
82 actual: TextPreview,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct ValidationReport {
89 diagnostics: Vec<EditError>,
90 total_diagnostics: usize,
91 truncated: bool,
92}
93
94impl ValidationReport {
95 pub(crate) fn new(diagnostics: Vec<EditError>, total_diagnostics: usize) -> Self {
96 Self {
97 truncated: diagnostics.len() < total_diagnostics,
98 diagnostics,
99 total_diagnostics,
100 }
101 }
102
103 #[must_use]
105 pub const fn is_valid(&self) -> bool {
106 self.total_diagnostics == 0
107 }
108
109 #[must_use]
111 pub fn diagnostics(&self) -> &[EditError] {
112 &self.diagnostics
113 }
114
115 #[must_use]
117 pub const fn total_diagnostics(&self) -> usize {
118 self.total_diagnostics
119 }
120
121 #[must_use]
123 pub const fn is_truncated(&self) -> bool {
124 self.truncated
125 }
126}
127
128impl MismatchDetails {
129 #[must_use]
130 pub const fn source_range(&self) -> ByteSpan {
131 self.source_range
132 }
133
134 #[must_use]
135 pub const fn expected(&self) -> &TextPreview {
136 &self.expected
137 }
138
139 #[must_use]
140 pub const fn actual(&self) -> &TextPreview {
141 &self.actual
142 }
143}
144
145#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
147#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
148pub enum ErrorCode {
149 SchemaMismatch,
150 InvalidPlan,
151 InvalidFile,
152 InvalidEdit,
153 InvalidPath,
154 UnprovenEdit,
155 PlanTooLarge,
156 PositionOutOfRange,
157 BeforeMismatch,
158 OverlappingEdits,
159 OutputTooLarge,
160 ValidationRejected,
161}
162
163impl ErrorCode {
164 #[must_use]
166 pub const fn as_str(self) -> &'static str {
167 match self {
168 Self::SchemaMismatch => "SCHEMA_MISMATCH",
169 Self::InvalidPlan => "INVALID_PLAN",
170 Self::InvalidFile => "INVALID_FILE",
171 Self::InvalidEdit => "INVALID_EDIT",
172 Self::InvalidPath => "INVALID_PATH",
173 Self::UnprovenEdit => "UNPROVEN_EDIT",
174 Self::PlanTooLarge => "PLAN_TOO_LARGE",
175 Self::PositionOutOfRange => "POSITION_OUT_OF_RANGE",
176 Self::BeforeMismatch => "BEFORE_MISMATCH",
177 Self::OverlappingEdits => "OVERLAPPING_EDITS",
178 Self::OutputTooLarge => "OUTPUT_TOO_LARGE",
179 Self::ValidationRejected => "VALIDATION_REJECTED",
180 }
181 }
182}
183
184impl fmt::Display for ErrorCode {
185 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186 formatter.write_str(self.as_str())
187 }
188}
189
190#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct EditError {
194 code: ErrorCode,
195 message: String,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 file_index: Option<usize>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 edit_index: Option<usize>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 related_edit_index: Option<usize>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 mismatch: Option<Box<MismatchDetails>>,
204}
205
206impl EditError {
207 pub(crate) fn new(code: ErrorCode, message: impl Into<String>) -> Self {
208 Self {
209 code,
210 message: message.into(),
211 file_index: None,
212 edit_index: None,
213 related_edit_index: None,
214 mismatch: None,
215 }
216 }
217
218 pub(crate) fn before_mismatch(
219 source_range: ByteSpan,
220 expected: &str,
221 actual: &str,
222 limits: DiagnosticLimits,
223 ) -> Self {
224 Self {
225 code: ErrorCode::BeforeMismatch,
226 message: "source text does not match the exact before guard".to_owned(),
227 file_index: None,
228 edit_index: None,
229 related_edit_index: None,
230 mismatch: Some(Box::new(MismatchDetails {
231 source_range,
232 expected: TextPreview::new(expected, limits.max_preview_bytes),
233 actual: TextPreview::new(actual, limits.max_preview_bytes),
234 })),
235 }
236 }
237
238 pub(crate) const fn at_file(mut self, file_index: usize) -> Self {
239 self.file_index = Some(file_index);
240 self
241 }
242
243 pub(crate) const fn at_edit(mut self, edit_index: usize) -> Self {
244 self.edit_index = Some(edit_index);
245 self
246 }
247
248 pub(crate) const fn with_related_edit(mut self, edit_index: usize) -> Self {
249 self.related_edit_index = Some(edit_index);
250 self
251 }
252
253 #[must_use]
255 pub const fn code(&self) -> ErrorCode {
256 self.code
257 }
258
259 #[must_use]
261 pub fn message(&self) -> &str {
262 &self.message
263 }
264
265 #[must_use]
267 pub const fn file_index(&self) -> Option<usize> {
268 self.file_index
269 }
270
271 #[must_use]
273 pub const fn edit_index(&self) -> Option<usize> {
274 self.edit_index
275 }
276
277 #[must_use]
279 pub const fn related_edit_index(&self) -> Option<usize> {
280 self.related_edit_index
281 }
282
283 #[must_use]
285 pub fn mismatch(&self) -> Option<&MismatchDetails> {
286 self.mismatch.as_deref()
287 }
288}
289
290impl fmt::Display for EditError {
291 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
292 write!(formatter, "{}: {}", self.code, self.message)
293 }
294}
295
296impl std::error::Error for EditError {}