rust_crate_src_mcp/
version.rs1use 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}