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 fn package_moniker(project: &[u8], import_root: &str) -> crate::core::moniker::Moniker {
74 let mut b = crate::core::moniker::MonikerBuilder::new();
75 b.project(project);
76 b.segment(crate::lang::kinds::EXTERNAL_PKG, import_root.as_bytes());
77 b.build()
78}
79
80pub(crate) fn extract_version(spec: &toml::Value) -> Option<String> {
81 match spec {
82 toml::Value::String(s) => Some(s.clone()),
83 toml::Value::Table(t) => t
84 .get("version")
85 .and_then(|v| v.as_str())
86 .map(str::to_string),
87 _ => None,
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn parse_minimal_package() {
97 let toml = r#"
98 [package]
99 name = "demo"
100 version = "0.1.0"
101 "#;
102 let deps = parse(toml).unwrap();
103 assert_eq!(
104 deps,
105 vec![Dep {
106 name: "demo".into(),
107 version: Some("0.1.0".into()),
108 dep_kind: "package".into(),
109 import_root: "demo".into(),
110 }]
111 );
112 }
113
114 #[test]
115 fn parse_string_dep_keeps_version() {
116 let toml = r#"
117 [package]
118 name = "demo"
119 version = "1.0.0"
120
121 [dependencies]
122 serde = "1.0"
123 "#;
124 let deps = parse(toml).unwrap();
125 assert!(deps.contains(&Dep {
126 name: "serde".into(),
127 version: Some("1.0".into()),
128 dep_kind: "normal".into(),
129 import_root: "serde".into(),
130 }));
131 }
132
133 #[test]
134 fn parse_table_dep_uses_version_field() {
135 let toml = r#"
136 [package]
137 name = "demo"
138 version = "1.0.0"
139
140 [dependencies]
141 tokio = { version = "1.40", features = ["full"] }
142 "#;
143 let deps = parse(toml).unwrap();
144 assert!(deps.contains(&Dep {
145 name: "tokio".into(),
146 version: Some("1.40".into()),
147 dep_kind: "normal".into(),
148 import_root: "tokio".into(),
149 }));
150 }
151
152 #[test]
153 fn parse_path_dep_has_no_version() {
154 let toml = r#"
155 [package]
156 name = "demo"
157 version = "1.0.0"
158
159 [dependencies]
160 local_lib = { path = "../local_lib" }
161 "#;
162 let deps = parse(toml).unwrap();
163 assert!(deps.contains(&Dep {
164 name: "local_lib".into(),
165 version: None,
166 dep_kind: "normal".into(),
167 import_root: "local_lib".into(),
168 }));
169 }
170
171 #[test]
172 fn parse_dev_and_build_dependencies_kinds() {
173 let toml = r#"
174 [package]
175 name = "demo"
176 version = "1.0.0"
177
178 [dev-dependencies]
179 criterion = "0.5"
180
181 [build-dependencies]
182 cc = "1.0"
183 "#;
184 let deps = parse(toml).unwrap();
185 assert!(
186 deps.iter()
187 .any(|d| d.name == "criterion" && d.dep_kind == "dev")
188 );
189 assert!(deps.iter().any(|d| d.name == "cc" && d.dep_kind == "build"));
190 }
191
192 #[test]
193 fn parse_hyphenated_name_normalizes_import_root() {
194 let toml = r#"
195 [package]
196 name = "demo"
197 version = "1.0.0"
198
199 [dependencies]
200 tree-sitter = "0.26"
201 multi-word-name = "1.0"
202 "#;
203 let deps = parse(toml).unwrap();
204 let ts = deps.iter().find(|d| d.name == "tree-sitter").unwrap();
205 assert_eq!(ts.import_root, "tree_sitter");
206 let mw = deps.iter().find(|d| d.name == "multi-word-name").unwrap();
207 assert_eq!(mw.import_root, "multi_word_name");
208 }
209
210 #[test]
211 fn parse_invalid_toml_returns_error() {
212 assert!(matches!(
213 parse("not [valid toml"),
214 Err(CargoError::Parse(_))
215 ));
216 }
217
218 #[test]
219 fn parse_missing_package_name_is_schema_error() {
220 let toml = r#"
221 [package]
222 version = "1.0.0"
223 "#;
224 assert!(matches!(parse(toml), Err(CargoError::Schema(_))));
225 }
226}