code_moniker_core/lang/rs/
build.rs1#[derive(Clone, Debug, Eq, PartialEq)]
2pub struct Dep {
3 pub name: String,
4 pub version: Option<String>,
5 pub dep_kind: String,
6 pub import_root: String,
7}
8
9#[derive(Debug)]
10pub enum CargoError {
11 Parse(toml::de::Error),
12 Schema(String),
13}
14
15impl std::fmt::Display for CargoError {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 Self::Parse(e) => write!(f, "Cargo.toml parse error: {e}"),
19 Self::Schema(s) => write!(f, "Cargo.toml schema error: {s}"),
20 }
21 }
22}
23
24impl std::error::Error for CargoError {}
25
26pub fn parse(content: &str) -> Result<Vec<Dep>, CargoError> {
27 let value: toml::Value = toml::from_str(content).map_err(CargoError::Parse)?;
28 let mut out = Vec::new();
29
30 if let Some(pkg) = value.get("package").and_then(|v| v.as_table()) {
31 let name = pkg
32 .get("name")
33 .and_then(|v| v.as_str())
34 .ok_or_else(|| CargoError::Schema("[package].name missing or not a string".into()))?;
35 let version = pkg
36 .get("version")
37 .and_then(|v| v.as_str())
38 .map(str::to_string);
39 out.push(Dep {
40 name: name.to_string(),
41 version,
42 dep_kind: "package".to_string(),
43 import_root: rust_import_root(name),
44 });
45 }
46
47 for (kind_table, kind_label) in [
48 ("dependencies", "normal"),
49 ("dev-dependencies", "dev"),
50 ("build-dependencies", "build"),
51 ] {
52 let Some(table) = value.get(kind_table).and_then(|v| v.as_table()) else {
53 continue;
54 };
55 for (name, spec) in table {
56 let version = extract_version(spec);
57 out.push(Dep {
58 name: name.clone(),
59 version,
60 dep_kind: kind_label.to_string(),
61 import_root: rust_import_root(name),
62 });
63 }
64 }
65
66 Ok(out)
67}
68
69pub(crate) fn rust_import_root(name: &str) -> String {
70 name.replace('-', "_")
71}
72
73pub(crate) fn extract_version(spec: &toml::Value) -> Option<String> {
74 match spec {
75 toml::Value::String(s) => Some(s.clone()),
76 toml::Value::Table(t) => t
77 .get("version")
78 .and_then(|v| v.as_str())
79 .map(str::to_string),
80 _ => None,
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn parse_minimal_package() {
90 let toml = r#"
91 [package]
92 name = "demo"
93 version = "0.1.0"
94 "#;
95 let deps = parse(toml).unwrap();
96 assert_eq!(
97 deps,
98 vec![Dep {
99 name: "demo".into(),
100 version: Some("0.1.0".into()),
101 dep_kind: "package".into(),
102 import_root: "demo".into(),
103 }]
104 );
105 }
106
107 #[test]
108 fn parse_string_dep_keeps_version() {
109 let toml = r#"
110 [package]
111 name = "demo"
112 version = "1.0.0"
113
114 [dependencies]
115 serde = "1.0"
116 "#;
117 let deps = parse(toml).unwrap();
118 assert!(deps.contains(&Dep {
119 name: "serde".into(),
120 version: Some("1.0".into()),
121 dep_kind: "normal".into(),
122 import_root: "serde".into(),
123 }));
124 }
125
126 #[test]
127 fn parse_table_dep_uses_version_field() {
128 let toml = r#"
129 [package]
130 name = "demo"
131 version = "1.0.0"
132
133 [dependencies]
134 tokio = { version = "1.40", features = ["full"] }
135 "#;
136 let deps = parse(toml).unwrap();
137 assert!(deps.contains(&Dep {
138 name: "tokio".into(),
139 version: Some("1.40".into()),
140 dep_kind: "normal".into(),
141 import_root: "tokio".into(),
142 }));
143 }
144
145 #[test]
146 fn parse_path_dep_has_no_version() {
147 let toml = r#"
148 [package]
149 name = "demo"
150 version = "1.0.0"
151
152 [dependencies]
153 local_lib = { path = "../local_lib" }
154 "#;
155 let deps = parse(toml).unwrap();
156 assert!(deps.contains(&Dep {
157 name: "local_lib".into(),
158 version: None,
159 dep_kind: "normal".into(),
160 import_root: "local_lib".into(),
161 }));
162 }
163
164 #[test]
165 fn parse_dev_and_build_dependencies_kinds() {
166 let toml = r#"
167 [package]
168 name = "demo"
169 version = "1.0.0"
170
171 [dev-dependencies]
172 criterion = "0.5"
173
174 [build-dependencies]
175 cc = "1.0"
176 "#;
177 let deps = parse(toml).unwrap();
178 assert!(
179 deps.iter()
180 .any(|d| d.name == "criterion" && d.dep_kind == "dev")
181 );
182 assert!(deps.iter().any(|d| d.name == "cc" && d.dep_kind == "build"));
183 }
184
185 #[test]
186 fn parse_hyphenated_name_normalizes_import_root() {
187 let toml = r#"
188 [package]
189 name = "demo"
190 version = "1.0.0"
191
192 [dependencies]
193 tree-sitter = "0.26"
194 multi-word-name = "1.0"
195 "#;
196 let deps = parse(toml).unwrap();
197 let ts = deps.iter().find(|d| d.name == "tree-sitter").unwrap();
198 assert_eq!(ts.import_root, "tree_sitter");
199 let mw = deps.iter().find(|d| d.name == "multi-word-name").unwrap();
200 assert_eq!(mw.import_root, "multi_word_name");
201 }
202
203 #[test]
204 fn parse_invalid_toml_returns_error() {
205 assert!(matches!(
206 parse("not [valid toml"),
207 Err(CargoError::Parse(_))
208 ));
209 }
210
211 #[test]
212 fn parse_missing_package_name_is_schema_error() {
213 let toml = r#"
214 [package]
215 version = "1.0.0"
216 "#;
217 assert!(matches!(parse(toml), Err(CargoError::Schema(_))));
218 }
219}