Skip to main content

code_moniker_core/lang/rs/
build.rs

1#[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}