axiomsync 0.1.6

Core data-processing engine for AxiomSync local retrieval runtime.
Documentation
use std::path::Path;
#[cfg(feature = "host-tools")]
use std::process::Command;

pub const AXIOMSYNC_HOST_TOOLS_ENV: &str = "AXIOMSYNC_HOST_TOOLS";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostToolsMode {
    Enabled,
    Disabled,
}

impl HostToolsMode {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Enabled => "enabled",
            Self::Disabled => "disabled",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostToolsPolicySource {
    Environment,
    TargetDefault,
}

impl HostToolsPolicySource {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Environment => "env",
            Self::TargetDefault => "target_default",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostToolsPolicy {
    pub mode: HostToolsMode,
    pub source: HostToolsPolicySource,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostCommandSpec<'a> {
    pub operation: &'a str,
    pub program: &'a str,
    pub args: &'a [&'a str],
    pub current_dir: Option<&'a Path>,
}

impl<'a> HostCommandSpec<'a> {
    #[must_use]
    pub const fn new(operation: &'a str, program: &'a str, args: &'a [&'a str]) -> Self {
        Self {
            operation,
            program,
            args,
            current_dir: None,
        }
    }

    #[must_use]
    pub const fn with_current_dir(mut self, current_dir: &'a Path) -> Self {
        self.current_dir = Some(current_dir);
        self
    }
}

#[cfg_attr(
    not(feature = "host-tools"),
    allow(
        dead_code,
        reason = "result shape stays stable across feature profiles"
    )
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostCommandResult {
    Blocked {
        reason: String,
    },
    SpawnError {
        error: String,
    },
    Completed {
        success: bool,
        stdout: String,
        stderr: String,
    },
}

#[must_use]
pub fn resolve_host_tools_policy() -> HostToolsPolicy {
    let env_raw = std::env::var(AXIOMSYNC_HOST_TOOLS_ENV).ok();
    resolve_host_tools_policy_with(env_raw.as_deref(), cfg!(target_os = "ios"))
}

#[must_use]
pub fn run_host_command(spec: HostCommandSpec<'_>) -> HostCommandResult {
    run_host_command_with_policy(spec, resolve_host_tools_policy())
}

#[must_use]
fn run_host_command_with_policy(
    spec: HostCommandSpec<'_>,
    policy: HostToolsPolicy,
) -> HostCommandResult {
    if policy.mode == HostToolsMode::Disabled {
        return HostCommandResult::Blocked {
            reason: format_host_tools_block_reason(spec.operation, policy),
        };
    }

    #[cfg(feature = "host-tools")]
    {
        let mut command = Command::new(spec.program);
        command.args(spec.args);
        if let Some(current_dir) = spec.current_dir {
            command.current_dir(current_dir);
        }

        match command.output() {
            Ok(output) => HostCommandResult::Completed {
                success: output.status.success(),
                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
            },
            Err(err) => HostCommandResult::SpawnError {
                error: err.to_string(),
            },
        }
    }

    #[cfg(not(feature = "host-tools"))]
    {
        let _ = policy;
        HostCommandResult::Blocked {
            reason: format_host_tools_feature_disabled_reason(spec.operation),
        }
    }
}

fn format_host_tools_block_reason(operation: &str, policy: HostToolsPolicy) -> String {
    format!(
        "host_tools_disabled operation={operation} mode={} source={} env={} (set env to on/1/true to enable host process execution)",
        policy.mode.as_str(),
        policy.source.as_str(),
        AXIOMSYNC_HOST_TOOLS_ENV
    )
}

#[cfg(not(feature = "host-tools"))]
fn format_host_tools_feature_disabled_reason(operation: &str) -> String {
    format!(
        "host_tools_unavailable operation={operation} feature=host-tools (compile axiomsync with feature `host-tools` to enable host process execution)"
    )
}

#[must_use]
fn resolve_host_tools_policy_with(env_raw: Option<&str>, target_is_ios: bool) -> HostToolsPolicy {
    if let Some(mode) = env_raw.and_then(parse_host_tools_mode) {
        return HostToolsPolicy {
            mode,
            source: HostToolsPolicySource::Environment,
        };
    }

    let mode = if target_is_ios {
        HostToolsMode::Disabled
    } else {
        HostToolsMode::Enabled
    };
    HostToolsPolicy {
        mode,
        source: HostToolsPolicySource::TargetDefault,
    }
}

#[must_use]
fn parse_host_tools_mode(raw: &str) -> Option<HostToolsMode> {
    match raw.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "on" | "enabled" => Some(HostToolsMode::Enabled),
        "0" | "false" | "no" | "off" | "disabled" | "none" => Some(HostToolsMode::Disabled),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_host_tools_mode_accepts_common_tokens() {
        assert_eq!(parse_host_tools_mode("on"), Some(HostToolsMode::Enabled));
        assert_eq!(parse_host_tools_mode("1"), Some(HostToolsMode::Enabled));
        assert_eq!(parse_host_tools_mode("off"), Some(HostToolsMode::Disabled));
        assert_eq!(parse_host_tools_mode("0"), Some(HostToolsMode::Disabled));
    }

    #[test]
    fn policy_prefers_environment_override() {
        let policy = resolve_host_tools_policy_with(Some("off"), false);
        assert_eq!(
            policy,
            HostToolsPolicy {
                mode: HostToolsMode::Disabled,
                source: HostToolsPolicySource::Environment,
            }
        );
    }

    #[test]
    fn policy_defaults_to_disabled_on_ios_target() {
        let policy = resolve_host_tools_policy_with(None, true);
        assert_eq!(policy.mode, HostToolsMode::Disabled);
        assert_eq!(policy.source, HostToolsPolicySource::TargetDefault);
    }

    #[test]
    fn policy_defaults_to_enabled_on_non_ios_target() {
        let policy = resolve_host_tools_policy_with(None, false);
        assert_eq!(policy.mode, HostToolsMode::Enabled);
        assert_eq!(policy.source, HostToolsPolicySource::TargetDefault);
    }

    #[test]
    fn run_host_command_with_disabled_policy_returns_blocked() {
        let spec = HostCommandSpec::new("test:blocked", "this-command-should-not-run", &[]);
        let result = run_host_command_with_policy(
            spec,
            HostToolsPolicy {
                mode: HostToolsMode::Disabled,
                source: HostToolsPolicySource::TargetDefault,
            },
        );
        match result {
            HostCommandResult::Blocked { reason } => {
                assert!(reason.contains("operation=test:blocked"));
            }
            other => panic!("expected Blocked result, got: {other:?}"),
        }
    }

    #[cfg(feature = "host-tools")]
    #[test]
    fn run_host_command_with_enabled_policy_reports_spawn_error() {
        let spec =
            HostCommandSpec::new("test:spawn_error", "axiomsync-command-does-not-exist", &[]);
        let result = run_host_command_with_policy(
            spec,
            HostToolsPolicy {
                mode: HostToolsMode::Enabled,
                source: HostToolsPolicySource::TargetDefault,
            },
        );
        match result {
            HostCommandResult::SpawnError { error } => assert!(!error.trim().is_empty()),
            other => panic!("expected SpawnError result, got: {other:?}"),
        }
    }

    #[cfg(all(unix, feature = "host-tools"))]
    #[test]
    fn run_host_command_with_enabled_policy_reports_success_output() {
        let spec = HostCommandSpec::new("test:success", "sh", &["-c", "printf ok"]);
        let result = run_host_command_with_policy(
            spec,
            HostToolsPolicy {
                mode: HostToolsMode::Enabled,
                source: HostToolsPolicySource::TargetDefault,
            },
        );
        match result {
            HostCommandResult::Completed {
                success,
                stdout,
                stderr: _,
            } => {
                assert!(success);
                assert_eq!(stdout, "ok");
            }
            other => panic!("expected Completed result, got: {other:?}"),
        }
    }

    #[cfg(all(unix, feature = "host-tools"))]
    #[test]
    fn run_host_command_with_enabled_policy_reports_failure_status() {
        let spec = HostCommandSpec::new("test:failure", "sh", &["-c", "exit 7"]);
        let result = run_host_command_with_policy(
            spec,
            HostToolsPolicy {
                mode: HostToolsMode::Enabled,
                source: HostToolsPolicySource::TargetDefault,
            },
        );
        match result {
            HostCommandResult::Completed {
                success,
                stdout: _,
                stderr: _,
            } => assert!(!success),
            other => panic!("expected Completed result, got: {other:?}"),
        }
    }

    #[cfg(not(feature = "host-tools"))]
    #[test]
    fn run_host_command_with_enabled_policy_is_blocked_when_feature_is_disabled() {
        let spec = HostCommandSpec::new("test:feature-off", "sh", &["-c", "printf ok"]);
        let result = run_host_command_with_policy(
            spec,
            HostToolsPolicy {
                mode: HostToolsMode::Enabled,
                source: HostToolsPolicySource::TargetDefault,
            },
        );
        match result {
            HostCommandResult::Blocked { reason } => {
                assert!(reason.contains("host_tools_unavailable"));
                assert!(reason.contains("operation=test:feature-off"));
            }
            other => panic!("expected Blocked result, got: {other:?}"),
        }
    }
}