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}