use crate::error::ParseError;
#[cfg(feature = "diagnostics")]
use ariadne::{Color, Label, Report, ReportKind, Source};
#[cfg(feature = "diagnostics")]
impl ParseError {
pub fn to_diagnostic(&self, filename: &str, source: &str) -> String {
let mut buf = Vec::new();
let primary_range = self.primary_byte_range(source);
let header_span = (filename, primary_range.clone());
let mut report =
Report::build(ReportKind::Error, header_span).with_message(format!("{}", self));
report = report.with_label(
Label::new((filename, primary_range))
.with_message(format!("{}", self.error))
.with_color(Color::Red),
);
report
.finish()
.write((filename, Source::from(source)), &mut buf)
.unwrap();
String::from_utf8_lossy(&buf).into_owned()
}
#[cfg(feature = "diagnostics")]
fn primary_byte_range(&self, source: &str) -> std::ops::Range<usize> {
if let Some(ref span) = self.span {
return span.start..span.end;
}
if let Some(line) = self.line {
let line_start: usize = source
.lines()
.take(line.saturating_sub(1))
.map(|l| l.len() + 1) .sum();
let line_len = source
.lines()
.nth(line.saturating_sub(1))
.map(|l| l.len())
.unwrap_or(0);
return line_start..line_start + line_len;
}
0..0
}
}
#[cfg(feature = "diagnostics")]
pub fn parse_with_diagnostics(
parser: &dyn crate::CitationParser,
input: &str,
filename: &str,
) -> Result<Vec<crate::Citation>, String> {
parser
.parse(input)
.map_err(|e| e.to_diagnostic(filename, input))
}
#[cfg(all(test, feature = "diagnostics"))]
mod tests {
use crate::{
CitationFormat,
error::{ParseError, SourceSpan, ValueError},
};
#[test]
fn test_to_diagnostic_with_span() {
let source = "TY - JOUR\nTI - Hello\nER -\n";
let err = ParseError::at_line(1, CitationFormat::Ris, ValueError::Syntax("oops".into()))
.with_span(SourceSpan::new(0, 10));
let diag = err.to_diagnostic("test.ris", source);
assert!(
diag.contains("test.ris"),
"filename should appear in output"
);
}
#[test]
fn test_to_diagnostic_line_only() {
let source = "TY - JOUR\nTI - Hello\nER -\n";
let err = ParseError::at_line(
2,
CitationFormat::Ris,
ValueError::MissingValue {
field: "title",
key: "TI",
},
);
let diag = err.to_diagnostic("test.ris", source);
assert!(diag.contains("test.ris"));
assert!(
diag.contains('2'),
"line 2 should appear somewhere in output"
);
}
#[test]
fn test_to_diagnostic_no_position() {
let source = "some content\n";
let err = ParseError::without_position(
CitationFormat::Ris,
ValueError::Syntax("bad input".into()),
);
let diag = err.to_diagnostic("test.ris", source);
assert!(diag.contains("test.ris"));
}
#[test]
fn test_to_diagnostic_contains_error_message() {
let source = "TY - JOUR\nER -\n";
let err = ParseError::at_line(
1,
CitationFormat::Ris,
ValueError::MissingValue {
field: "title",
key: "TI",
},
)
.with_span(SourceSpan::new(0, 10));
let diag = err.to_diagnostic("citations.ris", source);
assert!(
diag.contains("TI"),
"key 'TI' should appear in the diagnostic"
);
}
#[test]
fn test_ris_missing_title_diagnostic() {
use crate::{RisParser, parse_with_diagnostics};
let source = "TY - JOUR\nAU - Smith, John\nER -\n";
let result = parse_with_diagnostics(&RisParser::new(), source, "input.ris");
assert!(result.is_err(), "should fail: no title");
let diag = result.unwrap_err();
assert!(
diag.contains("input.ris"),
"filename should appear in output"
);
assert!(
diag.contains("TI"),
"missing-field key 'TI' should appear in output"
);
assert!(!diag.is_empty());
}
#[test]
fn test_pubmed_missing_title_diagnostic() {
use crate::{PubMedParser, parse_with_diagnostics};
let source = "PMID- 123\nAU - Smith J\n\n";
let result = parse_with_diagnostics(&PubMedParser::new(), source, "refs.nbib");
assert!(result.is_err());
let diag = result.unwrap_err();
assert!(diag.contains("refs.nbib"));
}
#[test]
fn test_csv_missing_title_diagnostic() {
use crate::{csv::CsvParser, parse_with_diagnostics};
let source = "Title,Author\n,Smith J";
let result = parse_with_diagnostics(&CsvParser::new(), source, "refs.csv");
assert!(result.is_err());
let diag = result.unwrap_err();
assert!(diag.contains("refs.csv"));
}
#[test]
fn test_valid_input_no_diagnostic() {
use crate::{RisParser, parse_with_diagnostics};
let source = "TY - JOUR\nTI - Good Paper\nER -\n";
let result = parse_with_diagnostics(&RisParser::new(), source, "good.ris");
assert!(result.is_ok(), "valid input should succeed");
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_diagnostic_contains_format_name() {
use crate::{RisParser, parse_with_diagnostics};
let source = "TY - JOUR\nAU - Smith\nER -\n";
let diag = parse_with_diagnostics(&RisParser::new(), source, "x.ris").unwrap_err();
assert!(
diag.contains("RIS"),
"format name should appear in the diagnostic"
);
}
}