use crate::error::{Error, ParseError};
use fyaml_sys::*;
use std::ffi::CStr;
use std::os::raw::{c_char, c_void};
use std::ptr;
unsafe extern "C" fn silent_output(
_diag: *mut fy_diag,
_user: *mut c_void,
_buf: *const c_char,
_len: usize,
) {
}
pub(crate) struct Diag {
ptr: *mut fy_diag,
}
impl Diag {
pub fn new() -> Option<Self> {
let cfg = fy_diag_cfg {
fp: ptr::null_mut(),
output_fn: Some(silent_output), user: ptr::null_mut(),
level: FYET_ERROR,
module_mask: u32::MAX, _bitfield_align_1: [],
_bitfield_1: fy_diag_cfg::new_bitfield_1(
false, false, false, false, false, ),
source_width: 0,
position_width: 0,
type_width: 0,
module_width: 0,
};
let ptr = unsafe { fy_diag_create(&cfg) };
if ptr.is_null() {
return None;
}
unsafe { fy_diag_set_collect_errors(ptr, true) };
Some(Self { ptr })
}
pub fn as_ptr(&self) -> *mut fy_diag {
self.ptr
}
pub fn first_error(&self) -> Option<ParseError> {
let mut prev: *mut std::ffi::c_void = ptr::null_mut();
let err = unsafe { fy_diag_errors_iterate(self.ptr, &mut prev) };
if err.is_null() {
None
} else {
Some(unsafe { parse_error_from_diag_error(&*err) })
}
}
pub fn first_error_or(&self, fallback_msg: &'static str) -> Error {
self.first_error()
.map(Error::ParseError)
.unwrap_or(Error::Parse(fallback_msg))
}
#[allow(dead_code)]
pub fn collect_errors(&self) -> Vec<ParseError> {
let mut errors = Vec::new();
let mut prev: *mut std::ffi::c_void = ptr::null_mut();
loop {
let err = unsafe { fy_diag_errors_iterate(self.ptr, &mut prev) };
if err.is_null() {
break;
}
let parse_err = unsafe { parse_error_from_diag_error(&*err) };
errors.push(parse_err);
}
errors
}
}
impl Drop for Diag {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { fy_diag_unref(self.ptr) };
}
}
}
pub(crate) fn diag_error(diag: Option<Diag>, fallback_msg: &'static str) -> Error {
diag.map(|d| d.first_error_or(fallback_msg))
.unwrap_or(Error::Parse(fallback_msg))
}
unsafe fn parse_error_from_diag_error(err: &fy_diag_error) -> ParseError {
let message = if err.msg.is_null() {
"unknown error".to_string()
} else {
CStr::from_ptr(err.msg).to_string_lossy().into_owned()
};
let line = if err.line >= 0 {
Some((err.line + 1) as u32)
} else {
None
};
let column = if err.column >= 0 {
Some((err.column + 1) as u32)
} else {
None
};
ParseError {
message,
line,
column,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Document;
#[test]
fn test_diag_creation() {
let diag = Diag::new();
assert!(diag.is_some());
}
#[test]
fn test_diag_collect_empty() {
let diag = Diag::new().unwrap();
let errors = diag.collect_errors();
assert!(errors.is_empty());
}
#[test]
fn test_parse_error_has_location() {
let result = Document::parse_str("[unclosed");
assert!(result.is_err());
let err = result.unwrap_err();
if let Error::ParseError(pe) = err {
assert!(pe.line().is_some(), "Expected line number");
assert!(pe.column().is_some(), "Expected column number");
assert!(!pe.message().is_empty(), "Expected non-empty error message");
} else {
panic!("Expected ParseError variant, got: {:?}", err);
}
}
#[test]
fn test_parse_error_location_tuple() {
let result = Document::parse_str("[unclosed");
let err = result.unwrap_err();
if let Error::ParseError(pe) = &err {
let loc = pe.location();
assert!(loc.is_some(), "Expected location tuple");
let (line, col) = loc.unwrap();
assert!(line >= 1, "Line should be 1-based");
assert!(col >= 1, "Column should be 1-based");
}
}
#[test]
fn test_parse_error_display() {
let result = Document::parse_str("key: [unclosed");
let err = result.unwrap_err();
let display = format!("{}", err);
assert!(
display.contains("Parse error"),
"Display should include 'Parse error'"
);
}
#[test]
fn test_multiline_parse_error_location() {
let yaml = "key: value\nlist:\n - [unclosed";
let result = Document::parse_str(yaml);
assert!(result.is_err());
let err = result.unwrap_err();
if let Error::ParseError(pe) = err {
assert!(
pe.line().is_some(),
"Expected line number for multiline error"
);
assert!(
pe.line().unwrap() > 1,
"Error should be after line 1, got: {:?}",
pe.line()
);
}
}
#[test]
fn test_multiple_errors_collection() {
let diag = Diag::new().unwrap();
let errors = diag.collect_errors();
assert!(errors.is_empty());
}
#[test]
fn test_unicode_in_error_context() {
let yaml = "key: 日本語\n[unclosed";
let result = Document::parse_str(yaml);
assert!(result.is_err());
let err = result.unwrap_err();
if let Error::ParseError(pe) = err {
let msg = pe.message();
assert!(!msg.is_empty(), "Error message should not be empty");
assert!(
msg.is_ascii()
|| msg
.chars()
.all(|c| !c.is_control() || c == '\n' || c == '\t'),
"Error message should be valid text"
);
}
}
#[test]
fn test_first_error_or_returns_collected() {
let result = Document::parse_str("[bad");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, Error::ParseError(_)),
"Expected ParseError variant, got {:?}",
err
);
}
#[test]
fn test_first_error_or_returns_fallback() {
let diag = Diag::new().unwrap();
let err = diag.first_error_or("fallback message");
match err {
Error::Parse(msg) => assert_eq!(msg, "fallback message"),
_ => panic!("Expected Error::Parse fallback"),
}
}
#[test]
fn test_first_error_returns_none_when_empty() {
let diag = Diag::new().unwrap();
assert!(diag.first_error().is_none());
}
#[test]
fn test_diag_error_helper_with_some() {
let diag = Diag::new();
let err = diag_error(diag, "fallback");
match err {
Error::Parse(msg) => assert_eq!(msg, "fallback"),
_ => panic!("Expected Error::Parse when no errors collected"),
}
}
#[test]
fn test_diag_error_helper_with_none() {
let err = diag_error(None, "fallback");
match err {
Error::Parse(msg) => assert_eq!(msg, "fallback"),
_ => panic!("Expected Error::Parse for None diag"),
}
}
}