fundaia 0.5.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
//! What crates.io says the newest release is.
//!
//! A client of its own rather than the one the commands use: this one carries a
//! budget measured in seconds and no bearer token, and neither belongs on the
//! client that talks to somebody's instance.

use std::time::Duration;

/// Where the crate is published, which is where the newest number lives.
const REGISTRY_URL: &str = "https://crates.io/api/v1/crates/fundaia";

/// What the registry answered, of which one field matters.
#[derive(Debug, serde::Deserialize)]
struct RegistryAnswer {
    #[serde(rename = "crate")]
    published: PublishedCrate,
}

#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
struct PublishedCrate {
    newest_version: String,
    /// Absent only if every version ever published was a pre-release.
    #[serde(default)]
    max_stable_version: Option<String>,
}

/// Asks crates.io, and answers `None` for every way that can fail.
///
/// The budget is the caller's because the two callers are not asking the same
/// question: the check behind every command is a courtesy that must not be felt,
/// and `upgrade` is somebody standing there having asked for this.
pub async fn latest_version(budget: Duration) -> Option<String> {
    let http = reqwest::Client::builder()
        .timeout(budget)
        // crates.io refuses an anonymous agent outright, by policy.
        .user_agent(concat!("fundaia/", env!("CARGO_PKG_VERSION")))
        .build()
        .ok()?;

    let response = http.get(REGISTRY_URL).send().await.ok()?;
    if !response.status().is_success() {
        return None;
    }

    let answer: RegistryAnswer = response.json().await.ok()?;

    // The newest *stable* one: a pre-release is published to be tried by
    // somebody who went looking for it, not to be suggested to everybody.
    Some(
        answer
            .published
            .max_stable_version
            .unwrap_or(answer.published.newest_version),
    )
}

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

    /// Trimmed from a real answer, keeping the shape and the two fields read.
    const REGISTRY_PAYLOAD: &str = r#"{
        "crate": {
            "id": "fundaia",
            "name": "fundaia",
            "newest_version": "0.4.0-rc1",
            "max_stable_version": "0.3.0",
            "description": "Command line for the Fundaia deployment platform"
        },
        "versions": []
    }"#;

    #[test]
    fn it_should_read_the_newest_stable_release_from_the_registry() {
        let answer: RegistryAnswer =
            serde_json::from_str(REGISTRY_PAYLOAD).expect("the payload is the registry's own");
        assert_eq!(answer.published.max_stable_version.unwrap(), "0.3.0");
    }

    #[test]
    fn it_should_ignore_the_fields_the_registry_sends_and_this_never_reads() {
        assert!(serde_json::from_str::<RegistryAnswer>(REGISTRY_PAYLOAD).is_ok());
    }

    #[test]
    fn it_should_fall_back_to_the_newest_version_when_nothing_stable_exists() {
        let answer: RegistryAnswer = serde_json::from_str(
            r#"{"crate":{"newest_version":"0.1.0-alpha1","max_stable_version":null}}"#,
        )
        .expect("a crate whose every release was a pre-release");
        assert_eq!(
            answer
                .published
                .max_stable_version
                .unwrap_or(answer.published.newest_version),
            "0.1.0-alpha1",
        );
    }
}