1use std::str::FromStr;
2#[cfg(feature = "resolver")]
3pub mod resolve;
4use crate::{
5 editor::{
6 ChildOfListElement, ComparableElement, ElementConverter, HasElementName, UpdatableElement,
7 XMLEditorError,
8 utils::{
9 add_if_present, create_basic_text_element, find_or_create_then_set_text_content,
10 sync_element, typed_from_element_using_builder,
11 },
12 },
13 types::Property,
14 utils::group_id_and_artifact_id_and_version_to_path,
15};
16use derive_builder::Builder;
17use edit_xml::{Document, Element};
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21#[derive(Debug, Serialize, Deserialize, Clone, Default)]
22pub struct Dependencies {
23 #[serde(default, rename = "dependency")]
24 pub dependencies: Vec<Dependency>,
25}
26#[derive(Debug, Error)]
27pub enum DependencyParseError {
28 #[error("Missing artifact id")]
29 MissingArtifactId,
30 #[error("Missing Version")]
31 MissingVersion,
32 #[error("Missing Separator")]
33 MissingSeparator,
34}
35#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Builder)]
37#[serde(rename_all = "camelCase")]
38pub struct Dependency {
39 #[builder(setter(into))]
44 pub group_id: String,
45 #[builder(setter(into))]
50 pub artifact_id: String,
51 #[builder(default, setter(into, strip_option))]
57 pub version: Option<Property>,
58 #[builder(default, setter(into, strip_option))]
63 #[serde(rename = "type")]
64 pub depend_type: Option<String>,
65 #[builder(default, setter(into, strip_option))]
66 pub scope: Option<String>,
67 #[builder(default, setter(into, strip_option))]
68 pub classifier: Option<String>,
69}
70
71impl Dependency {
72 pub fn is_same_dependency(&self, other: &Dependency) -> bool {
76 self.group_id == other.group_id && self.artifact_id == other.artifact_id
77 }
78
79 pub fn pom_name(&self) -> String {
80 let version = self.version.clone().unwrap_or_default();
81 format!("{}-{}.pom", self.artifact_id, version)
82 }
83 pub fn pom_path(&self) -> String {
84 let version = self.version.clone().unwrap_or_default();
85
86 let path = group_id_and_artifact_id_and_version_to_path(
87 &self.group_id,
88 &self.artifact_id,
89 &version.to_string(),
90 );
91 format!("{}/{}", path, self.pom_name())
92 }
93}
94impl ChildOfListElement for Dependency {
95 fn parent_element_name() -> &'static str {
96 "dependencies"
97 }
98}
99impl ComparableElement for Dependency {
100 fn is_same_item(&self, other: &Self) -> bool {
101 self.is_same_dependency(other)
102 }
103}
104impl UpdatableElement for Dependency {
105 fn update_element(
106 &self,
107 element: Element,
108 document: &mut Document,
109 ) -> Result<(), XMLEditorError> {
110 sync_element(
111 document,
112 element,
113 "version",
114 self.version.as_ref().map(|v| v.to_string()),
115 );
116 if let Some(depend_type) = &self.depend_type {
117 find_or_create_then_set_text_content(document, element, "type", depend_type);
118 }
119 if let Some(scope) = &self.scope {
120 find_or_create_then_set_text_content(document, element, "scope", scope);
121 }
122 if let Some(classifier) = &self.classifier {
123 find_or_create_then_set_text_content(document, element, "classifier", classifier);
124 }
125 Ok(())
126 }
127}
128impl TryFrom<&str> for Dependency {
129 type Error = DependencyParseError;
130
131 fn try_from(value: &str) -> Result<Self, Self::Error> {
132 if value.is_empty() || !value.contains(":") {
133 return Err(DependencyParseError::MissingSeparator);
134 }
135 let parts: Vec<&str> = value.split(':').collect();
136 let group_id = parts.first().unwrap().to_string();
137 let artifact_id = parts
138 .get(1)
139 .ok_or(DependencyParseError::MissingArtifactId)?
140 .to_string();
141 let version = parts
142 .get(2)
143 .ok_or(DependencyParseError::MissingVersion)?
144 .to_string();
145 let version = Property::Literal(version);
146 let result = Dependency {
149 group_id,
150 artifact_id,
151 version: Some(version),
152 depend_type: None,
153 scope: None,
154 classifier: None,
155 };
156 Ok(result)
157 }
158}
159impl TryFrom<String> for Dependency {
160 type Error = DependencyParseError;
161
162 fn try_from(value: String) -> Result<Self, Self::Error> {
163 Dependency::try_from(value.as_str())
164 }
165}
166impl FromStr for Dependency {
167 type Err = DependencyParseError;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 Dependency::try_from(s)
171 }
172}
173
174impl std::fmt::Display for Dependency {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 let version = self.version.clone().unwrap_or_default();
177 write!(f, "{}:{}:{}", self.group_id, self.artifact_id, version)
178 }
179}
180impl HasElementName for Dependency {
181 fn element_name() -> &'static str {
182 "dependency"
183 }
184}
185impl ElementConverter for Dependency {
186 typed_from_element_using_builder!(
187 DependencyBuilder,
188 element,
189 document,
190 "groupId"(String) => group_id,
191 "artifactId"(String) => artifact_id,
192 "version"(Property) => version,
193 "type"(String) => depend_type,
194 "scope"(String) => scope,
195 "classifier"(String) => classifier
196 );
197 fn into_children(self, document: &mut Document) -> Result<Vec<Element>, XMLEditorError> {
198 let Self {
199 group_id,
200 artifact_id,
201 version,
202 depend_type,
203 scope,
204 classifier,
205 } = self;
206
207 let mut children = vec![
208 create_basic_text_element(document, "groupId", group_id),
209 create_basic_text_element(document, "artifactId", artifact_id),
210 ];
211 add_if_present!(document, children, version, "version");
212 add_if_present!(document, children, depend_type, "type");
213 add_if_present!(document, children, scope, "scope");
214 add_if_present!(document, children, classifier, "classifier");
215
216 Ok(children)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222
223 use pretty_assertions::assert_eq;
224
225 use crate::{
226 editor::utils::test_utils,
227 utils::bug_testing::{self, BugFile},
228 };
229
230 pub use super::*;
231 #[test]
232 fn test_simple() {
233 let dep = Dependency {
234 group_id: "com.google.guava".to_string(),
235 artifact_id: "guava".to_string(),
236 version: Some("30.1-jre".parse().unwrap()),
237 depend_type: None,
238 scope: None,
239 classifier: None,
240 };
241 let dep_str = "com.google.guava:guava:30.1-jre";
242 assert_eq!(dep, Dependency::try_from(dep_str).unwrap());
243 assert_eq!(dep_str, dep.to_string());
244 }
245
246 #[test]
247 fn invalid_dependency_syntax() {
248 assert!(Dependency::try_from("".to_string()).is_err());
249 assert!(Dependency::from_str("com.google.guava").is_err());
250 }
251 #[test]
252 pub fn test_is_same_dependency() {
253 let dep = Dependency {
254 group_id: "com.google.guava".to_string(),
255 artifact_id: "guava".to_string(),
256 version: Some("30.1-jre".parse().unwrap()),
257 depend_type: None,
258 scope: None,
259 classifier: None,
260 };
261 let dep2 = Dependency {
262 group_id: "com.google.guava".to_string(),
263 artifact_id: "guava".to_string(),
264 version: Some("30.2-jre".parse().unwrap()),
265 depend_type: None,
266 scope: None,
267 classifier: None,
268 };
269 assert!(
270 dep.is_same_dependency(&dep2),
271 "Dependencies should be the same. Because the group id and artifact id are the same."
272 );
273 }
274 fn test_parse_methods(value: &str, expected: Dependency) -> anyhow::Result<()> {
275 let dep_via_edit_xml = test_utils::create_xml_to_element::<Dependency>(value)?;
276 let dep_via_serde: Dependency = quick_xml::de::from_str(value)?;
277
278 assert_eq!(dep_via_edit_xml, expected);
279 assert_eq!(dep_via_serde, expected);
280 println!("{:#?}", dep_via_edit_xml);
281
282 let dep_serialize_serde = quick_xml::se::to_string(&expected)?;
283 println!("Serialized Over Serde \n {}", dep_serialize_serde);
284 Ok(())
285 }
286 #[test]
287 pub fn parse_full() -> anyhow::Result<()> {
288 let test_value = r#"
289 <dependency>
290 <groupId>com.google.guava</groupId>
291 <artifactId>guava</artifactId>
292 <version>30.1-jre</version>
293 <type>jar</type>
294 <scope>compile</scope>
295 <classifier>tests</classifier>
296 </dependency>
297 "#;
298 test_parse_methods(
299 test_value,
300 Dependency {
301 group_id: "com.google.guava".to_string(),
302 artifact_id: "guava".to_string(),
303 version: Some("30.1-jre".parse().unwrap()),
304 depend_type: Some("jar".to_string()),
305 scope: Some("compile".to_string()),
306 classifier: Some("tests".to_string()),
307 },
308 )?;
309 Ok(())
310 }
311
312 #[test]
313 pub fn parse_min() -> anyhow::Result<()> {
314 let test_value = r#"
315 <dependency>
316 <groupId>com.google.guava</groupId>
317 <artifactId>guava</artifactId>
318 <version>30.1-jre</version>
319
320 </dependency>
321 "#;
322 test_parse_methods(
323 test_value,
324 Dependency {
325 group_id: "com.google.guava".to_string(),
326 artifact_id: "guava".to_string(),
327 version: Some("30.1-jre".parse().unwrap()),
328 ..Default::default()
329 },
330 )?;
331 Ok(())
332 }
333 #[test]
334 pub fn parse_no_version() -> anyhow::Result<()> {
335 let test_value = r#"
336 <dependency>
337 <groupId>com.google.guava</groupId>
338 <artifactId>guava</artifactId>
339 </dependency>
340 "#;
341 test_parse_methods(
342 test_value,
343 Dependency {
344 group_id: "com.google.guava".to_string(),
345 artifact_id: "guava".to_string(),
346 version: None,
347 ..Default::default()
348 },
349 )?;
350 Ok(())
351 }
352
353 #[test]
354 pub fn test_found_bugs() -> anyhow::Result<()> {
355 let depend_bugs_dir = bug_testing::get_bugs_path().join("depends");
356 let depend_bugs = depend_bugs_dir.read_dir()?;
357 for bug in depend_bugs {
358 let bug = bug?;
359 let bug_path = bug.path();
360 let bug_file = std::fs::read_to_string(&bug_path)?;
361 let bug: BugFile = toml::de::from_str(&bug_file)?;
362 if !bug.depends.is_empty() {
363 println!("Bug File: \n {}", bug.source);
364 println!("Error: {}", bug.error);
365 for found_bug in bug.depends {
366 println!("Testing Dependency: {:?}", found_bug.expected);
367 let expected_depends: Dependency = found_bug.expected.into();
368 println!("Expected Dependency: {}", expected_depends);
369 test_parse_methods(&found_bug.xml, expected_depends.clone())?;
370 }
371 }
372 }
373 Ok(())
374 }
375 #[test]
376 fn test_pom_name_and_path() {
377 let dep = Dependency {
378 group_id: "com.google.guava".to_string(),
379 artifact_id: "guava".to_string(),
380 version: Some("30.1-jre".parse().unwrap()),
381 depend_type: None,
382 scope: None,
383 classifier: None,
384 };
385 assert_eq!(dep.pom_name(), "guava-30.1-jre.pom");
386 assert_eq!(
387 dep.pom_path(),
388 "com/google/guava/guava/30.1-jre/guava-30.1-jre.pom"
389 );
390 }
391 #[test]
392 fn update_element_test() -> anyhow::Result<()> {
393 let actual_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
394 <dependency>
395 <groupId>com.google.guava</groupId>
396 <artifactId>guava</artifactId>
397 <version>30.1-jre</version>
398 </dependency>
399 "#;
400 let mut document = edit_xml::Document::parse_str(actual_xml).unwrap();
401 let Some(raw_element) = document.root_element() else {
402 println!("{}", actual_xml);
403 panic!("No root element found");
404 };
405
406 let dep = Dependency {
407 group_id: "com.google.guava".to_string(),
408 artifact_id: "guava".to_string(),
409 version: Some("30.1-jre".parse().unwrap()),
410 depend_type: Some("jar".to_string()),
411 scope: Some("compile".to_string()),
412 classifier: Some("tests".to_string()),
413 };
414
415 dep.update_element(raw_element, &mut document)?;
416
417 let new_xml = document.write_str()?;
418 let expected_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
419<dependency>
420 <groupId>com.google.guava</groupId>
421 <artifactId>guava</artifactId>
422 <version>30.1-jre</version>
423 <type>jar</type>
424 <scope>compile</scope>
425 <classifier>tests</classifier>
426</dependency>"#;
427 assert_eq!(new_xml, expected_xml);
428 Ok(())
429 }
430}