use kernel::{AxCode, AxError, Proxying, Reach, Release};
use wire::{ReleaseAnswer, ReleaseLine};
const LATEST_URL: &str = "https://registry.npmjs.org/sprawling/latest";
const RELEASES: &str = "https://github.com/2youg1/sprawling-agents/releases";
const PATIENCE: std::time::Duration = std::time::Duration::from_secs(10);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Built {
Released(Release),
FromSource,
}
pub fn built() -> Result<Built, AxError> {
match option_env!("SPRAWLING_RELEASE_TAG") {
None => Ok(Built::FromSource),
Some("") => Ok(Built::FromSource),
Some(tag) => Release::from_tag(tag, env!("CARGO_PKG_VERSION")).map(Built::Released),
}
}
pub fn newest() -> Result<Release, AxError> {
let client = gateway::client_for(Proxying::ExceptLocal, LATEST_URL)
.timeout(PATIENCE)
.build()
.map_err(|err| {
AxError::failure(AxCode::ToolUnavailable, "ask the registry", err.to_string())
.with_recovery("this machine refused to build an HTTP client")
})?;
let response = client
.get(LATEST_URL)
.header("accept", "application/json")
.send()
.map_err(|_| stopped(&client))?;
let status = response.status();
if !status.is_success() {
return Err(AxError::failure(
AxCode::ToolUnavailable,
"ask the registry",
LATEST_URL.to_owned(),
)
.with_recovery(format!(
"the registry answered {status}; the release page carries every \
archive either way: {RELEASES}"
)));
}
let body: serde_json::Value = response.json().map_err(|err| {
AxError::failure(
AxCode::ToolUnavailable,
"read the registry's answer",
err.to_string(),
)
.with_recovery(
"the registry answered something other than a version manifest; \
a proxy that returns a login page does this",
)
})?;
let version = body
.get("version")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
AxError::failure(
AxCode::ToolUnavailable,
"read the registry's answer",
LATEST_URL.to_owned(),
)
.with_recovery("the manifest carried no `version`")
})?;
Release::from_npm_version(version)
}
fn stopped(client: &reqwest::blocking::Client) -> AxError {
let Reach {
host,
named,
connected,
answered,
through,
..
} = gateway::reach(client, Proxying::ExceptLocal, LATEST_URL, 0);
AxError::failure(AxCode::ToolUnavailable, "ask the registry", host).with_recovery(format!(
"the call stopped there: name {named:?}, socket {connected:?}, \
request {answered:?}, through {through:?}"
))
}
fn line(release: &Release) -> ReleaseLine {
ReleaseLine {
version: release.npm_version(),
released: release.released(),
}
}
#[must_use]
pub fn answer() -> ReleaseAnswer {
let mine = match built() {
Ok(found) => found,
Err(refusal) => return ReleaseAnswer::Refused { refusal },
};
let newest = match newest() {
Ok(found) => found,
Err(refusal) => return ReleaseAnswer::Refused { refusal },
};
match mine {
Built::FromSource => ReleaseAnswer::Unreleased {
newest: line(&newest),
},
Built::Released(mine) => ReleaseAnswer::Stands {
mine: line(&mine),
newest: line(&newest),
verdict: kernel::release::stands(&mine, &newest),
},
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test code"
)]
mod tests {
use super::{Built, built};
#[test]
fn a_binary_knows_whether_it_came_out_of_a_release() {
match built().expect("this build's own tag decodes") {
Built::FromSource => {}
Built::Released(mine) => {
assert_eq!(mine.version(), env!("CARGO_PKG_VERSION"));
assert_eq!(
kernel::Release::from_npm_version(&mine.npm_version())
.expect("what this release publishes"),
mine
);
}
}
}
}