pub(crate) mod format;
mod css;
mod csv;
mod dotenv;
mod heuristics;
mod html;
mod javascript;
mod js;
mod json;
mod position;
mod schemes;
mod toml;
#[cfg(test)]
pub(crate) mod corpus;
pub(crate) use format::{FileType, determine_file_type};
pub(crate) use heuristics::PathType;
pub(crate) use position::Position;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Path {
pub(crate) value: String,
#[serde(rename = "type")]
pub(crate) kind: PathType,
#[serde(flatten)]
pub(crate) position: Position,
pub(crate) context: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum ErrorCategory {
Parsing,
Format,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Severity {
Info,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct ExtractionError {
pub(crate) category: ErrorCategory,
pub(crate) severity: Severity,
pub(crate) message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct Extraction {
pub(crate) success: bool,
pub(crate) paths: Vec<Path>,
pub(crate) errors: Vec<ExtractionError>,
}
impl Extraction {
fn found(paths: Vec<Path>) -> Self {
Self {
success: true,
paths,
errors: Vec::new(),
}
}
fn failed(category: ErrorCategory, severity: Severity, message: String) -> Self {
Self {
success: false,
paths: Vec::new(),
errors: vec![ExtractionError {
category,
severity,
message,
}],
}
}
}
pub(crate) type Extracted = Result<Vec<Path>, String>;
pub(crate) fn extract(content: &str, language_id: &str) -> Extraction {
let file_type = determine_file_type(language_id);
if file_type == FileType::Unknown {
return Extraction::failed(
ErrorCategory::Format,
Severity::Info,
format!(
"Path extraction is not supported for {language_id} files. \
Supported formats: CSV, TOML, ENV, JS, TS, JSON, HTML, CSS."
),
);
}
match extract_by_file_type(content, file_type) {
Ok(paths) => Extraction::found(paths),
Err(message) => Extraction::failed(ErrorCategory::Parsing, Severity::Error, message),
}
}
fn extract_by_file_type(content: &str, file_type: FileType) -> Extracted {
match file_type {
FileType::Csv => Ok(csv::extract(content)),
FileType::Toml => Ok(toml::extract(content)),
FileType::Dotenv => dotenv::extract(content),
FileType::Javascript | FileType::Typescript => javascript::extract(content),
FileType::Json => Ok(json::extract(content)),
FileType::Css => css::extract(content),
FileType::Html => html::extract(content),
FileType::Unknown => Ok(Vec::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unsupported_language_is_a_format_error_not_an_empty_result() {
let result = extract("print(\"hi\")", "python");
assert!(!result.success);
assert!(result.paths.is_empty());
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].category, ErrorCategory::Format);
assert_eq!(result.errors[0].severity, Severity::Info);
assert_eq!(
result.errors[0].message,
"Path extraction is not supported for python files. \
Supported formats: CSV, TOML, ENV, JS, TS, JSON, HTML, CSS."
);
}
#[test]
fn an_empty_document_succeeds_with_nothing() {
for language in ["json", "toml", "csv", "dotenv", "javascript", "css", "html"] {
let result = extract("", language);
assert!(result.success, "{language}");
assert!(result.paths.is_empty(), "{language}");
assert!(result.errors.is_empty(), "{language}");
}
}
#[test]
fn every_supported_language_id_dispatches() {
for language in [
"csv",
"toml",
"dotenv",
"env",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"json",
"jsonc",
"html",
"css",
"scss",
"less",
] {
assert!(extract("", language).success, "{language}");
}
}
}