maven_rs/editor/
utils.rs

1use super::{
2    ChildOfListElement, ComparableElement, ElementConverter, HasElementName, PomValue,
3    UpdatableElement, XMLEditorError,
4};
5use edit_xml::{Document, Element};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9#[error("Missing Element {0}")]
10pub struct MissingElementError(pub &'static str);
11
12pub fn find_element_or_err(
13    element: Element,
14    name: &'static str,
15    document: &Document,
16) -> Result<Element, MissingElementError> {
17    element
18        .find(document, name)
19        .ok_or(MissingElementError(name))
20}
21pub fn find_to_string_or_none(
22    element: Element,
23    name: &'static str,
24    document: &Document,
25) -> Option<String> {
26    element
27        .find(document, name)
28        .map(|x| x.text_content(document))
29}
30pub fn create_basic_text_element(
31    document: &mut Document,
32    name: impl Into<String>,
33    value: impl PomValue,
34) -> Element {
35    Element::build(name)
36        .add_text(value.to_string_for_editor())
37        .finish(document)
38}
39
40pub fn get_or_create_top_level_element(
41    name: &'static str,
42    document: &mut Document,
43    parent: Element,
44) -> Element {
45    if let Some(element) = parent.find(document, name) {
46        return element;
47    }
48    let element = Element::new(document, name);
49    parent
50        .push_child(document, element)
51        .expect("Failed to add element");
52    element
53}
54/// Finds an Element with the name of name.
55///
56/// If it does not exist, it will be created.
57///
58/// Then the text content of the element will be set to value.
59///
60/// The children of the element will be cleared.
61pub(crate) fn find_or_create_then_set_text_content(
62    document: &mut Document,
63    parent: Element,
64    name: &'static str,
65    value: impl Into<String>,
66) {
67    let element = get_or_create_top_level_element(name, document, parent);
68    element.set_text_content(document, value);
69}
70/// Syncs an element with the name of name.
71///
72/// If the value is Some, the element will be created or updated with the value.
73///
74/// If the value is None, the element will be removed.
75pub(crate) fn sync_element<V: Into<String>>(
76    document: &mut Document,
77    parent: Element,
78    name: &'static str,
79    value: Option<V>,
80) {
81    if let Some(value) = value {
82        let element = get_or_create_top_level_element(name, document, parent);
83        element.clear_children(document);
84        element.set_text_content(document, value);
85    } else {
86        let element = parent.find(document, name);
87        if let Some(element) = element {
88            element.detach(document).expect("Failed to remove element");
89        }
90    }
91}
92
93/// Adds or updates an element in a parent element. All elements in the parent element must be of the same type.
94///
95/// If the parent element does not exist, it will be created. and the element will be added to it.
96/// If the element already exists, it will be updated. Uses [UpdatableElement::is_same_item] to check if the element is the same.
97/// If the element does not exist, it will be added to the parent element.
98pub(crate) fn add_or_update_item<I>(
99    document: &mut Document,
100    parent_element: Option<Element>,
101    insert_into: Element,
102    item: I,
103) -> Result<Option<I>, XMLEditorError>
104where
105    I: UpdatableElement
106        + ElementConverter
107        + ChildOfListElement
108        + HasElementName
109        + ComparableElement,
110{
111    let Some(parent_container) = parent_element else {
112        // No parent element found element found, create it and add the dependency
113        let dependencies = Element::new(document, I::parent_element_name());
114        let value = item.into_element(document)?;
115        dependencies.push_child(document, value)?;
116        insert_into.push_child(document, dependencies)?;
117        return Ok(None);
118    };
119    let elements_in_parent = get_all_children_of_element::<I>(document, parent_container)?;
120    for (current_value, element) in elements_in_parent {
121        // A dependency with the same group_id and artifact_id is already present
122        // Update the version and return the old dependency
123        if current_value.is_same_item(&item) {
124            item.update_element(element, document)?;
125            return Ok(Some(current_value));
126        }
127    }
128    // No dependency with the same group_id and artifact_id is present
129    let value = item.into_element(document)?;
130    parent_container.push_child(document, value)?;
131    Ok(None)
132}
133/// Gets all children of an element and converts them to a specific type.
134pub(crate) fn get_all_children_of_element<E>(
135    document: &Document,
136    element: Element,
137) -> Result<Vec<(E, Element)>, XMLEditorError>
138where
139    E: ElementConverter + HasElementName,
140{
141    let mut result = vec![];
142
143    for raw_element in element.child_elements(document) {
144        let element_name = raw_element.name(document);
145        if element_name != E::element_name() {
146            return Err(XMLEditorError::UnexpectedElementType {
147                expected: E::element_name(),
148                found: element_name.to_owned(),
149            });
150        }
151        let value = E::from_element(raw_element, document)?;
152        result.push((value, raw_element));
153    }
154    Ok(result)
155}
156
157macro_rules! add_if_present {
158    (
159        $document:ident,
160        $children:ident,
161        $element:ident,
162        $name:literal
163    ) => {
164        if let Some(value) = $element {
165            $children.push(crate::editor::utils::create_basic_text_element(
166                $document, $name, value,
167            ));
168        }
169    };
170}
171
172pub(crate) use add_if_present;
173macro_rules! from_element_using_builder {
174    (
175        $builder:ident,
176        $element:ident,
177        $document:ident,
178        $(
179            $name:literal => $set_func:ident
180        ),*
181    ) => {
182        fn from_element(
183            element: edit_xml::Element,
184            document: &edit_xml::Document,
185        ) -> Result<Self, crate::editor::XMLEditorError> {
186        let mut builder = $builder::default();
187        for child in element.child_elements(document) {
188            match child.name(document) {
189                    $(
190                    $name => {
191                        builder.$set_func(child.text_content(document));
192                    }
193                    )*
194                    _ => {}
195                }
196            }
197            let result = builder.build()?;
198            return Ok(result);
199        }
200    };
201}
202pub(crate) use from_element_using_builder;
203
204macro_rules! typed_from_element_using_builder {
205    (
206        $builder:ident,
207        $element:ident,
208        $document:ident,
209        $(
210            $name:literal($element_type:ty) => $set_func:ident
211        ),*
212    ) => {
213        fn from_element(
214            element: edit_xml::Element,
215            document: &edit_xml::Document,
216        ) -> Result<Self, crate::editor::XMLEditorError> {
217        let mut builder = $builder::default();
218        for child in element.child_elements(document) {
219            match child.name(document) {
220                    $(
221                        $name => {
222                            builder.$set_func(<$element_type as crate::editor::PomValue>::from_element(child, document)?);
223                        }
224                    )*
225                    _ => {}
226                }
227            }
228            let result = builder.build()?;
229            return Ok(result);
230        }
231    };
232}
233pub(crate) use typed_from_element_using_builder;
234
235#[cfg(test)]
236pub(crate) mod test_utils {
237    use crate::editor::{ElementConverter, HasElementName, XMLEditorError};
238    use pretty_assertions::assert_eq;
239    #[track_caller]
240    pub fn create_xml_to_element<E>(xml: &str) -> Result<E, XMLEditorError>
241    where
242        E: ElementConverter + HasElementName,
243    {
244        let actual_xml = format!(
245            r#"<?xml version="1.0" encoding="UTF-8"?>
246            {xml}
247            "#
248        );
249        let document = edit_xml::Document::parse_str(&actual_xml).unwrap();
250        let Some(raw_element) = document.root_element() else {
251            println!("{}", actual_xml);
252            panic!("No root element found");
253        };
254        let name = raw_element.name(&document);
255        assert_eq!(
256            name,
257            E::element_name(),
258            "Expected element name to be {}",
259            E::element_name()
260        );
261        E::from_element(raw_element, &document)
262    }
263}