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
89fn direct_child<'a, 'input>(parent: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
90	parent
91		.children()
92		.find(|c| c.is_element() && c.tag_name().name() == name)
93}
94
95fn direct_child_text<'a>(parent: Node<'a, '_>, name: &str) -> Option<&'a str> {
96	direct_child(parent, name).and_then(|n| n.text().map(str::trim).filter(|s| !s.is_empty()))
97}
98
99#[cfg(test)]
100mod tests {
101	use super::*;
102
103	#[test]
104	fn parse_minimal_project() {
105		let xml = r#"
106            <project>
107                <groupId>com.example</groupId>
108                <artifactId>demo</artifactId>
109                <version>0.1.0</version>
110            </project>
111        "#;
112		let deps = parse(xml).unwrap();
113		assert_eq!(
114			deps,
115			vec![Dep {
116				name: "com.example:demo".into(),
117				version: Some("0.1.0".into()),
118				dep_kind: "package".into(),
119				import_root: "com.example:demo".into(),
120			}]
121		);
122	}
123
124	#[test]
125	fn parse_compile_dep_keeps_version() {
126		let xml = r#"
127            <project>
128                <groupId>com.example</groupId>
129                <artifactId>demo</artifactId>
130                <version>0.1.0</version>
131                <dependencies>
132                    <dependency>
133                        <groupId>com.google.guava</groupId>
134                        <artifactId>guava</artifactId>
135                        <version>33.0.0-jre</version>
136                    </dependency>
137                </dependencies>
138            </project>
139        "#;
140		let deps = parse(xml).unwrap();
141		assert!(deps.contains(&Dep {
142			name: "com.google.guava:guava".into(),
143			version: Some("33.0.0-jre".into()),
144			dep_kind: "compile".into(),
145			import_root: "com.google.guava:guava".into(),
146		}));
147	}
148
149	#[test]
150	fn parse_scope_test_tagged_dep_kind_test() {
151		let xml = r#"
152            <project>
153                <groupId>com.example</groupId>
154                <artifactId>demo</artifactId>
155                <version>0.1.0</version>
156                <dependencies>
157                    <dependency>
158                        <groupId>junit</groupId>
159                        <artifactId>junit</artifactId>
160                        <version>4.13.2</version>
161                        <scope>test</scope>
162                    </dependency>
163                </dependencies>
164            </project>
165        "#;
166		let deps = parse(xml).unwrap();
167		let junit = deps.iter().find(|d| d.name == "junit:junit").unwrap();
168		assert_eq!(junit.dep_kind, "test");
169	}
170
171	#[test]
172	fn parse_dep_without_groupid_uses_artifact_only() {
173		let xml = r#"
174            <project>
175                <artifactId>demo</artifactId>
176                <dependencies>
177                    <dependency>
178                        <artifactId>orphan</artifactId>
179                    </dependency>
180                </dependencies>
181            </project>
182        "#;
183		let deps = parse(xml).unwrap();
184		assert!(deps.iter().any(|d| d.name == "orphan"));
185	}
186
187	#[test]
188	fn parse_invalid_xml_returns_parse_error() {
189		assert!(matches!(parse("<project>"), Err(PomXmlError::Parse(_))));
190	}
191
192	#[test]
193	fn parse_non_project_root_is_schema_error() {
194		assert!(matches!(
195			parse("<settings></settings>"),
196			Err(PomXmlError::Schema(_))
197		));
198	}
199}