alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
pub mod bash;
pub mod c;
pub mod csharp;
pub mod dart;
pub mod documentation;
pub mod elixir;
pub mod go;
pub mod java;
pub mod json_validator;
pub mod kotlin;
pub mod php;
pub mod python;
pub mod r;
pub mod ruby;
pub mod rust;
pub mod swift;
pub mod toml_validator;
pub mod typescript;
pub mod yaml_validator;
pub mod zig;

use crate::snippets::error::Result;
use crate::snippets::session::ValidationSession;
use crate::snippets::types::{Language, Snippet, SnippetStatus, ValidationLevel};
use std::collections::HashMap;
use std::io::Read;
use std::io::Write;

pub trait SnippetValidator: Send + Sync {
    fn language(&self) -> Language;
    fn is_available(&self) -> bool;
    fn is_available_at(&self, _level: ValidationLevel) -> bool {
        self.is_available()
    }
    /// Validate a snippet at the requested level.
    ///
    /// # Errors
    ///
    /// Returns an error when the validator cannot execute its underlying toolchain.
    fn validate(
        &self,
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
    ) -> Result<(SnippetStatus, Option<String>)>;
    fn validate_in_session(
        &self,
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
        session: Option<&ValidationSession>,
    ) -> Result<(SnippetStatus, Option<String>)> {
        if session.is_some() {
            return Err(crate::snippets::error::Error::Other(format!(
                "{} validator does not support binding-aware sessions",
                self.language()
            )));
        }
        self.validate(snippet, level, timeout_secs)
    }
    fn max_level(&self) -> ValidationLevel;

    fn is_dependency_error(&self, _error_output: &str) -> bool {
        false
    }
}

pub struct ValidatorRegistry {
    validators: HashMap<Language, Box<dyn SnippetValidator>>,
}

impl ValidatorRegistry {
    #[must_use]
    pub fn new() -> Self {
        let mut registry = Self {
            validators: HashMap::new(),
        };

        registry.register(Box::new(rust::RustValidator));
        registry.register(Box::new(python::PythonValidator));
        registry.register(Box::new(typescript::TypeScriptValidator));
        registry.register(Box::new(php::PhpValidator));
        registry.register(Box::new(ruby::RubyValidator));
        registry.register(Box::new(elixir::ElixirValidator));
        registry.register(Box::new(bash::BashValidator));
        registry.register(Box::new(toml_validator::TomlValidator));
        registry.register(Box::new(c::CValidator));
        registry.register(Box::new(csharp::CsharpValidator));
        registry.register(Box::new(dart::DartValidator));
        registry.register(Box::new(go::GoValidator));
        registry.register(Box::new(java::JavaValidator));
        registry.register(Box::new(kotlin::KotlinValidator));
        registry.register(Box::new(swift::SwiftValidator));
        registry.register(Box::new(zig::ZigValidator));
        registry.register(Box::new(json_validator::JsonValidator));
        registry.register(Box::new(yaml_validator::YamlValidator));
        registry.register(Box::new(r::RValidator));
        registry.register(Box::new(documentation::TextValidator));
        registry.register(Box::new(documentation::MermaidValidator));
        registry.register(Box::new(documentation::PowerShellValidator));
        registry.register(Box::new(documentation::XmlValidator));
        registry.register(Box::new(documentation::DockerValidator));

        registry
    }

    fn register(&mut self, validator: Box<dyn SnippetValidator>) {
        self.validators.insert(validator.language(), validator);
    }

    #[must_use]
    pub fn get(&self, language: Language) -> Option<&dyn SnippetValidator> {
        self.validators.get(&language).map(Box::as_ref)
    }
}

impl Default for ValidatorRegistry {
    fn default() -> Self {
        Self::new()
    }
}

pub fn run_script(
    snippet: &Snippet,
    level: ValidationLevel,
    timeout_secs: u64,
    session: Option<&ValidationSession>,
    suffix: &str,
    program: &str,
    syntax_arguments: &[&str],
) -> Result<(SnippetStatus, Option<String>)> {
    let mut source = match session {
        Some(value) => tempfile::Builder::new()
            .suffix(suffix)
            .tempfile_in(&value.working_directory)?,
        None => tempfile::Builder::new().suffix(suffix).tempfile()?,
    };
    source.write_all(snippet.code.as_bytes())?;
    source.flush()?;
    let mut command = std::process::Command::new(program);
    if level == ValidationLevel::Run {
        command.arg(source.path());
    } else {
        command.args(syntax_arguments).arg(source.path());
    }
    if let Some(value) = session {
        value.apply(&mut command);
        command.env("RUBYLIB", &value.working_directory);
        command.env("R_LIBS_USER", &value.working_directory);
    }
    let (success, output) = run_command(&mut command, timeout_secs)?;
    Ok(if success {
        (SnippetStatus::Pass, None)
    } else {
        (SnippetStatus::Fail, Some(output))
    })
}

fn strip_ansi_codes(input: &str) -> String {
    let mut result = String::with_capacity(input.len());
    let mut chars = input.chars();

    while let Some(ch) = chars.next() {
        if ch == '\u{1b}' {
            if matches!(chars.next(), Some('[')) {
                for next in chars.by_ref() {
                    if next == 'm' {
                        break;
                    }
                }
            }
        } else {
            result.push(ch);
        }
    }

    result
}

/// Run a child process with a timeout and capture combined stdout/stderr.
///
/// # Errors
///
/// Returns an error when the child process cannot be spawned, waited on, or times out.
pub fn run_command(command: &mut std::process::Command, timeout_secs: u64) -> Result<(bool, String)> {
    sanitize_environment(command);
    let mut child = command
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|err| crate::snippets::error::Error::Other(format!("spawn failed: {err}")))?;

    let timeout = std::time::Duration::from_secs(timeout_secs);
    match child.wait_timeout(timeout) {
        Ok(Some(status)) => {
            let mut output = String::new();

            if let Some(mut stdout) = child.stdout.take() {
                let _ = stdout.read_to_string(&mut output);
            }

            if let Some(mut stderr) = child.stderr.take() {
                let _ = stderr.read_to_string(&mut output);
            }

            Ok((status.success(), strip_ansi_codes(&output)))
        }
        Ok(None) => {
            let _ = child.kill();
            let _ = child.wait();
            Err(crate::snippets::error::Error::Timeout {
                command: format!("{command:?}"),
                timeout_secs,
            })
        }
        Err(err) => Err(crate::snippets::error::Error::Other(format!("wait failed: {err}"))),
    }
}

fn sanitize_environment(command: &mut std::process::Command) {
    const ALLOWED_VARIABLES: &[&str] = &[
        "PATH",
        "PATHEXT",
        "SYSTEMROOT",
        "WINDIR",
        "TMP",
        "TEMP",
        "TMPDIR",
        "LANG",
        "LC_ALL",
    ];
    let values: Vec<_> = ALLOWED_VARIABLES
        .iter()
        .filter_map(|key| std::env::var_os(key).map(|value| (*key, value)))
        .collect();
    let explicit_values = command
        .get_envs()
        .filter_map(|(key, value)| value.map(|value| (key.to_os_string(), value.to_os_string())))
        .collect::<Vec<_>>();
    command.env_clear();
    command.envs(values);
    command.envs(explicit_values);
    command.env("NO_COLOR", "1");
}

trait WaitTimeout {
    fn wait_timeout(&mut self, timeout: std::time::Duration) -> std::io::Result<Option<std::process::ExitStatus>>;
}

impl WaitTimeout for std::process::Child {
    fn wait_timeout(&mut self, timeout: std::time::Duration) -> std::io::Result<Option<std::process::ExitStatus>> {
        let start = std::time::Instant::now();
        let poll_interval = std::time::Duration::from_millis(50);

        loop {
            if let Some(status) = self.try_wait()? {
                return Ok(Some(status));
            }

            if start.elapsed() >= timeout {
                return Ok(None);
            }

            std::thread::sleep(poll_interval);
        }
    }
}