Skip to main content

code_moniker_core/lang/java/
build.rs

1use roxmltree::{Document, Node};
2
3#[derive(Clone, Debug, Eq, PartialEq)]
4pub struct Dep {
5	pub name: String,
6	pub version: Option<String>,
7	pub dep_kind: String,
8	pub import_root: String,
9}
10
11#[derive(Debug)]
12pub enum PomXmlError {
13	Parse(String),
14	Schema(String),
15}
16
17impl std::fmt::Display for PomXmlError {
18	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19		match self {
20			Self::Parse(e) => write!(f, "pom.xml parse error: {e}"),
21			Self::Schema(s) => write!(f, "pom.xml schema error: {s}"),
22		}
23	}
24}
25
26impl std::error::Error for PomXmlError {}
27
28pub fn parse(content: &str) -> Result<Vec<Dep>, PomXmlError> {
29	let doc = Document::parse(content).map_err(|e| PomXmlError::Parse(e.to_string()))?;
30	let root = doc.root_element();
31	if root.tag_name().name() != "project" {
32		return Err(PomXmlError::Schema(format!(
33			"top-level element is <{}>, expected <project>",
34			root.tag_name().name()
35		)));
36	}
37
38	let mut out = Vec::new();
39
40	let group = direct_child_text(root, "groupId");
41	let artifact = direct_child_text(root, "artifactId");
42	if let Some(artifact) = artifact {
43		let version = direct_child_text(root, "version").map(str::to_string);
44		let coord = coord(group.unwrap_or(""), artifact);
45		out.push(Dep {
46			name: coord.clone(),
47			version,
48			dep_kind: "package".into(),
49			import_root: coord,
50		});
51	}
52
53	if let Some(deps_node) = direct_child(root, "dependencies") {
54		for dep in deps_node.children().filter(is_dependency) {
55			let g = direct_child_text(dep, "groupId").unwrap_or("");
56			let a = direct_child_text(dep, "artifactId").unwrap_or("");
57			if a.is_empty() {
58				continue;
59			}
60			let version = direct_child_text(dep, "version").map(str::to_string);
61			let scope = direct_child_text(dep, "scope")
62				.map(str::to_string)
63				.unwrap_or_else(|| "compile".into());
64			let coord = coord(g, a);
65			out.push(Dep {
66				name: coord.clone(),
67				version,
68				dep_kind: scope,
69				import_root: coord,
70			});
71		}
72	}
73
74	Ok(out)
75}
76
77fn coord(group: &str, artifact: &str) -> String {
78	if group.is_empty() {
79		artifact.to_string()
80	} else {
81		format!("{group}:{artifact}")
82	}
83}
84
85fn is_dependency(n: &Node<'_, '_>) -> bool {
86	n.is_element() && n.tag_name().name() == "dependency"
87}
88
89// Single-head shape; non-stdlib refs use `lang:java/package:…` so this row
90// is emitted for column uniformity but does not bind via `@>`.
91pub fn package_moniker(project: &[u8], import_root: &str) -> crate::core::moniker::Moniker {
92	let mut b = crate::core::moniker::MonikerBuilder::new();
93	b.project(project);
94	b.segment(crate::lang::kinds::EXTERNAL_PKG, import_root.as_bytes());
95	b.build()
96}
97
98fn direct_child<'a, 'input>(parent: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
99	parent
100		.children()
101		.find(|c| c.is_element() && c.tag_name().name() == name)
102}
103
104fn direct_child_text<'a>(parent: Node<'a, '_>, name: &str) -> Option<&'a str> {
105	direct_child(parent, name).and_then(|n| n.text().map(str::trim).filter(|s| !s.is_empty()))
106}
107
108#[cfg(test)]
109mod tests {
110	use super::*;
111
112	#[test]
113	fn parse_minimal_project() {
114		let xml = r#"
115            <project>
116                <groupId>com.example</groupId>
117                <artifactId>demo</artifactId>
118                <version>0.1.0</version>
119            </project>
120        "#;
121		let deps = parse(xml).unwrap();
122		assert_eq!(
123			deps,
124			vec![Dep {
125				name: "com.example:demo".into(),
126				version: Some("0.1.0".into()),
127				dep_kind: "package".into(),
128				import_root: "com.example:demo".into(),
129			}]
130		);
131	}
132
133	#[test]
134	fn parse_compile_dep_keeps_version() {
135		let xml = r#"
136            <project>
137                <groupId>com.example</groupId>
138                <artifactId>demo</artifactId>
139                <version>0.1.0</version>
140                <dependencies>
141                    <dependency>
142                        <groupId>com.google.guava</groupId>
143                        <artifactId>guava</artifactId>
144                        <version>33.0.0-jre</version>
145                    </dependency>
146                </dependencies>
147            </project>
148        "#;
149		let deps = parse(xml).unwrap();
150		assert!(deps.contains(&Dep {
151			name: "com.google.guava:guava".into(),
152			version: Some("33.0.0-jre".into()),
153			dep_kind: "compile".into(),
154			import_root: "com.google.guava:guava".into(),
155		}));
156	}
157
158	#[test]
159	fn parse_scope_test_tagged_dep_kind_test() {
160		let xml = r#"
161            <project>
162                <groupId>com.example</groupId>
163                <artifactId>demo</artifactId>
164                <version>0.1.0</version>
165                <dependencies>
166                    <dependency>
167                        <groupId>junit</groupId>
168                        <artifactId>junit</artifactId>
169                        <version>4.13.2</version>
170                        <scope>test</scope>
171                    </dependency>
172                </dependencies>
173            </project>
174        "#;
175		let deps = parse(xml).unwrap();
176		let junit = deps.iter().find(|d| d.name == "junit:junit").unwrap();
177		assert_eq!(junit.dep_kind, "test");
178	}
179
180	#[test]
181	fn parse_dep_without_groupid_uses_artifact_only() {
182		let xml = r#"
183            <project>
184                <artifactId>demo</artifactId>
185                <dependencies>
186                    <dependency>
187                        <artifactId>orphan</artifactId>
188                    </dependency>
189                </dependencies>
190            </project>
191        "#;
192		let deps = parse(xml).unwrap();
193		assert!(deps.iter().any(|d| d.name == "orphan"));
194	}
195
196	#[test]
197	fn parse_invalid_xml_returns_parse_error() {
198		assert!(matches!(parse("<project>"), Err(PomXmlError::Parse(_))));
199	}
200
201	#[test]
202	fn parse_non_project_root_is_schema_error() {
203		assert!(matches!(
204			parse("<settings></settings>"),
205			Err(PomXmlError::Schema(_))
206		));
207	}
208}