use crate::{CanonicalValue, Format};
use std::error::Error;
use std::path::Path;
pub fn detect_content(input: &[u8]) -> Result<Format, Box<dyn Error>> {
let stripped = input.strip_prefix("\u{feff}".as_bytes()).unwrap_or(input);
let text = std::str::from_utf8(stripped)
.map_err(|_| "input is not valid UTF-8; format detection requires text")?;
if text.trim().is_empty() {
return Err("empty input; cannot detect format".into());
}
if serde_json::from_str::<serde_json::Value>(text).is_ok() {
return Ok(Format::Json);
}
if is_ndjson(text) {
return Ok(Format::Ndjson);
}
let trimmed = text.trim_start();
if trimmed.starts_with('<') && quick_xml::Reader::from_str(text).read_event().is_ok() {
return Ok(Format::Xml);
}
if looks_like_delimited(text, b',') && CanonicalValue::from_csv(text).is_ok() {
return Ok(Format::Csv);
}
if looks_like_delimited(text, b'\t') && CanonicalValue::from_tsv(text).is_ok() {
return Ok(Format::Tsv);
}
if toml::from_str::<toml::Value>(text).is_ok() {
return Ok(Format::Toml);
}
if CanonicalValue::from_yaml(text).is_ok() {
return Ok(Format::Yaml);
}
Err("could not detect format from content; use --from to specify".into())
}
pub fn detect_extension(path: &Path) -> Option<Format> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
match ext.as_str() {
"csv" => Some(Format::Csv),
"tsv" | "tab" => Some(Format::Tsv),
"json" => Some(Format::Json),
"ndjson" | "jsonl" => Some(Format::Ndjson),
"yaml" | "yml" => Some(Format::Yaml),
"xml" => Some(Format::Xml),
"toml" => Some(Format::Toml),
"xlsx" => Some(Format::Xlsx),
"sql" => Some(Format::Sql),
_ => None,
}
}
fn is_ndjson(text: &str) -> bool {
let mut count = 0;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if serde_json::from_str::<serde_json::Value>(trimmed).is_err() {
return false;
}
count += 1;
}
count > 0
}
fn looks_like_delimited(text: &str, delim: u8) -> bool {
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let first = match lines.next() {
Some(l) => l.as_bytes(),
None => return false,
};
let count_first = first.iter().filter(|&&b| b == delim).count();
if count_first == 0 {
return false;
}
lines.next().is_some() || count_first >= 1
}