use std::path::Path;
use pest::Parser;
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "src/parser/preprocessor.pest"]
struct PreprocessorParser;
type PreprocessError = Box<pest::error::Error<Rule>>;
pub fn preprocess(input: &str, working_dir: &Path) -> Result<String, PreprocessError> {
let mut lines: Vec<String> = vec![];
let mut pairs = PreprocessorParser::parse(Rule::body, input).map_err(Box::new)?;
let Some(pair) = pairs.next() else {
return Err(custom_error(input, "Failed to parse preprocessor input"));
};
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::line => {
lines.push(inner_pair.as_str().trim().to_string());
}
Rule::import_target => {
let arg = inner_pair.as_str().trim();
lines.push(resolve_import(arg, working_dir)?);
}
_ => {
return Err(custom_error(
inner_pair.as_str(),
format!("Unexpected preprocessor rule: {:?}", inner_pair.as_rule()),
));
}
}
}
Ok(lines.join("\n"))
}
fn resolve_import(path: &str, working_dir: &Path) -> Result<String, PreprocessError> {
let import_path = working_dir.join(path);
let file_content = std::fs::read_to_string(&import_path).map_err(|err| {
custom_error(
path,
format!("Failed to read import '{}': {}", import_path.display(), err),
)
})?;
let import_working_dir = import_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| working_dir.to_path_buf());
preprocess(&file_content, &import_working_dir)
}
fn custom_error(source: &str, message: impl Into<String>) -> PreprocessError {
Box::new(pest::error::Error::new_from_pos(
pest::error::ErrorVariant::CustomError {
message: message.into(),
},
pest::Position::from_start(source),
))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn nested_imports_resolve_relative_to_imported_file() {
let dir = tempdir().expect("tempdir should exist");
let root = dir.path();
let nested_dir = root.join("nested");
std::fs::create_dir_all(&nested_dir).expect("nested dir should exist");
std::fs::write(root.join("first.hen"), "<< nested/second.hen\n").expect("first import");
std::fs::write(nested_dir.join("second.hen"), "<< third.hen\n").expect("second import");
std::fs::write(nested_dir.join("third.hen"), "GET https://example.com\n")
.expect("third import");
let output = preprocess("<< first.hen\n", root).expect("preprocess should succeed");
assert_eq!(output, "GET https://example.com");
}
#[test]
fn missing_import_returns_readable_error() {
let dir = tempdir().expect("tempdir should exist");
let err = preprocess("<< missing.hen\n", dir.path()).expect_err("preprocess should fail");
assert!(err.to_string().contains("Failed to read import"));
assert!(err.to_string().contains("missing.hen"));
}
}