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 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}