pub(crate) mod collect;
pub(crate) mod corpus;
pub(crate) mod csv;
pub(crate) mod dotenv;
pub(crate) mod fallback;
pub(crate) mod format;
#[cfg(test)]
mod fuzz;
pub(crate) mod ini;
pub(crate) mod json;
pub(crate) mod locate;
pub(crate) mod position;
pub(crate) mod source;
pub(crate) mod text;
pub(crate) mod toml;
pub(crate) mod yaml;
use serde::Serialize;
pub(crate) use format::{FALLBACK_FORMAT, SUPPORTED_FORMATS, resolve_format};
pub(crate) use position::Position;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Options {
pub(crate) csv_has_header: bool,
pub(crate) csv_column: Option<usize>,
pub(crate) multiline: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Found {
pub(crate) value: String,
#[serde(flatten)]
pub(crate) position: Option<Position>,
}
pub(crate) fn extract(text: &str, format: &str, options: Options) -> Vec<String> {
let trimmed = text::trim(text);
if trimmed.is_empty() {
return Vec::new();
}
match format::canonical(format) {
"json" => json::extract(trimmed),
"yaml" => yaml::extract(trimmed),
"csv" => csv::extract(trimmed, options),
"toml" => toml::extract(trimmed),
"ini" => ini::extract(trimmed),
"env" => dotenv::extract(trimmed),
other => match source::language(other) {
Some(language) => source::extract(trimmed, language),
None => fallback::extract_with(trimmed, options.multiline),
},
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Extraction {
pub(crate) found: Vec<Found>,
pub(crate) parse_error: Option<String>,
}
pub(crate) fn examine(text: &str, format: &str, options: Options) -> Extraction {
let trimmed = text::trim(text);
if trimmed.is_empty() {
return Extraction::default();
}
let shift = text.len() - text::trim_start(text).len();
let index = position::PositionIndex::new(text);
if format::canonical(format) == "json" {
return Extraction {
found: json::extract_spanned(trimmed)
.into_iter()
.map(|(value, offset)| Found {
value,
position: Some(index.at(offset + shift)),
})
.collect(),
parse_error: json::parse_error(trimmed),
};
}
Extraction {
found: locate::locate(text, extract(text, format, options)),
parse_error: parse_error(text, format),
}
}
pub(crate) fn parse_error(text: &str, format: &str) -> Option<String> {
let trimmed = text::trim(text);
if trimmed.is_empty() {
return None;
}
match format::canonical(format) {
"json" => json::parse_error(trimmed),
"yaml" => yaml::parse_error(trimmed),
"csv" => csv::parse_error(trimmed),
"toml" => toml::parse_error(trimmed),
"ini" => ini::parse_error(trimmed),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_document_yields_nothing() {
assert!(extract("", "json", Options::default()).is_empty());
assert!(extract(" \n\t ", "json", Options::default()).is_empty());
assert!(parse_error(" ", "json").is_none());
}
#[test]
fn an_unknown_format_falls_back_rather_than_failing() {
assert_eq!(
extract("const a = 'hello';", "klingon", Options::default()),
["hello"]
);
assert!(parse_error("const a = 'hello';", "klingon").is_none());
}
#[test]
fn a_source_language_is_read_by_its_own_rules() {
let source = "def f():\n \"\"\"One\ndocstring.\"\"\"\n";
assert_eq!(
extract(source, "python", Options::default()),
["One\ndocstring."]
);
assert!(extract(source, FALLBACK_FORMAT, Options::default()).is_empty());
assert!(parse_error(source, "python").is_none());
}
#[test]
fn locating_does_not_change_what_was_extracted() {
let text = "{\"a\":\"one\",\"b\":\"two\"}";
let values = extract(text, "json", Options::default());
let examined = examine(text, "json", Options::default());
assert_eq!(
values,
examined
.found
.iter()
.map(|f| f.value.clone())
.collect::<Vec<_>>()
);
}
}