wp-lang 0.3.0

WPL language crate with AST, parser, evaluator, builtins, and generators.
Documentation
use std::io::{self, Read};
use std::path::{Path, PathBuf};

use super::{DEFAULT_RULE_FILE, DEFAULT_SAMPLE_FILE, SampleInput};

pub(crate) fn resolve_source_path(path: Option<&Path>) -> Option<PathBuf> {
    match path {
        Some(path) if path == Path::new("-") => Some(path.to_path_buf()),
        Some(path) if path.is_dir() => Some(path.join(DEFAULT_RULE_FILE)),
        Some(path) => Some(path.to_path_buf()),
        None => None,
    }
}

pub(crate) fn resolve_sample_input(
    sample: &SampleInput,
    source_path: Option<&Path>,
) -> SampleInput {
    match sample {
        SampleInput::Inline(data) => SampleInput::Inline(data.clone()),
        SampleInput::DefaultFile => {
            let path = if let Some(source_path) = source_path {
                if source_path != Path::new("-") {
                    source_path
                        .parent()
                        .map(|base| base.join(DEFAULT_SAMPLE_FILE))
                        .unwrap_or_else(|| PathBuf::from(DEFAULT_SAMPLE_FILE))
                } else {
                    PathBuf::from(DEFAULT_SAMPLE_FILE)
                }
            } else {
                PathBuf::from(DEFAULT_SAMPLE_FILE)
            };
            SampleInput::File(path)
        }
        SampleInput::File(path) if path.is_absolute() => SampleInput::File(path.clone()),
        SampleInput::File(path) => {
            if path.is_dir() {
                SampleInput::File(path.join(DEFAULT_SAMPLE_FILE))
            } else {
                SampleInput::File(path.clone())
            }
        }
    }
}

pub(crate) fn load_input(path: Option<&Path>) -> Result<(String, Option<String>), String> {
    match path {
        Some(path) if path != Path::new("-") => {
            let source = std::fs::read_to_string(path)
                .map_err(|err| format!("failed to read {}: {err}", path.display()))?;
            Ok((source, Some(path.display().to_string())))
        }
        _ => {
            let mut source = String::new();
            io::stdin()
                .read_to_string(&mut source)
                .map_err(|err| format!("failed to read stdin: {err}"))?;
            Ok((source, None))
        }
    }
}

pub(crate) fn load_sample_data(sample: &SampleInput) -> Result<String, String> {
    match sample {
        SampleInput::Inline(data) => Ok(data.clone()),
        SampleInput::DefaultFile => {
            Err("internal error: default sample path was not resolved".to_string())
        }
        SampleInput::File(path) => std::fs::read_to_string(path)
            .map_err(|err| format!("failed to read sample data {}: {err}", path.display())),
    }
}

#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use super::*;

    #[test]
    fn test_resolve_directory_defaults() {
        let source = resolve_source_path(Some(Path::new("examples/wpl-check/csv_demo"))).unwrap();
        let sample = resolve_sample_input(&SampleInput::DefaultFile, Some(source.as_path()));

        assert_eq!(
            source,
            PathBuf::from("examples/wpl-check/csv_demo/rule.wpl")
        );
        assert_eq!(
            sample,
            SampleInput::File(PathBuf::from("examples/wpl-check/csv_demo/sample.txt"))
        );
    }

    #[test]
    fn test_explicit_relative_sample_path_is_not_rebased() {
        let source = PathBuf::from("examples/wpl-check/package_demo/rule.wpl");
        let sample = resolve_sample_input(
            &SampleInput::File(PathBuf::from("custom.txt")),
            Some(source.as_path()),
        );

        assert_eq!(sample, SampleInput::File(PathBuf::from("custom.txt")));
    }

    #[test]
    fn test_load_sample_data_preserves_trailing_newline() {
        let path = PathBuf::from(format!(
            "/tmp/wpl_check_sample_{}_{}.txt",
            std::process::id(),
            "newline"
        ));
        std::fs::write(&path, "42,alice,\n").unwrap();

        let loaded = load_sample_data(&SampleInput::File(path.clone())).unwrap();
        assert_eq!(loaded, "42,alice,\n");

        let _ = std::fs::remove_file(path);
    }
}