code_moniker_core/lang/java/
build.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");
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}