1use crate::OutputFormat;
20
21pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
23
24pub const MAX_YAML_SIZE: usize = 1024 * 1024;
26
27pub use quillmark_content::MAX_NESTING_DEPTH;
32
33pub use crate::document::limits::MAX_YAML_DEPTH;
35
36pub const MAX_CARD_COUNT: usize = 1000;
38
39pub const MAX_FIELD_COUNT: usize = 1000;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
47#[serde(rename_all = "lowercase")]
48pub enum Severity {
49 Error,
51 Warning,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct Location {
58 pub file: String,
60 pub line: u32,
62 pub column: u32,
64}
65
66#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct Diagnostic {
75 pub severity: Severity,
76 #[serde(skip_serializing_if = "Option::is_none", default)]
78 pub code: Option<String>,
79 pub message: String,
80 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub location: Option<Location>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
92 pub path: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none", default)]
94 pub hint: Option<String>,
95 #[serde(skip_serializing_if = "Vec::is_empty", default)]
97 pub source_chain: Vec<String>,
98}
99
100impl Diagnostic {
101 pub fn new(severity: Severity, message: String) -> Self {
102 Self {
103 severity,
104 code: None,
105 message,
106 location: None,
107 path: None,
108 hint: None,
109 source_chain: Vec::new(),
110 }
111 }
112
113 pub fn with_code(mut self, code: String) -> Self {
114 self.code = Some(code);
115 self
116 }
117
118 pub fn with_location(mut self, location: Location) -> Self {
119 self.location = Some(location);
120 self
121 }
122
123 pub fn with_path(mut self, path: String) -> Self {
127 self.path = Some(path);
128 self
129 }
130
131 pub fn with_hint(mut self, hint: String) -> Self {
132 self.hint = Some(hint);
133 self
134 }
135
136 pub fn with_source(mut self, source: &(dyn std::error::Error + 'static)) -> Self {
138 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(source);
139 while let Some(err) = current {
140 self.source_chain.push(err.to_string());
141 current = err.source();
142 }
143 self
144 }
145
146 pub fn fmt_pretty(&self) -> String {
147 let mut result = format!(
148 "[{}] {}",
149 match self.severity {
150 Severity::Error => "ERROR",
151 Severity::Warning => "WARN",
152 },
153 self.message
154 );
155
156 if let Some(ref code) = self.code {
157 result.push_str(&format!(" ({})", code));
158 }
159
160 if let Some(ref loc) = self.location {
161 result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.column));
162 }
163
164 if let Some(ref path) = self.path {
165 result.push_str(&format!("\n at {}", path));
166 }
167
168 if let Some(ref hint) = self.hint {
169 result.push_str(&format!("\n hint: {}", hint));
170 }
171
172 result
173 }
174
175}
176
177impl std::fmt::Display for Diagnostic {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 write!(f, "{}", self.message)
180 }
181}
182
183#[derive(thiserror::Error, Debug)]
184pub enum ParseError {
185 #[error("Input too large: {size} bytes (max: {max} bytes)")]
186 InputTooLarge { size: usize, max: usize },
187
188 #[error("Invalid YAML structure: {0}")]
189 InvalidStructure(String),
190
191 #[error("{0}")]
196 EmptyInput(String),
197
198 #[error("{0}")]
204 MissingQuill(String),
205
206 #[error("Invalid $quill reference '{value}': {reason}")]
210 InvalidQuillReference {
211 value: String,
212 reason: String,
214 },
215
216 #[error("{0}")]
220 BodyImport(String),
221
222 #[error("YAML error at line {line}: {message}")]
223 YamlErrorWithLocation {
224 message: String,
225 line: usize,
227 block_index: usize,
229 hint: Option<String>,
233 },
234}
235
236impl ParseError {
237 pub fn to_diagnostic(&self) -> Diagnostic {
238 match self {
239 ParseError::InputTooLarge { size, max } => Diagnostic::new(
240 Severity::Error,
241 format!("Input too large: {} bytes (max: {} bytes)", size, max),
242 )
243 .with_code("parse::input_too_large".to_string()),
244 ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
245 .with_code("parse::invalid_structure".to_string()),
246 ParseError::EmptyInput(msg) => Diagnostic::new(Severity::Error, msg.clone())
247 .with_code("parse::empty_input".to_string()),
248 ParseError::MissingQuill(msg) => Diagnostic::new(Severity::Error, msg.clone())
249 .with_code("parse::missing_quill".to_string()),
250 ParseError::BodyImport(msg) => Diagnostic::new(Severity::Error, msg.clone())
251 .with_code("parse::body_import".to_string()),
252 ParseError::InvalidQuillReference { value, reason } => Diagnostic::new(
253 Severity::Error,
254 format!("Invalid $quill reference '{}': {}", value, reason),
255 )
256 .with_code("parse::invalid_quill_reference".to_string())
257 .with_hint(crate::version::quill_ref_hint().to_string()),
258 ParseError::YamlErrorWithLocation {
259 message,
260 line,
261 block_index,
262 hint,
263 } => {
264 let mut d = Diagnostic::new(
265 Severity::Error,
266 format!(
267 "YAML error at line {} (block {}): {}",
268 line, block_index, message
269 ),
270 )
271 .with_code("parse::yaml_error_with_location".to_string());
272 if let Some(h) = hint {
273 d = d.with_hint(h.clone());
274 }
275 d
276 }
277 }
278 }
279}
280
281#[derive(Debug)]
291pub struct RenderError {
292 diags: Vec<Diagnostic>,
294}
295
296impl RenderError {
297 pub fn new(diags: Vec<Diagnostic>) -> Self {
303 debug_assert!(
304 !diags.is_empty(),
305 "RenderError requires at least one diagnostic"
306 );
307 Self { diags }
308 }
309
310 pub fn from_diag(diag: Diagnostic) -> Self {
312 Self { diags: vec![diag] }
313 }
314
315 pub fn diagnostics(&self) -> &[Diagnostic] {
318 &self.diags
319 }
320
321 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
323 self.diags
324 }
325
326 pub fn summary_message(diags: &[Diagnostic]) -> String {
333 match diags {
334 [d] => d.message.clone(),
335 [first, ..] => format!("{} error(s): {}", diags.len(), first.message),
336 [] => "render error".to_string(),
337 }
338 }
339}
340
341impl std::fmt::Display for RenderError {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 write!(f, "{}", Self::summary_message(&self.diags))
347 }
348}
349
350impl std::error::Error for RenderError {}
351
352impl From<ParseError> for RenderError {
353 fn from(err: ParseError) -> Self {
354 RenderError::from_diag(err.to_diagnostic())
355 }
356}
357
358#[derive(Debug)]
359pub struct RenderResult {
360 pub artifacts: Vec<crate::Artifact>,
361 pub warnings: Vec<Diagnostic>,
362 pub output_format: OutputFormat,
363 pub regions: Vec<crate::RenderedRegion>,
369}
370
371impl RenderResult {
372 pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
373 Self {
374 artifacts,
375 warnings: Vec::new(),
376 output_format,
377 regions: Vec::new(),
378 }
379 }
380}
381
382pub fn print_errors(err: &RenderError) {
383 for d in err.diagnostics() {
384 eprintln!("{}", d.fmt_pretty());
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_diagnostic_with_source_chain() {
394 let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
395 let diag =
396 Diagnostic::new(Severity::Error, "Rendering failed".to_string()).with_source(&root_err);
397
398 assert_eq!(diag.source_chain.len(), 1);
399 assert!(diag.source_chain[0].contains("File not found"));
400 }
401
402 #[test]
403 fn test_diagnostic_serialization() {
404 let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
405 .with_code("E001".to_string())
406 .with_location(Location {
407 file: "test.typ".to_string(),
408 line: 10,
409 column: 5,
410 });
411
412 let json = serde_json::to_string(&diag).unwrap();
413 assert!(json.contains("Test error"));
414 assert!(json.contains("E001"));
415 assert!(json.contains("\"severity\":\"error\""));
416 assert!(json.contains("\"column\":5"));
417 }
418
419 #[test]
420 fn test_render_error_single_diagnostic_shape() {
421 let err = RenderError::from_diag(Diagnostic::new(
422 Severity::Error,
423 "no such backend".to_string(),
424 ));
425 assert_eq!(err.diagnostics().len(), 1);
426 assert_eq!(err.to_string(), "no such backend");
427
428 let owned = err.into_diagnostics();
429 assert_eq!(owned.len(), 1);
430 assert_eq!(owned[0].message, "no such backend");
431 }
432
433 #[test]
434 fn test_render_error_display_aggregates_multi_diagnostic() {
435 let err = RenderError::new(vec![
436 Diagnostic::new(Severity::Error, "a".to_string()),
437 Diagnostic::new(Severity::Error, "b".to_string()),
438 ]);
439 assert_eq!(err.to_string(), "2 error(s): a");
440 }
441
442 #[test]
443 fn test_diagnostic_fmt_pretty() {
444 let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
445 .with_code("W001".to_string())
446 .with_location(Location {
447 file: "input.md".to_string(),
448 line: 5,
449 column: 10,
450 })
451 .with_hint("Use the new field name instead".to_string());
452
453 let output = diag.fmt_pretty();
454 assert!(output.contains("[WARN]"));
455 assert!(output.contains("Deprecated field used"));
456 assert!(output.contains("W001"));
457 assert!(output.contains("input.md:5:10"));
458 assert!(output.contains("hint:"));
459 }
460
461 #[test]
462 fn test_diagnostic_with_path() {
463 let diag = Diagnostic::new(Severity::Error, "Type mismatch".to_string())
464 .with_code("validation::type_mismatch".to_string())
465 .with_path("cards.indorsement[0].signature_block".to_string());
466
467 assert_eq!(
468 diag.path.as_deref(),
469 Some("cards.indorsement[0].signature_block")
470 );
471
472 let json = serde_json::to_string(&diag).unwrap();
473 assert!(json.contains("\"path\":\"cards.indorsement[0].signature_block\""));
474
475 let pretty = diag.fmt_pretty();
476 assert!(pretty.contains("at cards.indorsement[0].signature_block"));
477 }
478
479}