maven_rs/pom/
repositories.rs

1use std::{fmt::Display, str::FromStr};
2
3use derive_builder::Builder;
4use edit_xml::Element;
5use serde::{Deserialize, Serialize};
6use strum::{Display, EnumString};
7
8use crate::{
9    editor::{
10        ChildOfListElement, ComparableElement, ElementConverter, HasElementName, InvalidValueError,
11        PomValue, UpdatableElement,
12        utils::{
13            add_if_present, find_or_create_then_set_text_content, sync_element,
14            typed_from_element_using_builder,
15        },
16    },
17    utils::serde_utils::serde_via_string_types,
18};
19
20#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
21pub struct Repositories {
22    #[serde(rename = "repository")]
23    pub repositories: Vec<Repository>,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Builder)]
27#[serde(rename_all = "camelCase")]
28pub struct Repository {
29    #[builder(setter(into, strip_option), default)]
30    pub id: Option<String>,
31    #[builder(setter(into, strip_option), default)]
32    pub name: Option<String>,
33    pub url: String,
34    #[builder(setter(into, strip_option), default)]
35    pub layout: Option<String>,
36    #[builder(setter(into, strip_option), default)]
37    pub update_policy: Option<UpdatePolicy>,
38    #[builder(setter(into, strip_option), default)]
39    pub checksum_policy: Option<ChecksumPolicy>,
40    #[builder(setter(into, strip_option), default)]
41    pub releases: Option<SubRepositoryRules>,
42    #[builder(setter(into, strip_option), default)]
43    pub snapshots: Option<SubRepositoryRules>,
44}
45impl HasElementName for Repository {
46    fn element_name() -> &'static str {
47        "repository"
48    }
49}
50impl ChildOfListElement for Repository {
51    fn parent_element_name() -> &'static str {
52        "repositories"
53    }
54}
55impl ComparableElement for Repository {
56    fn is_same_item(&self, other: &Self) -> bool {
57        if self.name.is_none() {
58            return false;
59        }
60        self.name == other.name
61    }
62}
63impl UpdatableElement for Repository {
64    fn update_element(
65        &self,
66        element: Element,
67        document: &mut edit_xml::Document,
68    ) -> Result<(), crate::editor::XMLEditorError> {
69        sync_element(document, element, "id", self.id.as_deref());
70        sync_element(document, element, "name", self.name.as_deref());
71        find_or_create_then_set_text_content(document, element, "url", self.url.as_str());
72        sync_element(document, element, "layout", self.layout.as_deref());
73        sync_element(document, element, "checksumPolicy", self.checksum_policy);
74        sync_element(document, element, "updatePolicy", self.update_policy);
75        // TODO: Layout
76        Ok(())
77    }
78}
79impl ElementConverter for Repository {
80    fn from_element(
81        element: edit_xml::Element,
82        document: &edit_xml::Document,
83    ) -> Result<Self, crate::editor::XMLEditorError> {
84        let mut builder = RepositoryBuilder::default();
85        for child in element.child_elements(document) {
86            match child.name(document) {
87                "id" => {
88                    builder.id(String::from_element(child, document)?);
89                }
90                "name" => {
91                    builder.name(String::from_element(child, document)?);
92                }
93                "url" => {
94                    builder.url(String::from_element(child, document)?);
95                }
96                "layout" => {
97                    builder.layout(String::from_element(child, document)?);
98                }
99                "updatePolicy" => {
100                    builder.update_policy(UpdatePolicy::from_element(child, document)?);
101                }
102                "checksumPolicy" => {
103                    builder.checksum_policy(ChecksumPolicy::from_element(child, document)?);
104                }
105                "releases" => {
106                    builder.releases(SubRepositoryRules::from_element(child, document)?);
107                }
108                "snapshots" => {
109                    builder.snapshots(SubRepositoryRules::from_element(child, document)?);
110                }
111                _ => {}
112            }
113        }
114        let result = builder.build()?;
115        Ok(result)
116    }
117    // TODO: Releases, Snapshots
118
119    fn into_children(
120        self,
121        document: &mut edit_xml::Document,
122    ) -> Result<Vec<edit_xml::Element>, crate::editor::XMLEditorError> {
123        let Self {
124            id,
125            name,
126            url,
127            layout,
128            update_policy,
129            checksum_policy,
130            releases,
131            snapshots,
132        } = self;
133        let mut children = vec![];
134        add_if_present!(document, children, id, "id");
135        add_if_present!(document, children, name, "name");
136        children.push(crate::editor::utils::create_basic_text_element(
137            document, "url", url,
138        ));
139        add_if_present!(document, children, layout, "layout");
140        add_if_present!(document, children, update_policy, "updatePolicy");
141        add_if_present!(document, children, checksum_policy, "checksumPolicy");
142        if let Some(releases) = releases {
143            let element = Element::new(document, "releases");
144            let release_children = releases.into_children(document)?;
145            for child in release_children {
146                element.push_child(document, child)?;
147            }
148            children.push(element);
149        }
150        if let Some(snapshots) = snapshots {
151            let element = Element::new(document, "snapshots");
152            let snapshot_children = snapshots.into_children(document)?;
153            for child in snapshot_children {
154                element.push_child(document, child)?;
155            }
156            children.push(element);
157        }
158        Ok(children)
159    }
160}
161
162#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Builder)]
163#[serde(rename_all = "camelCase")]
164pub struct SubRepositoryRules {
165    #[builder(setter(into, strip_option), default)]
166    pub enabled: Option<bool>,
167    #[builder(setter(into, strip_option), default)]
168    pub update_policy: Option<UpdatePolicy>,
169    #[builder(setter(into, strip_option), default)]
170    pub checksum_policy: Option<ChecksumPolicy>,
171}
172impl ElementConverter for SubRepositoryRules {
173    typed_from_element_using_builder!(
174        SubRepositoryRulesBuilder,
175        element,
176        document,
177        "enabled"(bool) => enabled,
178        "updatePolicy"(UpdatePolicy) => update_policy,
179        "checksumPolicy"(ChecksumPolicy) => checksum_policy
180    );
181    fn into_children(
182        self,
183        document: &mut edit_xml::Document,
184    ) -> Result<Vec<edit_xml::Element>, crate::editor::XMLEditorError> {
185        let Self {
186            enabled,
187            update_policy,
188            checksum_policy,
189        } = self;
190        let mut children = vec![];
191        add_if_present!(document, children, enabled, "enabled");
192        add_if_present!(document, children, update_policy, "updatePolicy");
193        add_if_present!(document, children, checksum_policy, "checksumPolicy");
194
195        Ok(children)
196    }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString)]
200#[strum(serialize_all = "camelCase")]
201pub enum ChecksumPolicy {
202    Ignore,
203    Fail,
204    Warn,
205}
206impl From<ChecksumPolicy> for String {
207    fn from(policy: ChecksumPolicy) -> Self {
208        policy.to_string()
209    }
210}
211
212serde_via_string_types!(ChecksumPolicy);
213impl PomValue for ChecksumPolicy {
214    fn from_str_for_editor(value: &str) -> Result<Self, InvalidValueError> {
215        match value {
216            "ignore" => Ok(ChecksumPolicy::Ignore),
217            "fail" => Ok(ChecksumPolicy::Fail),
218            "warn" => Ok(ChecksumPolicy::Warn),
219            _ => Err(InvalidValueError::InvalidValue {
220                expected: "ignore, fail, or warn",
221                found: value.to_owned(),
222            }),
223        }
224    }
225    fn to_string_for_editor(&self) -> String {
226        self.to_string()
227    }
228}
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString)]
230#[strum(serialize_all = "camelCase")]
231pub enum RepositoryLayout {
232    Default,
233    Legacy,
234}
235serde_via_string_types!(RepositoryLayout);
236impl PomValue for RepositoryLayout {
237    fn from_str_for_editor(value: &str) -> Result<Self, InvalidValueError> {
238        match value {
239            "default" => Ok(RepositoryLayout::Default),
240            "legacy" => Ok(RepositoryLayout::Legacy),
241            _ => Err(InvalidValueError::InvalidValue {
242                expected: "default or legacy",
243                found: value.to_owned(),
244            }),
245        }
246    }
247    fn to_string_for_editor(&self) -> String {
248        self.to_string()
249    }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum UpdatePolicy {
254    Always,
255    Daily,
256    Interval(usize),
257    Never,
258}
259impl From<UpdatePolicy> for String {
260    fn from(policy: UpdatePolicy) -> Self {
261        policy.to_string()
262    }
263}
264
265impl FromStr for UpdatePolicy {
266    type Err = InvalidValueError;
267
268    fn from_str(s: &str) -> Result<Self, Self::Err> {
269        match s {
270            "always" => Ok(UpdatePolicy::Always),
271            "daily" => Ok(UpdatePolicy::Daily),
272            "never" => Ok(UpdatePolicy::Never),
273            other => {
274                if other.starts_with("interval:") {
275                    let interval = other.strip_prefix("interval:").ok_or_else(|| {
276                        InvalidValueError::InvalidValue {
277                            expected: "interval:<number>",
278                            found: other.to_owned(),
279                        }
280                    })?;
281                    let interval: usize =
282                        interval
283                            .parse()
284                            .map_err(|_| InvalidValueError::InvalidFormattedValue {
285                                error: interval.to_string(),
286                            })?;
287                    Ok(UpdatePolicy::Interval(interval))
288                } else {
289                    Err(InvalidValueError::InvalidValue {
290                        expected: "always, daily, never, or interval:<number>",
291                        found: other.to_owned(),
292                    })
293                }
294            }
295        }
296    }
297}
298impl Display for UpdatePolicy {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        match self {
301            UpdatePolicy::Always => write!(f, "always"),
302            UpdatePolicy::Daily => write!(f, "daily"),
303            UpdatePolicy::Interval(interval) => write!(f, "interval:{}", interval),
304            UpdatePolicy::Never => write!(f, "never"),
305        }
306    }
307}
308
309impl PomValue for UpdatePolicy {
310    fn from_str_for_editor(value: &str) -> Result<Self, InvalidValueError> {
311        value.parse()
312    }
313
314    fn to_string_for_editor(&self) -> String {
315        self.to_string()
316    }
317}
318serde_via_string_types!(UpdatePolicy);
319
320#[cfg(test)]
321mod tests {
322    use std::str::FromStr;
323
324    use crate::editor::utils::test_utils;
325
326    use super::*;
327    fn inner_layout_test(layout: RepositoryLayout, expected: &str) {
328        assert_eq!(layout.to_string(), expected);
329        assert_eq!(RepositoryLayout::from_str(expected).unwrap(), layout);
330    }
331    #[test]
332    fn layout() {
333        inner_layout_test(RepositoryLayout::Default, "default");
334        inner_layout_test(RepositoryLayout::Legacy, "legacy");
335    }
336
337    fn inner_update_policy_test(policy: UpdatePolicy, expected: &str) {
338        assert_eq!(policy.to_string(), expected);
339        assert_eq!(UpdatePolicy::from_str(expected).unwrap(), policy);
340    }
341    #[test]
342    fn update_policy() {
343        inner_update_policy_test(UpdatePolicy::Always, "always");
344        inner_update_policy_test(UpdatePolicy::Daily, "daily");
345        inner_update_policy_test(UpdatePolicy::Interval(5), "interval:5");
346        inner_update_policy_test(UpdatePolicy::Never, "never");
347    }
348    fn inner_checksum_policy(policy: ChecksumPolicy, expected: &str) {
349        assert_eq!(policy.to_string(), expected);
350        assert_eq!(ChecksumPolicy::from_str(expected).unwrap(), policy);
351    }
352    #[test]
353    fn checksum_policy() {
354        inner_checksum_policy(ChecksumPolicy::Ignore, "ignore");
355        inner_checksum_policy(ChecksumPolicy::Fail, "fail");
356        inner_checksum_policy(ChecksumPolicy::Warn, "warn");
357    }
358
359    fn test_parse_methods(value: &str, expected: Repository) -> anyhow::Result<()> {
360        let dep_via_edit_xml = test_utils::create_xml_to_element::<Repository>(value)?;
361        let dep_via_serde: Repository = quick_xml::de::from_str(value)?;
362
363        assert_eq!(dep_via_edit_xml, expected);
364        assert_eq!(dep_via_serde, expected);
365        println!("{:#?}", dep_via_edit_xml);
366
367        let dep_serialize_serde = quick_xml::se::to_string(&expected)?;
368        println!("Serialized Over Serde \n {}", dep_serialize_serde);
369        Ok(())
370    }
371
372    #[test]
373    fn basic_repository() -> anyhow::Result<()> {
374        test_parse_methods(
375            r#"
376            <repository>
377                <id>central</id>
378                <name>Maven Central</name>
379                <url>https://repo.maven.apache.org/maven2/</url>
380            </repository>
381        "#,
382            Repository {
383                id: Some("central".to_string()),
384                name: Some("Maven Central".to_string()),
385                url: "https://repo.maven.apache.org/maven2/".to_string(),
386                ..Default::default()
387            },
388        )
389    }
390    #[test]
391    fn just_url() -> anyhow::Result<()> {
392        test_parse_methods(
393            r#"
394            <repository>
395                <url>https://repo.maven.apache.org/maven2/</url>
396            </repository>
397        "#,
398            Repository {
399                url: "https://repo.maven.apache.org/maven2/".to_string(),
400                ..Default::default()
401            },
402        )
403    }
404    #[test]
405    fn with_release_settings() -> anyhow::Result<()> {
406        test_parse_methods(
407            r#"
408                <repository>
409                    <url>https://repo.maven.apache.org/maven2/</url>
410                    <releases>
411                        <enabled>true</enabled>
412                        <updatePolicy>daily</updatePolicy>
413                        <checksumPolicy>fail</checksumPolicy>
414                    </releases>
415                </repository>
416            "#,
417            Repository {
418                url: "https://repo.maven.apache.org/maven2/".to_string(),
419                releases: Some(SubRepositoryRules {
420                    enabled: Some(true),
421                    update_policy: Some(UpdatePolicy::Daily),
422                    checksum_policy: Some(ChecksumPolicy::Fail),
423                }),
424                ..Default::default()
425            },
426        )
427    }
428    #[test]
429    fn with_snapshot_settings() -> anyhow::Result<()> {
430        test_parse_methods(
431            r#"
432                <repository>
433                    <url>https://repo.maven.apache.org/maven2/</url>
434                    <snapshots>
435                        <enabled>true</enabled>
436                        <updatePolicy>daily</updatePolicy>
437                        <checksumPolicy>fail</checksumPolicy>
438                    </snapshots>
439                </repository>
440            "#,
441            Repository {
442                url: "https://repo.maven.apache.org/maven2/".to_string(),
443                snapshots: Some(SubRepositoryRules {
444                    enabled: Some(true),
445                    update_policy: Some(UpdatePolicy::Daily),
446                    checksum_policy: Some(ChecksumPolicy::Fail),
447                }),
448                ..Default::default()
449            },
450        )
451    }
452
453    #[test]
454    fn with_empty_sub_rules() -> anyhow::Result<()> {
455        test_parse_methods(
456            r#"
457                <repository>
458                    <url>https://repo.maven.apache.org/maven2/</url>
459                    <releases> </releases>
460                    <snapshots/>
461                </repository>
462            "#,
463            Repository {
464                url: "https://repo.maven.apache.org/maven2/".to_string(),
465                releases: Some(SubRepositoryRules::default()),
466                snapshots: Some(SubRepositoryRules::default()),
467                ..Default::default()
468            },
469        )
470    }
471
472    #[test]
473    fn update_element_test() -> anyhow::Result<()> {
474        let actual_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
475            <repository>
476                <id>central</id>
477                <url>https://repo.maven.apache.org/maven2/</url>
478            </repository>
479            "#;
480        let mut document = edit_xml::Document::parse_str(actual_xml).unwrap();
481        let Some(raw_element) = document.root_element() else {
482            println!("{}", actual_xml);
483            panic!("No root element found");
484        };
485
486        let repository = Repository {
487            id: Some("central".to_string()),
488            name: Some("Maven Central".to_string()),
489            url: "https://repo.maven.apache.org/maven2/".to_string(),
490            layout: Some("default".to_string()),
491            update_policy: Some(UpdatePolicy::Daily),
492            checksum_policy: Some(ChecksumPolicy::Fail),
493
494            ..Default::default()
495        };
496
497        repository.update_element(raw_element, &mut document)?;
498
499        let new_xml = document.write_str()?;
500        let expected_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
501<repository>
502  <id>central</id>
503  <url>https://repo.maven.apache.org/maven2/</url>
504  <name>Maven Central</name>
505  <layout>default</layout>
506  <checksumPolicy>fail</checksumPolicy>
507  <updatePolicy>daily</updatePolicy>
508</repository>"#;
509        assert_eq!(new_xml, expected_xml);
510        Ok(())
511    }
512}