code_moniker_core/lang/java/
pom_manifest.rs1use 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
91pub 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}