aegis-tool 0.3.3

Aegis SSH client and managed host agent.
Documentation
use semver::Version;

use super::local_agent;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum State {
    Unknown,
    Current,
    Older(String),
    Newer(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ProbeResult {
    pub(super) state: State,
    pub(super) detail: Option<String>,
}

impl ProbeResult {
    pub(super) fn state(state: State) -> Self {
        Self {
            state,
            detail: None,
        }
    }

    pub(super) fn unknown(detail: impl Into<String>) -> Self {
        Self {
            state: State::Unknown,
            detail: Some(detail.into()),
        }
    }
}

#[cfg(test)]
pub(super) fn current() -> Version {
    Version::parse(env!("CARGO_PKG_VERSION")).expect("aegis-tool package version must be semver")
}

pub(super) fn from_json(payload: &str, target: &Version) -> ProbeResult {
    let payload = payload.trim();
    if payload.is_empty() {
        return ProbeResult::unknown("agent version probe returned no output");
    }
    match serde_json::from_str::<local_agent::AgentVersionResponse>(payload) {
        Ok(response) => from_text(&response.version, target),
        Err(error) => ProbeResult::unknown(format!(
            "agent version probe returned invalid JSON: {error}: {payload}"
        )),
    }
}

pub(super) fn from_text(version: &str, target: &Version) -> ProbeResult {
    let version = version.trim();
    match state(version, target) {
        State::Unknown if version.is_empty() => {
            ProbeResult::unknown("agent version probe returned no output")
        }
        State::Unknown => ProbeResult::unknown(format!("agent version probe returned `{version}`")),
        State::Older(version) => ProbeResult {
            state: State::Older(version.clone()),
            detail: Some(format!("agent still reported v{version}")),
        },
        state => ProbeResult::state(state),
    }
}

fn state(remote_version: &str, target: &Version) -> State {
    let remote_version = remote_version.trim().trim_start_matches('v');
    if remote_version.is_empty() {
        return State::Unknown;
    }
    let Ok(remote) = Version::parse(remote_version) else {
        return State::Unknown;
    };
    match remote.cmp(target) {
        std::cmp::Ordering::Less => State::Older(remote_version.to_string()),
        std::cmp::Ordering::Equal => State::Current,
        std::cmp::Ordering::Greater => State::Newer(remote_version.to_string()),
    }
}