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
89pub 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}