rust_crate_src_mcp/
version.rs

1use snafu::Snafu;
2
3#[derive(serde::Deserialize)]
4struct CrateResponse {
5    #[serde(rename = "crate")]
6    crate_info: CrateInfo,
7    versions: Vec<VersionInfo>,
8}
9
10#[derive(serde::Deserialize)]
11struct CrateInfo {
12    max_version: String,
13}
14
15#[derive(serde::Deserialize)]
16struct VersionInfo {
17    num: String,
18}
19
20pub async fn resolve_version(
21    crate_name: &str,
22    version_req: Option<&str>,
23) -> Result<String, ResolveVersionError> {
24    use resolve_version_error::*;
25    use snafu::ResultExt;
26
27    let client = reqwest::Client::new();
28    let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
29    let resp = client
30        .get(&url)
31        .header("User-Agent", "rust-crate-src-mcp")
32        .send()
33        .await
34        .context(CratesIoQuerySnafu { crate_name })?
35        .json::<CrateResponse>()
36        .await
37        .context(CratesIoQuerySnafu { crate_name })?;
38
39    match version_req {
40        None => Ok(resp.crate_info.max_version),
41        Some(req_str) => {
42            let req = semver::VersionReq::parse(req_str).context(SemverParseSnafu {
43                requirement: req_str,
44            })?;
45            resp.versions
46                .iter()
47                .filter_map(|v| semver::Version::parse(&v.num).ok())
48                .filter(|v| req.matches(v))
49                .max()
50                .map(|v| v.to_string())
51                .ok_or_else(|| {
52                    NoMatchingVersionSnafu {
53                        crate_name,
54                        requirement: req_str,
55                    }
56                    .build()
57                })
58        }
59    }
60}
61
62#[derive(Debug, Snafu)]
63#[snafu(module)]
64pub enum ResolveVersionError {
65    #[snafu(display("failed to query crates.io for crate '{crate_name}'"))]
66    CratesIoQuery {
67        crate_name: String,
68        source: reqwest::Error,
69    },
70    #[snafu(display("failed to parse semver requirement '{requirement}'"))]
71    SemverParse {
72        requirement: String,
73        source: semver::Error,
74    },
75    #[snafu(display("no version matching '{requirement}' found for crate '{crate_name}'"))]
76    NoMatchingVersion {
77        crate_name: String,
78        requirement: String,
79    },
80}