use crate::OutputFormat;
pub const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024;
pub const MAX_YAML_SIZE: usize = 1024 * 1024;
pub const MAX_NESTING_DEPTH: usize = 100;
pub const MAX_YAML_DEPTH: usize = 100;
pub const MAX_CARD_COUNT: usize = 1000;
pub const MAX_FIELD_COUNT: usize = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Severity {
Error,
Warning,
Note,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Location {
pub file: String,
pub line: u32,
pub col: u32,
}
#[derive(Debug, serde::Serialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: Option<String>,
pub message: String,
pub primary: Option<Location>,
pub hint: Option<String>,
#[serde(skip)]
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl Diagnostic {
pub fn new(severity: Severity, message: String) -> Self {
Self {
severity,
code: None,
message,
primary: None,
hint: None,
source: None,
}
}
pub fn with_code(mut self, code: String) -> Self {
self.code = Some(code);
self
}
pub fn with_location(mut self, location: Location) -> Self {
self.primary = Some(location);
self
}
pub fn with_hint(mut self, hint: String) -> Self {
self.hint = Some(hint);
self
}
pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
self.source = Some(source);
self
}
pub fn source_chain(&self) -> Vec<String> {
let mut chain = Vec::new();
let mut current_source = self
.source
.as_ref()
.map(|b| b.as_ref() as &dyn std::error::Error);
while let Some(err) = current_source {
chain.push(err.to_string());
current_source = err.source();
}
chain
}
pub fn fmt_pretty(&self) -> String {
let mut result = format!(
"[{}] {}",
match self.severity {
Severity::Error => "ERROR",
Severity::Warning => "WARN",
Severity::Note => "NOTE",
},
self.message
);
if let Some(ref code) = self.code {
result.push_str(&format!(" ({})", code));
}
if let Some(ref loc) = self.primary {
result.push_str(&format!("\n --> {}:{}:{}", loc.file, loc.line, loc.col));
}
if let Some(ref hint) = self.hint {
result.push_str(&format!("\n hint: {}", hint));
}
result
}
pub fn fmt_pretty_with_source(&self) -> String {
let mut result = self.fmt_pretty();
for (i, cause) in self.source_chain().iter().enumerate() {
result.push_str(&format!("\n cause {}: {}", i + 1, cause));
}
result
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SerializableDiagnostic {
pub severity: Severity,
pub code: Option<String>,
pub message: String,
pub primary: Option<Location>,
pub hint: Option<String>,
pub source_chain: Vec<String>,
}
impl From<Diagnostic> for SerializableDiagnostic {
fn from(diag: Diagnostic) -> Self {
let source_chain = diag.source_chain();
Self {
severity: diag.severity,
code: diag.code,
message: diag.message,
primary: diag.primary,
hint: diag.hint,
source_chain,
}
}
}
impl From<&Diagnostic> for SerializableDiagnostic {
fn from(diag: &Diagnostic) -> Self {
Self {
severity: diag.severity,
code: diag.code.clone(),
message: diag.message.clone(),
primary: diag.primary.clone(),
hint: diag.hint.clone(),
source_chain: diag.source_chain(),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ParseError {
#[error("Input too large: {size} bytes (max: {max} bytes)")]
InputTooLarge {
size: usize,
max: usize,
},
#[error("YAML parsing error: {0}")]
YamlError(#[from] serde_saphyr::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Invalid YAML structure: {0}")]
InvalidStructure(String),
#[error("{}", .diag.message)]
MissingCardDirective {
diag: Box<Diagnostic>,
},
#[error("YAML error at line {line}: {message}")]
YamlErrorWithLocation {
message: String,
line: usize,
block_index: usize,
},
#[error("{0}")]
Other(String),
}
impl ParseError {
pub fn missing_card_directive() -> Self {
let diag = Diagnostic::new(
Severity::Error,
"Inline metadata block missing CARD directive".to_string(),
)
.with_code("parse::missing_card".to_string())
.with_hint(
"Add 'CARD: <card_type>' to specify which card this block belongs to. \
Example:\n---\nCARD: my_card_type\nfield: value\n---"
.to_string(),
);
ParseError::MissingCardDirective {
diag: Box::new(diag),
}
}
pub fn to_diagnostic(&self) -> Diagnostic {
match self {
ParseError::MissingCardDirective { diag } => Diagnostic {
severity: diag.severity,
code: diag.code.clone(),
message: diag.message.clone(),
primary: diag.primary.clone(),
hint: diag.hint.clone(),
source: None, },
ParseError::InputTooLarge { size, max } => Diagnostic::new(
Severity::Error,
format!("Input too large: {} bytes (max: {} bytes)", size, max),
)
.with_code("parse::input_too_large".to_string()),
ParseError::YamlError(e) => {
Diagnostic::new(Severity::Error, format!("YAML parsing error: {}", e))
.with_code("parse::yaml_error".to_string())
} ParseError::JsonError(e) => {
Diagnostic::new(Severity::Error, format!("JSON conversion error: {}", e))
.with_code("parse::json_error".to_string())
}
ParseError::InvalidStructure(msg) => Diagnostic::new(Severity::Error, msg.clone())
.with_code("parse::invalid_structure".to_string()),
ParseError::YamlErrorWithLocation {
message,
line,
block_index,
} => Diagnostic::new(
Severity::Error,
format!(
"YAML error at line {} (block {}): {}",
line, block_index, message
),
)
.with_code("parse::yaml_error_with_location".to_string()),
ParseError::Other(msg) => Diagnostic::new(Severity::Error, msg.clone()),
}
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for ParseError {
fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
ParseError::Other(err.to_string())
}
}
impl From<String> for ParseError {
fn from(msg: String) -> Self {
ParseError::Other(msg)
}
}
impl From<&str> for ParseError {
fn from(msg: &str) -> Self {
ParseError::Other(msg.to_string())
}
}
#[derive(thiserror::Error, Debug)]
pub enum RenderError {
#[error("{diag}")]
EngineCreation {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
InvalidFrontmatter {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
TemplateFailed {
diag: Box<Diagnostic>,
},
#[error("Backend compilation failed with {} error(s)", diags.len())]
CompilationFailed {
diags: Vec<Diagnostic>,
},
#[error("{diag}")]
FormatNotSupported {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
UnsupportedBackend {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
DynamicAssetCollision {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
DynamicFontCollision {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
InputTooLarge {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
YamlTooLarge {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
NestingTooDeep {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
ValidationFailed {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
InvalidSchema {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
QuillConfig {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
VersionNotFound {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
QuillNotFound {
diag: Box<Diagnostic>,
},
#[error("{diag}")]
InvalidVersion {
diag: Box<Diagnostic>,
},
}
impl RenderError {
pub fn diagnostics(&self) -> Vec<&Diagnostic> {
match self {
RenderError::CompilationFailed { diags } => diags.iter().collect(),
RenderError::EngineCreation { diag }
| RenderError::InvalidFrontmatter { diag }
| RenderError::TemplateFailed { diag }
| RenderError::FormatNotSupported { diag }
| RenderError::UnsupportedBackend { diag }
| RenderError::DynamicAssetCollision { diag }
| RenderError::DynamicFontCollision { diag }
| RenderError::InputTooLarge { diag }
| RenderError::YamlTooLarge { diag }
| RenderError::NestingTooDeep { diag }
| RenderError::ValidationFailed { diag }
| RenderError::InvalidSchema { diag }
| RenderError::QuillConfig { diag }
| RenderError::VersionNotFound { diag }
| RenderError::QuillNotFound { diag }
| RenderError::InvalidVersion { diag } => vec![diag.as_ref()],
}
}
}
impl From<ParseError> for RenderError {
fn from(err: ParseError) -> Self {
RenderError::InvalidFrontmatter {
diag: Box::new(
Diagnostic::new(Severity::Error, err.to_string())
.with_code("parse::error".to_string()),
),
}
}
}
#[derive(Debug)]
pub struct RenderResult {
pub artifacts: Vec<crate::Artifact>,
pub warnings: Vec<Diagnostic>,
pub output_format: OutputFormat,
}
impl RenderResult {
pub fn new(artifacts: Vec<crate::Artifact>, output_format: OutputFormat) -> Self {
Self {
artifacts,
warnings: Vec::new(),
output_format,
}
}
pub fn with_warning(mut self, warning: Diagnostic) -> Self {
self.warnings.push(warning);
self
}
}
pub fn print_errors(err: &RenderError) {
match err {
RenderError::CompilationFailed { diags } => {
for d in diags {
eprintln!("{}", d.fmt_pretty());
}
}
RenderError::TemplateFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::InvalidFrontmatter { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::EngineCreation { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::FormatNotSupported { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::UnsupportedBackend { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::DynamicAssetCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::DynamicFontCollision { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::InputTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::YamlTooLarge { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::NestingTooDeep { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::ValidationFailed { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::InvalidSchema { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::QuillConfig { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::VersionNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::QuillNotFound { diag } => eprintln!("{}", diag.fmt_pretty()),
RenderError::InvalidVersion { diag } => eprintln!("{}", diag.fmt_pretty()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_with_source_chain() {
let root_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let diag = Diagnostic::new(Severity::Error, "Rendering failed".to_string())
.with_source(Box::new(root_err));
let chain = diag.source_chain();
assert_eq!(chain.len(), 1);
assert!(chain[0].contains("File not found"));
}
#[test]
fn test_diagnostic_serialization() {
let diag = Diagnostic::new(Severity::Error, "Test error".to_string())
.with_code("E001".to_string())
.with_location(Location {
file: "test.typ".to_string(),
line: 10,
col: 5,
});
let serializable: SerializableDiagnostic = diag.into();
let json = serde_json::to_string(&serializable).unwrap();
assert!(json.contains("Test error"));
assert!(json.contains("E001"));
}
#[test]
fn test_render_error_diagnostics_extraction() {
let diag1 = Diagnostic::new(Severity::Error, "Error 1".to_string());
let diag2 = Diagnostic::new(Severity::Error, "Error 2".to_string());
let err = RenderError::CompilationFailed {
diags: vec![diag1, diag2],
};
let diags = err.diagnostics();
assert_eq!(diags.len(), 2);
}
#[test]
fn test_diagnostic_fmt_pretty() {
let diag = Diagnostic::new(Severity::Warning, "Deprecated field used".to_string())
.with_code("W001".to_string())
.with_location(Location {
file: "input.md".to_string(),
line: 5,
col: 10,
})
.with_hint("Use the new field name instead".to_string());
let output = diag.fmt_pretty();
assert!(output.contains("[WARN]"));
assert!(output.contains("Deprecated field used"));
assert!(output.contains("W001"));
assert!(output.contains("input.md:5:10"));
assert!(output.contains("hint:"));
}
#[test]
fn test_diagnostic_fmt_pretty_with_source() {
let root_err = std::io::Error::other("Underlying error");
let diag = Diagnostic::new(Severity::Error, "Top-level error".to_string())
.with_code("E002".to_string())
.with_source(Box::new(root_err));
let output = diag.fmt_pretty_with_source();
assert!(output.contains("[ERROR]"));
assert!(output.contains("Top-level error"));
assert!(output.contains("cause 1:"));
assert!(output.contains("Underlying error"));
}
#[test]
fn test_render_result_with_warnings() {
let artifacts = vec![];
let warning = Diagnostic::new(Severity::Warning, "Test warning".to_string());
let result = RenderResult::new(artifacts, OutputFormat::Pdf).with_warning(warning);
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0].message, "Test warning");
}
}