pub fn has_builtin(format: &str) -> bool {
checker_for(format).is_some()
}
pub const BUILTIN_FORMATS: &[&str] = &["json", "xml", "yaml", "csv", "toml"];
type Checker = fn(&str) -> Result<(), String>;
fn checker_for(format: &str) -> Option<Checker> {
match format.trim().to_ascii_lowercase().as_str() {
"json" => Some(check_json),
"xml" => Some(check_xml),
"yaml" | "yml" => Some(check_yaml),
"csv" => Some(check_csv),
"toml" => Some(check_toml),
_ => None,
}
}
pub fn check(format: Option<&str>, content: &str) -> Result<(), String> {
let Some(checker) = format.and_then(checker_for) else {
return Ok(());
};
checker(content)
}
fn check_json(content: &str) -> Result<(), String> {
serde_json::from_str::<serde_json::Value>(content)
.map(|_| ())
.map_err(|e| e.to_string())
}
fn check_toml(content: &str) -> Result<(), String> {
content
.parse::<toml::Table>()
.map(|_| ())
.map_err(|e| e.to_string())
}
fn check_yaml(content: &str) -> Result<(), String> {
if uses_anchors_or_aliases(content) {
return Ok(());
}
yaml_rust2::YamlLoader::load_from_str(content)
.map(|_| ())
.map_err(|e| e.to_string())
}
fn uses_anchors_or_aliases(content: &str) -> bool {
let bytes = content.as_bytes();
bytes.iter().enumerate().any(|(i, &b)| {
if b != b'&' && b != b'*' {
return false;
}
let before_ok = match i {
0 => true,
_ => matches!(
bytes[i - 1],
b' ' | b'\t' | b'\n' | b'\r' | b'[' | b'{' | b',' | b'-'
),
};
let after_ok = bytes
.get(i + 1)
.is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_');
before_ok && after_ok
})
}
fn check_xml(content: &str) -> Result<(), String> {
use quick_xml::events::Event;
let mut reader = quick_xml::Reader::from_str(content);
let mut buf = Vec::new();
let mut depth: i64 = 0;
let mut saw_element = false;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Eof) => break,
Ok(Event::Start(_)) => {
saw_element = true;
depth += 1;
}
Ok(Event::Empty(_)) => saw_element = true,
Ok(Event::End(_)) => depth -= 1,
Ok(_) => {}
Err(e) => return Err(e.to_string()),
}
buf.clear();
}
if !saw_element {
return Err("no XML elements found; this looks like plain text".to_string());
}
match depth {
0 => Ok(()),
open => Err(format!(
"{open} element(s) left unclosed at the end of the document"
)),
}
}
fn check_csv(content: &str) -> Result<(), String> {
if content.trim().is_empty() {
return Err("the CSV is empty".to_string());
}
let mut reader = csv::ReaderBuilder::new()
.flexible(false)
.from_reader(content.as_bytes());
for record in reader.records() {
record.map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg(test)]
mod tests;