Skip to main content

auc_tool/
release.rs

1use std::time::Duration;
2
3use anyhow::{Context, Result, anyhow, bail};
4use capulus::managed::{CargoRegistry, ReleaseSource, ResolvedRelease, VersionTarget};
5use semver::Version;
6use serde::Deserialize;
7
8const CRATE_API: &str = "https://crates.io/api/v1/crates/auc-tool";
9
10#[derive(Clone)]
11pub struct AucReleaseSource {
12    client: reqwest::Client,
13}
14
15impl AucReleaseSource {
16    pub fn new() -> Result<Self> {
17        Ok(Self {
18            client: reqwest::Client::builder()
19                .timeout(Duration::from_secs(30))
20                .user_agent(concat!("auc/", env!("CARGO_PKG_VERSION")))
21                .build()
22                .context("failed to construct the auc release HTTP client")?,
23        })
24    }
25
26    async fn published_versions(&self) -> Result<Vec<PublishedVersion>> {
27        let response: CrateResponse = self
28            .client
29            .get(CRATE_API)
30            .send()
31            .await
32            .context("failed to query crates.io for auc-tool releases")?
33            .error_for_status()
34            .context("crates.io rejected the auc-tool release query")?
35            .json()
36            .await
37            .context("crates.io returned malformed auc-tool release metadata")?;
38        if response.versions.len() > 10_000 {
39            bail!("crates.io returned an unreasonable auc-tool release list");
40        }
41        response
42            .versions
43            .into_iter()
44            .map(|published| {
45                Ok(PublishedVersion {
46                    version: Version::parse(&published.num).with_context(|| {
47                        format!(
48                            "crates.io returned invalid auc-tool version {:?}",
49                            published.num
50                        )
51                    })?,
52                    yanked: published.yanked,
53                })
54            })
55            .collect()
56    }
57}
58
59impl ReleaseSource for AucReleaseSource {
60    async fn resolve(&self, target: VersionTarget) -> Result<ResolvedRelease> {
61        let versions = self.published_versions().await?;
62        let version = match target {
63            VersionTarget::Latest => versions
64                .into_iter()
65                .filter(|published| published.is_installable())
66                .map(|published| published.version)
67                .max()
68                .ok_or_else(|| anyhow!("auc-tool has no non-yanked stable release on crates.io"))?,
69            VersionTarget::Exact(value) => {
70                let version = Version::parse(&value)
71                    .with_context(|| format!("requested auc-tool version {value:?} is invalid"))?;
72                if !version.pre.is_empty() || !version.build.is_empty() {
73                    bail!("requested auc-tool release must be a stable semantic version");
74                }
75                versions
76                    .into_iter()
77                    .find(|published| published.version == version && published.is_installable())
78                    .map(|published| published.version)
79                    .ok_or_else(|| {
80                        anyhow!("auc-tool {version} is not a published non-yanked release")
81                    })?
82            }
83        };
84        let release = ResolvedRelease {
85            version,
86            registry: CargoRegistry::CratesIo,
87        };
88        release.validate()?;
89        Ok(release)
90    }
91}
92
93#[derive(Deserialize)]
94struct CrateResponse {
95    versions: Vec<CratesIoVersion>,
96}
97
98#[derive(Deserialize)]
99struct CratesIoVersion {
100    num: String,
101    yanked: bool,
102}
103
104struct PublishedVersion {
105    version: Version,
106    yanked: bool,
107}
108
109impl PublishedVersion {
110    fn is_installable(&self) -> bool {
111        !self.yanked && self.version.pre.is_empty() && self.version.build.is_empty()
112    }
113}