maven_rs/pom/
depend.rs

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/// A dependency in a pom file.
36#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Builder)]
37#[serde(rename_all = "camelCase")]
38pub struct Dependency {
39    /// The group id of the dependency.
40    /// ```xml
41    /// <groupId>com.google.guava</groupId>
42    /// ```
43    #[builder(setter(into))]
44    pub group_id: String,
45    /// The artifact id of the dependency.
46    /// ```xml
47    /// <artifactId>guava</artifactId>
48    /// ```
49    #[builder(setter(into))]
50    pub artifact_id: String,
51    /// The version of the dependency.
52    ///
53    /// ```xml
54    /// <version>1.0.0</version>
55    /// ```
56    #[builder(default, setter(into, strip_option))]
57    pub version: Option<Property>,
58    /// The type of the dependency.
59    /// ```xml
60    /// <type>jar</type>
61    /// ```
62    #[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    /// Checks if the dependency is the same as the other dependency.
73    ///
74    /// Basically, it checks if the group id and artifact id are the same.
75    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        // TODO: Add support for type, scope, and classifier.
147
148        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}