maven_rs/pom/
editor.rs

1//! Pom Editor
2//!
3//! ## What is a Pom Editor?
4//! A pom editor is a struct that allows you to edit and create pom files.
5//! ## What is the difference between a [PomEditor] and a [crate::pom::Pom]?
6//!
7//! [crate::pom::Pom] is a struct that represents the data in a pom file. If you were to use it to edit a pom file. The data could be lost due
8//! How XML works with Serde
9//!
10//! [PomEditor] uses the crate edit-xml to parse the xml into a dom like structure.
11//! This allows for easy editing of the xml file. Without losing any data or original structure.
12
13use std::io::Write;
14
15use edit_xml::{Document, Element, ReadOptions, WriteOptions};
16mod build;
17mod dependency_management;
18mod distribution_management;
19use super::{depend::Dependency, Developer, Parent, Repository, Scm};
20use crate::editor::{
21    utils::{add_or_update_item, get_all_children_of_element, MissingElementError},
22    ElementConverter, UpdatableElement, XMLEditorError,
23};
24pub use build::*;
25pub use dependency_management::*;
26pub use distribution_management::*;
27/// A struct that allows editing and creating pom files
28/// A pom file is an xml file that follows the maven pom schema
29#[derive(Debug)]
30pub struct PomEditor {
31    /// The document is kept private to prevent the root element from being changed.
32    ///
33    /// The root element must always be a project element. As this code assumes it exists and without it. Panicking would occur.
34    document: Document,
35    pub ident_level: usize,
36}
37impl Default for PomEditor {
38    fn default() -> Self {
39        let document = Document::new_with_root("project", |project| {
40            project
41                .attribute("xmlns", "http://maven.apache.org/POM/4.0.0")
42                .attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
43                .attribute(
44                    "xsi:schemaLocation",
45                    "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd",
46                )
47        });
48        let mut editor = Self {
49            document,
50            ident_level: 2,
51        };
52
53        editor.set_model_version("4.0.0");
54        editor
55    }
56}
57macro_rules! simple_type_getter_setter {
58    (
59        $(#[$shared_docs:meta])*
60        $name:literal {
61            $(#[$set_docs:meta])*
62            set: $set:ident,
63            $(#[$get_docs:meta])*
64            get: $get:ident,
65        }
66    ) => {
67        $(#[$set_docs])*
68        $(#[$shared_docs])*
69        pub fn $set<S, O>(&mut self, value: O)
70            where
71                S: Into<String>,
72                O: Into<Option<S>>
73            {
74            let root = self.root();
75            let value: Option<S> = value.into();
76            if let Some(value) = value {
77                let value = value.into();
78                let element = crate::editor::utils::get_or_create_top_level_element(
79                    $name,
80                    &mut self.document,
81                    root,
82                );
83                element.set_text_content(&mut self.document, value);
84            }else{
85                let element = root.find(&self.document, $name);
86                if let Some(element) = element {
87                    element.detach(&mut self.document).expect("Failed to remove element");
88                }
89            }
90        }
91        $(#[$get_docs])*
92        $(#[$shared_docs])*
93        pub fn $get(&self) -> Option<String> {
94            let root = self.root();
95            let element = root.find(&self.document, $name);
96            return element.map(|x| x.text_content(&self.document));
97        }
98
99    };
100    [
101        $(
102            $(#[$shared_docs:meta])*
103            $name:literal {
104                $(#[$set_docs:meta])*
105                set: $set:ident,
106                $(#[$get_docs:meta])*
107                get: $get:ident,
108            }
109        ),*
110    ] => {
111        $(
112
113            simple_type_getter_setter! {
114                $(#[$shared_docs])*
115                $name {
116                    $(#[$set_docs])*
117                    set: $set,
118                    $(#[$get_docs])*
119                    get: $get,
120                }
121            }
122        )*
123    };
124
125}
126macro_rules! top_level_structured_type {
127    (
128        $(#[$set_docs:meta])*
129        set: $set:ident,
130        $(#[$get_docs:meta])*
131        get: $get:ident,
132        $element_name:literal => $structured_type:ident,
133    ) => {
134        $(#[$get_docs])*
135        pub fn $get(&self) -> Result<Option<$structured_type>, XMLEditorError> {
136            let root = self.root();
137            root.find(&self.document, $element_name)
138                .map(|x| $structured_type::from_element(x, &self.document))
139                .transpose()
140        }
141        $(#[$set_docs])*
142        pub fn $set<U>(&mut self, value: U) -> Result<(), XMLEditorError>
143        where
144            U: Into<Option<$structured_type>> {
145            let value: Option<$structured_type> = value.into();
146            let root = self.root();
147            let existing_element = root.find(&self.document, $element_name);
148            if let Some(value) = value{
149                if let Some(element) = existing_element {
150                    value.update_element(element, &mut self.document)?;
151                    return Ok(());
152                }
153                let new_element = value.into_element(&mut self.document)?;
154                root.push_child(&mut self.document, new_element)?;
155            }else{
156                if let Some(element) = existing_element {
157                    element.detach(&mut self.document)?;
158                }
159            }
160
161            Ok(())
162        }
163    };
164}
165
166macro_rules! list_item_getter_and_add {
167    (
168        $(#[$get_docs:meta])*
169        get: $get:ident,
170        $(#[$add_docs:meta])*
171        add: $add:ident,
172        $(#[$clear_docs:meta])*
173        clear: $clear:ident,
174        $parent:literal => $list_element:ident
175    ) => {
176        $(#[$get_docs])*
177        pub fn $get(&self) -> Result<Vec<$list_element>, XMLEditorError> {
178            let root = self.root();
179            let Some(parent_element) = root.find(&self.document, $parent)
180            else {
181                return Ok(vec![]);
182            };
183            let result =
184                get_all_children_of_element::<$list_element>(&self.document, parent_element)?;
185            Ok(result.into_iter().map(|(v, _)| v).collect())
186        }
187        $(#[$add_docs])*
188        pub fn $add(
189            &mut self,
190            value: $list_element,
191        ) -> Result<Option<$list_element>, XMLEditorError> {
192            let root = self.root();
193            let parent_element = root.find(&self.document, $parent);
194            add_or_update_item(&mut self.document, parent_element, root, value)
195        }
196        $(#[$clear_docs])*
197        pub fn  $clear(&mut self)-> Result<(), XMLEditorError> {
198            let root = self.root();
199            let parent_element = root.find(&self.document, $parent);
200            if let Some(parent_element) = parent_element {
201                parent_element.clear_children(&mut self.document);
202            }
203            Ok(())
204        }
205    };
206}
207impl PomEditor {
208    /// Creates a new [PomEditor] with the group id and artifact id set
209    pub fn new_with_group_and_artifact(group_id: &str, artifact_id: &str) -> Self {
210        let mut editor = Self::default();
211        editor.set_group_id(group_id);
212        editor.set_artifact_id(artifact_id);
213        editor
214    }
215    top_level_structured_type!(
216        /// Sets the parent of the pom file
217        ///
218        /// If [None] is passed in. The parent element will be removed
219        set: set_parent,
220        /// Gets the parent of the pom file
221        get: get_parent,
222        "parent" => Parent,
223    );
224    top_level_structured_type!(
225        /// Sets the scm of the pom file
226        ///
227        /// If [None] is passed in. The scm element will be removed
228        set: set_scm,
229        /// Gets the scm of the pom file
230        get: get_scm,
231        "scm" => Scm,
232    );
233    simple_type_getter_setter![
234        /// The group id of the pom
235        ///
236        /// [More Info](https://maven.apache.org/pom.html#maven-coordinates)
237        /// Example Usage:
238        /// ```rust
239        /// use maven_rs::pom::editor::PomEditor;
240        /// let mut editor = PomEditor::default();
241        /// editor.set_group_id("dev.wyatt-herkamp");
242        /// assert_eq!(editor.get_group_id(), Some("dev.wyatt-herkamp".to_string()));
243        /// ```
244        "groupId" {
245            /// Sets the group Id in the pom file. For the maven project
246            set: set_group_id,
247            get: get_group_id,
248        },
249        /// The artifact id of the pom
250        /// [More Info](https://maven.apache.org/pom.html#maven-coordinates)
251        ///
252        /// Example Usage:
253        /// ```rust
254        /// use maven_rs::pom::editor::PomEditor;
255        /// let mut editor = PomEditor::default();
256        /// editor.set_artifact_id("test");
257        /// assert_eq!(editor.get_artifact_id(), Some("test".to_string()));
258        /// ```
259        "artifactId" {
260            set: set_artifact_id,
261            get: get_artifact_id,
262        },
263        /// The version of the project file
264        /// [More Info](https://maven.apache.org/pom.html#maven-coordinates)
265        ///
266        /// Example Usage:
267        /// ```rust
268        /// use maven_rs::pom::editor::PomEditor;
269        /// let mut editor = PomEditor::default();
270        /// editor.set_version("1.0.0");
271        /// assert_eq!(editor.get_version(), Some("1.0.0".to_string()));
272        /// ```
273        "version" {
274            set: set_version,
275            get: get_version,
276        },
277        /// The name of the maven project
278        ///
279        /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
280        "name" {
281            set: set_name,
282            get: get_name,
283        },
284        /// The description of the maven project
285        ///
286        /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
287        "description" {
288            set: set_description,
289            get: get_description,
290        },
291        /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
292        "url" {
293            set: set_url,
294            get: get_url,
295        },
296        /// The inception year of the project
297        "inceptionYear" {
298            set: set_inception_year,
299            get: get_inception_year,
300        },
301        /// Sets the model version of the pom file
302        ///
303        /// The model version is currently always 4.0.0
304        "modelVersion" {
305            /// Sets the model version of the pom file
306            set: set_model_version,
307            /// Gets the model version of the pom file
308            get: get_model_version,
309        },
310        /// [More Info](https://maven.apache.org/pom.html#Packaging)
311        "packaging" {
312            set: set_packaging,
313            get: get_packaging,
314        }
315    ];
316
317    list_item_getter_and_add!(
318        /// Gets all the repositories in the pom file
319        /// ```rust
320        /// use maven_rs::pom::editor::PomEditor;
321        /// use maven_rs::pom::Repository;
322        /// let xml = r#"
323        /// <project>
324        ///   <repositories>
325        ///    <repository>
326        ///      <id>central</id>
327        ///      <name>Maven Central Repository</name>
328        ///      <url>https://repo.maven.apache.org/maven2</url>
329        ///   </repository>
330        /// </repositories>
331        /// </project>
332        /// "#;
333        /// let editor = PomEditor::load_from_str(xml).unwrap();
334        /// let repositories = editor.get_repositories().unwrap();
335        /// assert_eq!(repositories.len(), 1);
336        /// assert_eq!(repositories[0].id, Some("central".to_string()));
337        get: get_repositories,
338        /// Adds or Updates a repository in the pom file
339        /// ```rust
340        /// use maven_rs::pom::editor::PomEditor;
341        /// use maven_rs::pom::Repository;
342        /// let mut editor = PomEditor::default();
343        /// editor.add_or_update_repository(Repository {
344        ///   id: Some("central".to_string()),
345        ///   name: Some("Maven Central Repository".to_string()),
346        ///   url: "https://repo.maven.apache.org/maven2".to_string(),
347        ///   ..Default::default()
348        /// }).unwrap();
349        /// let repositories = editor.get_repositories().unwrap();
350        /// assert_eq!(repositories.len(), 1);
351        /// assert_eq!(repositories[0].id, Some("central".to_string()));
352        /// ```
353        add: add_or_update_repository,
354        /// Clears all the repositories in the pom file
355        clear: clear_repositories,
356        "repositories" => Repository
357    );
358    list_item_getter_and_add!(
359        /// Gets all the developers in the pom file
360        /// ```rust
361        /// use maven_rs::pom::editor::PomEditor;
362        /// use maven_rs::pom::Developer;
363        /// let xml = r#"
364        /// <project>
365        ///    <developers>
366        ///       <developer>
367        ///         <id>dev.wyatt-herkamp</id>
368        ///         <name>Wyatt Herkamp</name>
369        ///        </developer>
370        ///   </developers>
371        /// </project>
372        /// "#;
373        /// let editor = PomEditor::load_from_str(xml).unwrap();
374        /// let developers = editor.get_developers().unwrap();
375        /// assert_eq!(developers.len(), 1);
376        /// assert_eq!(developers[0].id, Some("dev.wyatt-herkamp".to_string()));
377        /// ```
378        get: get_developers,
379        /// Adds or Updates a developer in the pom file
380        /// ```rust
381        /// use maven_rs::pom::editor::PomEditor;
382        /// use maven_rs::pom::Developer;
383        /// let mut editor = PomEditor::default();
384        /// editor.add_or_update_developer(Developer {
385        ///    id: Some("dev.wyatt-herkamp".to_string()),
386        ///    name: Some("Wyatt Herkamp".to_string()),
387        ///    ..Default::default()
388        /// }).unwrap();
389        /// let developers = editor.get_developers().unwrap();
390        /// assert_eq!(developers.len(), 1);
391        /// assert_eq!(developers[0].id, Some("dev.wyatt-herkamp".to_string()));
392        /// ```
393        add: add_or_update_developer,
394        /// Clears all the developers in the pom file
395        clear: clear_developers,
396        "developers" => Developer
397    );
398    list_item_getter_and_add!(
399        /// Gets all the dependencies in the pom file
400        /// ```rust
401        /// use maven_rs::pom::editor::PomEditor;
402        /// use maven_rs::pom::Dependency;
403        /// let xml = r#"
404        /// <project>
405        ///  <dependencies>
406        ///   <dependency>
407        ///    <groupId>com.google.guava</groupId>
408        ///    <artifactId>guava</artifactId>
409        ///    <version>30.1-jre</version>
410        /// </dependency>
411        /// </dependencies>
412        /// </project>
413        /// "#;
414        /// let editor = PomEditor::load_from_str(xml).unwrap();
415        /// let dependencies = editor.get_dependencies().unwrap();
416        /// assert_eq!(dependencies.len(), 1);
417        /// assert_eq!(dependencies[0].group_id, "com.google.guava".to_string());
418        /// ```
419        get: get_dependencies,
420        /// Adds or Updates a dependency in the pom file
421        ///
422        /// ```rust
423        /// use maven_rs::pom::editor::PomEditor;
424        /// use maven_rs::pom::Dependency;
425        /// let mut editor = PomEditor::default();
426        /// editor.add_or_update_dependency(Dependency {
427        ///  group_id: "com.google.guava".to_string(),
428        /// artifact_id: "guava".to_string(),
429        /// version: Some("30.1-jre".parse().unwrap()),
430        /// ..Default::default()
431        /// }).unwrap();
432        /// let dependencies = editor.get_dependencies().unwrap();
433        /// assert_eq!(dependencies.len(), 1);
434        /// assert_eq!(dependencies[0].group_id, "com.google.guava".to_string());
435        /// ```
436        add: add_or_update_dependency,
437        /// Clears all the dependencies in the pom file
438        clear: clear_dependencies,
439        "dependencies" => Dependency
440    );
441    // TODO:  pluginRepositories
442    // Loads a pom from a string
443    pub fn load_from_str(value: &str) -> Result<Self, XMLEditorError> {
444        let document = Document::parse_str_with_opts(
445            value,
446            ReadOptions {
447                require_decl: false,
448                ..Default::default()
449            },
450        )?;
451        Self::assert_requirements_for_pom(&document)?;
452        Ok(Self {
453            document,
454            ident_level: 2,
455        })
456    }
457    /// Loads a pom from a reader
458    ///
459    /// # Errors
460    /// If the xml is not a valid pom file
461    pub fn load_from_reader<R: std::io::Read>(reader: R) -> Result<Self, XMLEditorError> {
462        let document = Document::parse_reader_with_opts(
463            reader,
464            ReadOptions {
465                require_decl: false,
466                ..Default::default()
467            },
468        )?;
469        Self::assert_requirements_for_pom(&document)?;
470        Ok(Self {
471            document,
472            ident_level: 2,
473        })
474    }
475
476    /// Assets that the document has an root element of project
477    fn assert_requirements_for_pom(document: &Document) -> Result<(), XMLEditorError> {
478        let root = document
479            .root_element()
480            .ok_or(MissingElementError("project"))?;
481        if root.name(document) != "project" {
482            return Err(XMLEditorError::UnexpectedElementType {
483                expected: "project",
484                found: root.name(document).to_owned(),
485            });
486        }
487        Ok(())
488    }
489
490    pub(crate) fn root(&self) -> Element {
491        self.document.root_element().unwrap()
492    }
493    pub fn write_to_str(&self) -> Result<String, XMLEditorError> {
494        self.document
495            .write_str_with_opts(WriteOptions {
496                write_decl: true,
497                indent_size: self.ident_level,
498                ..Default::default()
499            })
500            .map_err(XMLEditorError::from)
501    }
502    pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), XMLEditorError> {
503        self.document
504            .write_with_opts(
505                writer,
506                WriteOptions {
507                    write_decl: true,
508                    indent_size: self.ident_level,
509                    ..Default::default()
510                },
511            )
512            .map_err(XMLEditorError::from)
513    }
514}
515
516#[cfg(test)]
517mod tests {
518
519    use super::*;
520    #[test]
521    pub fn create() -> anyhow::Result<()> {
522        let mut editor = PomEditor::default();
523        editor.set_group_id("dev.wyatt-herkamp");
524        editor.set_artifact_id("test");
525        let value = editor.write_to_str()?;
526        println!("{}", value);
527        let mut new_editor = PomEditor::load_from_str(value.as_str())?;
528
529        // Make sure the group id and artifact id are correct
530        assert_eq!(
531            new_editor.get_group_id(),
532            Some("dev.wyatt-herkamp".to_string())
533        );
534        assert_eq!(new_editor.get_artifact_id(), Some("test".to_string()));
535        // Try Changing the group id and artifact id
536        new_editor.set_group_id("dev.wyatt-herkamp2");
537        new_editor.set_artifact_id("test2");
538        assert_eq!(
539            new_editor.get_group_id(),
540            Some("dev.wyatt-herkamp2".to_string())
541        );
542        assert_eq!(new_editor.get_artifact_id(), Some("test2".to_string()));
543        let value = new_editor.write_to_str()?;
544        println!("{}", value);
545        Ok(())
546    }
547    #[test]
548    pub fn dependencies() -> anyhow::Result<()> {
549        let mut editor = PomEditor::new_with_group_and_artifact("dev.wyatt-herkamp", "test");
550        let dependency = Dependency {
551            group_id: "com.google.guava".to_string(),
552            artifact_id: "guava".to_string(),
553            version: Some("30.1-jre".parse().unwrap()),
554            depend_type: None,
555            scope: None,
556            classifier: None,
557        };
558        editor.add_or_update_dependency(dependency.clone())?;
559
560        let value = editor.write_to_str()?;
561        println!("{}", value);
562
563        let new_editor = PomEditor::load_from_str(value.as_str())?;
564        let dependencies = new_editor.get_dependencies()?;
565        println!("{:#?}", dependencies);
566        assert!(dependencies.len() == 1);
567        assert_eq!(dependencies[0], dependency);
568
569        Ok(())
570    }
571
572    #[test]
573    pub fn repositories() -> anyhow::Result<()> {
574        let mut editor = PomEditor::new_with_group_and_artifact("dev.wyatt-herkamp", "test");
575        let repository = Repository {
576            id: Some("central".to_string()),
577            name: Some("Maven Central Repository".to_string()),
578            url: "https://repo.maven.apache.org/maven2".to_string(),
579            ..Default::default()
580        };
581        editor.add_or_update_repository(repository.clone())?;
582
583        let value = editor.write_to_str()?;
584        println!("{}", value);
585
586        let new_editor = PomEditor::load_from_str(value.as_str())?;
587        let repositories = new_editor.get_repositories()?;
588        println!("{:#?}", repositories);
589        assert!(repositories.len() == 1);
590        assert_eq!(repositories[0], repository);
591
592        Ok(())
593    }
594}