Skip to main content

code_moniker_core/lang/java/
pom_manifest.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").or_else(|| parent_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")
44			.or_else(|| parent_child_text(root, "version"))
45			.map(str::to_string);
46		let coord = coord(group.unwrap_or(""), artifact);
47		out.push(Dep {
48			name: coord.clone(),
49			version,
50			dep_kind: "package".into(),
51			import_root: coord,
52		});
53	}
54
55	if let Some(deps_node) = direct_child(root, "dependencies") {
56		for dep in deps_node.children().filter(is_dependency) {
57			let g = direct_child_text(dep, "groupId").unwrap_or("");
58			let a = direct_child_text(dep, "artifactId").unwrap_or("");
59			if a.is_empty() {
60				continue;
61			}
62			let version = direct_child_text(dep, "version").map(str::to_string);
63			let scope = direct_child_text(dep, "scope")
64				.map(str::to_string)
65				.unwrap_or_else(|| "compile".into());
66			let coord = coord(g, a);
67			out.push(Dep {
68				name: coord.clone(),
69				version,
70				dep_kind: scope,
71				import_root: coord,
72			});
73		}
74	}
75
76	Ok(out)
77}
78
79fn coord(group: &str, artifact: &str) -> String {
80	if group.is_empty() {
81		artifact.to_string()
82	} else {
83		format!("{group}:{artifact}")
84	}
85}
86
87fn is_dependency(n: &Node<'_, '_>) -> bool {
88	n.is_element() && n.tag_name().name() == "dependency"
89}
90
91// Single-head shape; non-stdlib refs use `lang:java/package:…` so this row
92// is emitted for column uniformity but does not bind via `@>`.
93pub fn package_moniker(project: &[u8], import_root: &str) -> crate::core::moniker::Moniker {
94	let mut b = crate::core::moniker::MonikerBuilder::new();
95	b.project(project);
96	b.segment(crate::lang::kinds::EXTERNAL_PKG, import_root.as_bytes());
97	b.build()
98}
99
100fn direct_child<'a, 'input>(parent: Node<'a, 'input>, name: &str) -> Option<Node<'a, 'input>> {
101	parent
102		.children()
103		.find(|c| c.is_element() && c.tag_name().name() == name)
104}
105
106fn direct_child_text<'a>(parent: Node<'a, '_>, name: &str) -> Option<&'a str> {
107	direct_child(parent, name).and_then(|n| n.text().map(str::trim).filter(|s| !s.is_empty()))
108}
109
110fn parent_child_text<'a>(root: Node<'a, '_>, name: &str) -> Option<&'a str> {
111	direct_child(root, "parent").and_then(|parent| direct_child_text(parent, name))
112}
113
114#[cfg(test)]
115mod tests {
116	use super::*;
117
118	#[test]
119	fn parse_minimal_project() {
120		let xml = r#"
121            <project>
122                <groupId>com.example</groupId>
123                <artifactId>demo</artifactId>
124                <version>0.1.0</version>
125            </project>
126        "#;
127		let deps = parse(xml).unwrap();
128		assert_eq!(
129			deps,
130			vec![Dep {
131				name: "com.example:demo".into(),
132				version: Some("0.1.0".into()),
133				dep_kind: "package".into(),
134				import_root: "com.example:demo".into(),
135			}]
136		);
137	}
138
139	#[test]
140	fn parse_compile_dep_keeps_version() {
141		let xml = r#"
142            <project>
143                <groupId>com.example</groupId>
144                <artifactId>demo</artifactId>
145                <version>0.1.0</version>
146                <dependencies>
147                    <dependency>
148                        <groupId>com.google.guava</groupId>
149                        <artifactId>guava</artifactId>
150                        <version>33.0.0-jre</version>
151                    </dependency>
152                </dependencies>
153            </project>
154        "#;
155		let deps = parse(xml).unwrap();
156		assert!(deps.contains(&Dep {
157			name: "com.google.guava:guava".into(),
158			version: Some("33.0.0-jre".into()),
159			dep_kind: "compile".into(),
160			import_root: "com.google.guava:guava".into(),
161		}));
162	}
163
164	#[test]
165	fn parse_scope_test_tagged_dep_kind_test() {
166		let xml = r#"
167            <project>
168                <groupId>com.example</groupId>
169                <artifactId>demo</artifactId>
170                <version>0.1.0</version>
171                <dependencies>
172                    <dependency>
173                        <groupId>junit</groupId>
174                        <artifactId>junit</artifactId>
175                        <version>4.13.2</version>
176                        <scope>test</scope>
177                    </dependency>
178                </dependencies>
179            </project>
180        "#;
181		let deps = parse(xml).unwrap();
182		let junit = deps.iter().find(|d| d.name == "junit:junit").unwrap();
183		assert_eq!(junit.dep_kind, "test");
184	}
185
186	#[test]
187	fn parse_dep_without_groupid_uses_artifact_only() {
188		let xml = r#"
189            <project>
190                <artifactId>demo</artifactId>
191                <dependencies>
192                    <dependency>
193                        <artifactId>orphan</artifactId>
194                    </dependency>
195                </dependencies>
196            </project>
197        "#;
198		let deps = parse(xml).unwrap();
199		assert!(deps.iter().any(|d| d.name == "orphan"));
200	}
201
202	#[test]
203	fn parse_project_inherits_parent_group_and_version() {
204		let xml = r#"
205            <project>
206                <parent>
207                    <groupId>com.example</groupId>
208                    <artifactId>platform</artifactId>
209                    <version>1.2.3</version>
210                </parent>
211                <artifactId>worker</artifactId>
212            </project>
213        "#;
214		let deps = parse(xml).unwrap();
215		assert_eq!(
216			deps,
217			vec![Dep {
218				name: "com.example:worker".into(),
219				version: Some("1.2.3".into()),
220				dep_kind: "package".into(),
221				import_root: "com.example:worker".into(),
222			}]
223		);
224	}
225
226	#[test]
227	fn parse_invalid_xml_returns_parse_error() {
228		assert!(matches!(parse("<project>"), Err(PomXmlError::Parse(_))));
229	}
230
231	#[test]
232	fn parse_non_project_root_is_schema_error() {
233		assert!(matches!(
234			parse("<settings></settings>"),
235			Err(PomXmlError::Schema(_))
236		));
237	}
238}