docs_mcp/sparse_index/
types.rs1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct IndexLine {
8 pub name: String,
10 pub vers: String,
12 #[serde(default)]
14 pub deps: Vec<DepEntry>,
15 pub cksum: String,
17 #[serde(default)]
19 pub features: HashMap<String, Vec<String>>,
20 #[serde(default)]
22 pub yanked: bool,
23 pub rust_version: Option<String>,
25 pub features2: Option<HashMap<String, Vec<String>>>,
27}
28
29impl IndexLine {
30 pub fn all_features(&self) -> HashMap<String, Vec<String>> {
32 let mut merged = self.features.clone();
33 if let Some(f2) = &self.features2 {
34 merged.extend(f2.clone());
35 }
36 merged
37 }
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
41pub struct DepEntry {
42 pub name: String,
43 pub req: String,
44 pub package: Option<String>,
46 pub kind: Option<DepKind>,
47 #[serde(default)]
48 pub optional: bool,
49 #[serde(default)]
50 pub default_features: bool,
51 #[serde(default)]
52 pub features: Vec<String>,
53 pub target: Option<String>,
54}
55
56#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
57#[serde(rename_all = "lowercase")]
58pub enum DepKind {
59 Normal,
60 Dev,
61 Build,
62}
63
64pub fn compute_path(name: &str) -> String {
72 let n = name.to_lowercase();
73 match n.len() {
74 0 => panic!("empty crate name"),
75 1 => format!("1/{n}"),
76 2 => format!("2/{n}"),
77 3 => format!("3/{}/{n}", &n[0..1]),
78 _ => format!("{}/{}/{n}", &n[0..2], &n[2..4]),
79 }
80}
81
82pub fn find_latest_stable(lines: &[IndexLine]) -> Option<&IndexLine> {
89 use semver::Version;
90
91 let stable: Vec<&IndexLine> = lines
93 .iter()
94 .filter(|l| !l.yanked && !l.vers.contains('-'))
95 .collect();
96
97 if !stable.is_empty() {
98 return stable
99 .into_iter()
100 .max_by_key(|l| Version::parse(&l.vers).ok());
101 }
102
103 lines
105 .iter()
106 .filter(|l| !l.yanked)
107 .max_by_key(|l| Version::parse(&l.vers).ok())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_compute_path_1_char() {
116 assert_eq!(compute_path("a"), "1/a");
117 }
118
119 #[test]
120 fn test_compute_path_2_chars() {
121 assert_eq!(compute_path("io"), "2/io");
122 }
123
124 #[test]
125 fn test_compute_path_3_chars() {
126 assert_eq!(compute_path("url"), "3/u/url");
127 }
128
129 #[test]
130 fn test_compute_path_4_plus_chars() {
131 assert_eq!(compute_path("serde"), "se/rd/serde");
132 }
133
134 #[test]
135 fn test_compute_path_uppercase() {
136 assert_eq!(compute_path("SERDE"), "se/rd/serde");
137 }
138
139 #[test]
140 fn test_find_latest_stable_ignores_yanked() {
141 let lines = vec![
142 make_line("1.0.0", false, false),
143 make_line("1.1.0", true, false), make_line("0.9.0", false, false),
145 ];
146 let latest = find_latest_stable(&lines).unwrap();
147 assert_eq!(latest.vers, "1.0.0");
148 }
149
150 #[test]
151 fn test_find_latest_stable_ignores_prerelease() {
152 let lines = vec![
153 make_line("1.0.0", false, false),
154 make_line("1.1.0-alpha.1", false, true),
155 make_line("0.9.0", false, false),
156 ];
157 let latest = find_latest_stable(&lines).unwrap();
158 assert_eq!(latest.vers, "1.0.0");
159 }
160
161 #[test]
162 fn test_find_latest_stable_fallback_to_prerelease() {
163 let lines = vec![
164 make_line("1.0.0-alpha.1", false, true),
165 make_line("0.9.0-beta.1", false, true),
166 ];
167 let latest = find_latest_stable(&lines).unwrap();
168 assert_eq!(latest.vers, "1.0.0-alpha.1");
169 }
170
171 fn make_line(vers: &str, yanked: bool, _is_pre: bool) -> IndexLine {
172 IndexLine {
173 name: "test".to_string(),
174 vers: vers.to_string(),
175 deps: vec![],
176 cksum: "abc".to_string(),
177 features: Default::default(),
178 yanked,
179 rust_version: None,
180 features2: None,
181 }
182 }
183}