Skip to main content

r_description/
lossy.rs

1//! A library for parsing and manipulating R DESCRIPTION files.
2//!
3//! See https://r-pkgs.org/description.html and https://cran.r-project.org/doc/manuals/R-exts.html
4//! for more information
5//!
6//! See the ``lossless`` module for a lossless parser that is
7//! forgiving in the face of errors and preserves formatting while editing
8//! at the expense of a more complex API.
9use deb822_derive::{FromDeb822, ToDeb822};
10use deb822_fast::{FromDeb822Paragraph, ToDeb822Paragraph};
11
12use crate::RCode;
13use std::iter::Peekable;
14
15use crate::relations::SyntaxKind::*;
16use crate::relations::{lex, SyntaxKind, VersionConstraint};
17use crate::version::Version;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20/// A URL entry in the URL field.
21pub struct UrlEntry {
22    /// URL
23    pub url: url::Url,
24
25    /// Optional label for the URL.
26    pub label: Option<String>,
27}
28
29impl std::fmt::Display for UrlEntry {
30    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31        write!(f, "{}", self.url.as_str())?;
32        if let Some(label) = &self.label {
33            write!(f, " ({label})")?;
34        }
35        Ok(())
36    }
37}
38
39impl std::str::FromStr for UrlEntry {
40    type Err = String;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        if let Some(pos) = s.find('(') {
44            let url = s[..pos].trim();
45            let label = s[pos + 1..s.len() - 1].trim();
46            Ok(UrlEntry {
47                url: url::Url::parse(url).map_err(|e| e.to_string())?,
48                label: Some(label.to_string()),
49            })
50        } else {
51            Ok(UrlEntry {
52                url: url::Url::parse(s).map_err(|e| e.to_string())?,
53                label: None,
54            })
55        }
56    }
57}
58
59fn serialize_url_list(urls: &[UrlEntry]) -> String {
60    let mut s = String::new();
61    for (i, url) in urls.iter().enumerate() {
62        if i > 0 {
63            s.push_str(", ");
64        }
65        s.push_str(url.to_string().as_str());
66    }
67    s
68}
69
70fn deserialize_url_list(s: &str) -> Result<Vec<UrlEntry>, String> {
71    s.split([',', '\n'].as_ref())
72        .filter(|s| !s.trim().is_empty())
73        .map(|s| s.trim().parse())
74        .collect::<Result<Vec<_>, String>>()
75        .map_err(|e| e.to_string())
76}
77
78fn serialize_repository_list(repositories: &[url::Url]) -> String {
79    let mut s = String::new();
80    for (i, repository) in repositories.iter().enumerate() {
81        if i > 0 {
82            s.push_str(", ");
83        }
84        s.push_str(repository.as_str());
85    }
86    s
87}
88
89fn deserialize_repository_list(s: &str) -> Result<Vec<url::Url>, String> {
90    s.split([',', '\n'].as_ref())
91        .filter(|s| !s.trim().is_empty())
92        .map(|s| url::Url::parse(s.trim()).map_err(|e| e.to_string()))
93        .collect()
94}
95
96#[derive(FromDeb822, ToDeb822, Debug, PartialEq, Eq)]
97/// A DESCRIPTION file.
98pub struct RDescription {
99    /// The name of the package.
100    #[deb822(field = "Package")]
101    pub name: String,
102
103    /// A short description of the package.
104    #[deb822(field = "Description")]
105    pub description: String,
106
107    #[deb822(field = "Title")]
108    /// The title of the package.
109    pub title: String,
110
111    #[deb822(field = "Maintainer")]
112    /// The maintainer of the package.
113    pub maintainer: Option<String>,
114
115    #[deb822(field = "Author")]
116    /// Who wrote the the package
117    pub author: Option<String>,
118
119    /// 'Authors@R' is a special field that can contain R code
120    /// that is evaluated to get the authors and maintainers.
121    #[deb822(field = "Authors@R")]
122    pub authors: Option<RCode>,
123
124    #[deb822(field = "Version")]
125    /// The version of the package.
126    pub version: Version,
127
128    /// If the DESCRIPTION file is not written in pure ASCII, the encoding
129    /// field must be used to specify the encoding.
130    #[deb822(field = "Encoding")]
131    pub encoding: Option<String>,
132
133    #[deb822(field = "License")]
134    /// The license of the package.
135    pub license: String,
136
137    #[deb822(field = "URL", serialize_with = serialize_url_list, deserialize_with = deserialize_url_list)]
138    // TODO: parse this as a list of URLs, separated by commas
139    /// URLs related to the package.
140    pub url: Option<Vec<UrlEntry>>,
141
142    #[deb822(field = "BugReports")]
143    /// The URL or email address where bug reports should be sent.
144    pub bug_reports: Option<String>,
145
146    #[deb822(field = "Imports")]
147    /// The packages that this package depends on.
148    pub imports: Option<Relations>,
149
150    #[deb822(field = "Suggests")]
151    /// The packages that this package suggests.
152    pub suggests: Option<Relations>,
153
154    #[deb822(field = "Depends")]
155    /// The packages that this package depends on.
156    pub depends: Option<Relations>,
157
158    #[deb822(field = "LinkingTo")]
159    /// The packages that this package links to.
160    pub linking_to: Option<Relations>,
161
162    #[deb822(field = "LazyData")]
163    /// Whether the package has lazy data.
164    pub lazy_data: Option<String>,
165
166    #[deb822(field = "Collate")]
167    /// The order in which R scripts are loaded.
168    pub collate: Option<String>,
169
170    #[deb822(field = "VignetteBuilder")]
171    /// The package used to build vignettes.
172    pub vignette_builder: Option<String>,
173
174    #[deb822(field = "SystemRequirements")]
175    /// The system requirements for the package.
176    pub system_requirements: Option<String>,
177
178    #[deb822(field = "Date")]
179    /// The release date of the current version of the package.
180    /// Strongly recommended to use the ISO 8601 format: YYYY-MM-DD
181    pub date: Option<String>,
182
183    #[deb822(field = "Language")]
184    /// Indicates the package documentation is not in English.
185    /// This should be a comma-separated list of IETF language
186    /// tags as defined by RFC5646
187    pub language: Option<String>,
188
189    #[deb822(field = "Repository")]
190    /// The R Repository to use for this package. E.g. "CRAN" or "Bioconductor"
191    pub repository: Option<String>,
192
193    #[deb822(field = "Additional_repositories", serialize_with = serialize_repository_list, deserialize_with = deserialize_repository_list)]
194    /// Additional repositories where dependency packages may be found.
195    pub additional_repositories: Option<Vec<url::Url>>,
196}
197
198/// A relation entry in a relationship field.
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub struct Relation {
201    /// Package name.
202    pub name: String,
203    /// Version constraint and version.
204    pub version: Option<(VersionConstraint, Version)>,
205}
206
207impl Default for Relation {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl Relation {
214    /// Create an empty relation.
215    pub fn new() -> Self {
216        Self {
217            name: String::new(),
218            version: None,
219        }
220    }
221
222    /// Check if this entry is satisfied by the given package versions.
223    ///
224    /// # Arguments
225    /// * `package_version` - A function that returns the version of a package.
226    ///
227    /// # Example
228    /// ```
229    /// use r_description::lossy::Relation;
230    /// use r_description::Version;
231    /// let entry: Relation = "cli (>= 2.0)".parse().unwrap();
232    /// assert!(entry.satisfied_by(|name: &str| -> Option<Version> {
233    ///    match name {
234    ///    "cli" => Some("2.0".parse().unwrap()),
235    ///    _ => None
236    /// }}));
237    /// ```
238    pub fn satisfied_by(&self, package_version: impl crate::relations::VersionLookup) -> bool {
239        let actual = package_version.lookup_version(self.name.as_str());
240        if let Some((vc, version)) = &self.version {
241            if let Some(actual) = actual {
242                match vc {
243                    VersionConstraint::GreaterThanEqual => actual.as_ref() >= version,
244                    VersionConstraint::LessThanEqual => actual.as_ref() <= version,
245                    VersionConstraint::Equal => actual.as_ref() == version,
246                    VersionConstraint::NotEqual => actual.as_ref() != version,
247                    VersionConstraint::GreaterThan => actual.as_ref() > version,
248                    VersionConstraint::LessThan => actual.as_ref() < version,
249                }
250            } else {
251                false
252            }
253        } else {
254            actual.is_some()
255        }
256    }
257}
258
259impl std::fmt::Display for Relation {
260    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
261        write!(f, "{}", self.name)?;
262        if let Some((constraint, version)) = &self.version {
263            write!(f, " ({constraint} {version})")?;
264        }
265        Ok(())
266    }
267}
268
269#[cfg(feature = "serde")]
270impl<'de> serde::Deserialize<'de> for Relation {
271    fn deserialize<D>(deserializer: D) -> Result<Relation, D::Error>
272    where
273        D: serde::Deserializer<'de>,
274    {
275        let s = String::deserialize(deserializer)?;
276        s.parse().map_err(serde::de::Error::custom)
277    }
278}
279
280#[cfg(feature = "serde")]
281impl serde::Serialize for Relation {
282    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
283    where
284        S: serde::Serializer,
285    {
286        self.to_string().serialize(serializer)
287    }
288}
289
290/// A collection of relation entries in a relationship field.
291#[derive(Debug, Clone, PartialEq, Eq, Hash)]
292pub struct Relations(pub Vec<Relation>);
293
294impl std::ops::Index<usize> for Relations {
295    type Output = Relation;
296
297    fn index(&self, index: usize) -> &Self::Output {
298        &self.0[index]
299    }
300}
301
302impl std::ops::IndexMut<usize> for Relations {
303    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
304        &mut self.0[index]
305    }
306}
307
308impl FromIterator<Relation> for Relations {
309    fn from_iter<I: IntoIterator<Item = Relation>>(iter: I) -> Self {
310        Self(iter.into_iter().collect())
311    }
312}
313
314impl Default for Relations {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320impl Relations {
321    /// Create an empty relations.
322    pub fn new() -> Self {
323        Self(Vec::new())
324    }
325
326    /// Remove an entry from the relations.
327    pub fn remove(&mut self, index: usize) {
328        self.0.remove(index);
329    }
330
331    /// Iterate over the entries in the relations.
332    pub fn iter(&self) -> impl Iterator<Item = &Relation> {
333        self.0.iter()
334    }
335
336    /// Number of entries in the relations.
337    pub fn len(&self) -> usize {
338        self.0.len()
339    }
340
341    /// Check if the relations are empty.
342    pub fn is_empty(&self) -> bool {
343        self.0.is_empty()
344    }
345
346    /// Check if the relations are satisfied by the given package versions.
347    pub fn satisfied_by(
348        &self,
349        package_version: impl crate::relations::VersionLookup + Copy,
350    ) -> bool {
351        self.0.iter().all(|r| r.satisfied_by(package_version))
352    }
353}
354
355impl std::fmt::Display for Relations {
356    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
357        for (i, relation) in self.0.iter().enumerate() {
358            if i > 0 {
359                f.write_str(", ")?;
360            }
361            write!(f, "{relation}")?;
362        }
363        Ok(())
364    }
365}
366
367impl std::str::FromStr for Relation {
368    type Err = String;
369
370    fn from_str(s: &str) -> Result<Self, Self::Err> {
371        let tokens = lex(s);
372        let mut tokens = tokens.into_iter().peekable();
373
374        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
375            while let Some((k, _)) = tokens.peek() {
376                match k {
377                    WHITESPACE | NEWLINE => {
378                        tokens.next();
379                    }
380                    _ => break,
381                }
382            }
383        }
384
385        let name = match tokens.next() {
386            Some((IDENT, name)) => name,
387            _ => return Err("Expected package name".to_string()),
388        };
389
390        eat_whitespace(&mut tokens);
391
392        let version = if let Some((L_PARENS, _)) = tokens.peek() {
393            tokens.next();
394            eat_whitespace(&mut tokens);
395            let mut constraint = String::new();
396            while let Some((kind, t)) = tokens.peek() {
397                match kind {
398                    EQUAL | L_ANGLE | R_ANGLE | NOT => {
399                        constraint.push_str(t);
400                        tokens.next();
401                    }
402                    _ => break,
403                }
404            }
405            let constraint = constraint.parse()?;
406            eat_whitespace(&mut tokens);
407            // Read IDENT and COLON tokens until we see R_PARENS
408            let version_string = match tokens.next() {
409                Some((IDENT, s)) => s,
410                _ => return Err("Expected version string".to_string()),
411            };
412            let version: Version = version_string.parse().map_err(|e: String| e.to_string())?;
413            eat_whitespace(&mut tokens);
414            if let Some((R_PARENS, _)) = tokens.next() {
415            } else {
416                return Err(format!("Expected ')', found {:?}", tokens.next()));
417            }
418            Some((constraint, version))
419        } else {
420            None
421        };
422
423        eat_whitespace(&mut tokens);
424
425        if let Some((kind, _)) = tokens.next() {
426            return Err(format!("Unexpected token: {kind:?}"));
427        }
428
429        Ok(Relation { name, version })
430    }
431}
432
433impl std::str::FromStr for Relations {
434    type Err = String;
435
436    fn from_str(s: &str) -> Result<Self, Self::Err> {
437        let mut relations = Vec::new();
438        if s.is_empty() {
439            return Ok(Relations(relations));
440        }
441        for relation in s.split(',') {
442            let relation = relation.trim();
443            if relation.is_empty() {
444                // Ignore empty entries.
445                continue;
446            }
447            relations.push(relation.parse()?);
448        }
449        Ok(Relations(relations))
450    }
451}
452
453#[cfg(feature = "serde")]
454impl<'de> serde::Deserialize<'de> for Relations {
455    fn deserialize<D>(deserializer: D) -> Result<Relations, D::Error>
456    where
457        D: serde::Deserializer<'de>,
458    {
459        let s = String::deserialize(deserializer)?;
460        s.parse().map_err(serde::de::Error::custom)
461    }
462}
463
464#[cfg(feature = "serde")]
465impl serde::Serialize for Relations {
466    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
467    where
468        S: serde::Serializer,
469    {
470        self.to_string().serialize(serializer)
471    }
472}
473
474impl std::str::FromStr for RDescription {
475    type Err = String;
476
477    fn from_str(s: &str) -> Result<Self, Self::Err> {
478        let para = deb822_fast::Paragraph::from_str(s).map_err(|e| e.to_string())?;
479        Self::from_paragraph(&para)
480    }
481}
482
483impl std::fmt::Display for RDescription {
484    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
485        let para: deb822_fast::Paragraph = self.to_paragraph();
486        f.write_str(&para.to_string())?;
487        Ok(())
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_parse() {
497        let s = r###"Package: mypackage
498Title: What the Package Does (One Line, Title Case)
499Version: 0.0.0.9000
500Authors@R: 
501    person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
502           comment = c(ORCID = "YOUR-ORCID-ID"))
503Description: What the package does (one paragraph).
504License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
505    license
506Encoding: UTF-8
507Roxygen: list(markdown = TRUE)
508RoxygenNote: 7.3.2
509"###;
510        let desc: RDescription = s.parse().unwrap();
511
512        assert_eq!(desc.name, "mypackage".to_string());
513        assert_eq!(
514            desc.title,
515            "What the Package Does (One Line, Title Case)".to_string()
516        );
517        assert_eq!(desc.version, "0.0.0.9000".parse().unwrap());
518        assert_eq!(
519            desc.authors,
520            Some(RCode(
521                r#"
522person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
523comment = c(ORCID = "YOUR-ORCID-ID"))"#
524                    .to_string()
525            ))
526        );
527        assert_eq!(
528            desc.description,
529            "What the package does (one paragraph).".to_string()
530        );
531        assert_eq!(
532            desc.license,
533            "`use_mit_license()`, `use_gpl3_license()` or friends to pick a\nlicense".to_string()
534        );
535        assert_eq!(desc.encoding, Some("UTF-8".to_string()));
536
537        assert_eq!(
538            desc.to_string(),
539            r###"Package: mypackage
540Description: What the package does (one paragraph).
541Title: What the Package Does (One Line, Title Case)
542Authors@R: 
543 person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
544 comment = c(ORCID = "YOUR-ORCID-ID"))
545Version: 0.0.0.9000
546Encoding: UTF-8
547License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
548 license
549"###
550        );
551    }
552
553    #[test]
554    fn test_parse_dplyr() {
555        let s = include_str!("../testdata/dplyr.desc");
556        let desc: RDescription = s.parse().unwrap();
557
558        assert_eq!(desc.name, "dplyr".to_string());
559    }
560
561    #[test]
562    fn test_parse_relations() {
563        let input = "cli";
564        let parsed: Relations = input.parse().unwrap();
565        assert_eq!(parsed.to_string(), input);
566        assert_eq!(parsed.len(), 1);
567        let relation = &parsed[0];
568        assert_eq!(relation.to_string(), "cli");
569        assert_eq!(relation.version, None);
570
571        let input = "cli (>= 0.20.21)";
572        let parsed: Relations = input.parse().unwrap();
573        assert_eq!(parsed.to_string(), input);
574        assert_eq!(parsed.len(), 1);
575        let relation = &parsed[0];
576        assert_eq!(relation.to_string(), "cli (>= 0.20.21)");
577        assert_eq!(
578            relation.version,
579            Some((
580                VersionConstraint::GreaterThanEqual,
581                "0.20.21".parse().unwrap()
582            ))
583        );
584
585        let parsed: Relations = "xml2 (> 1.0.0)".parse().unwrap();
586        assert_eq!(parsed.len(), 1);
587        assert_eq!(parsed[0].name, "xml2");
588        assert_eq!(
589            parsed[0].version,
590            Some((VersionConstraint::GreaterThan, "1.0.0".parse().unwrap()))
591        );
592
593        let parsed: Relations = "xml2 (< 2.0.0)".parse().unwrap();
594        assert_eq!(parsed.len(), 1);
595        assert_eq!(parsed[0].name, "xml2");
596        assert_eq!(
597            parsed[0].version,
598            Some((VersionConstraint::LessThan, "2.0.0".parse().unwrap()))
599        );
600
601        let parsed: Relations = "xml2 (== 2.0.0)".parse().unwrap();
602        assert_eq!(parsed.len(), 1);
603        assert_eq!(
604            parsed[0].version,
605            Some((VersionConstraint::Equal, "2.0.0".parse().unwrap()))
606        );
607        assert_eq!(parsed.to_string(), "xml2 (== 2.0.0)");
608
609        let parsed: Relations = "xml2 (!= 2.0.0)".parse().unwrap();
610        assert_eq!(parsed.len(), 1);
611        assert_eq!(
612            parsed[0].version,
613            Some((VersionConstraint::NotEqual, "2.0.0".parse().unwrap()))
614        );
615        assert_eq!(parsed.to_string(), "xml2 (!= 2.0.0)");
616    }
617
618    #[test]
619    fn test_multiple() {
620        let input = "cli (>= 0.20.21), cli (< 0.21)";
621        let parsed: Relations = input.parse().unwrap();
622        assert_eq!(parsed.to_string(), input);
623        assert_eq!(parsed.len(), 2);
624        let relation = &parsed[0];
625        assert_eq!(relation.to_string(), "cli (>= 0.20.21)");
626        assert_eq!(
627            relation.version,
628            Some((
629                VersionConstraint::GreaterThanEqual,
630                "0.20.21".parse().unwrap()
631            ))
632        );
633        let relation = &parsed[1];
634        assert_eq!(relation.to_string(), "cli (< 0.21)");
635        assert_eq!(
636            relation.version,
637            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
638        );
639    }
640
641    #[cfg(feature = "serde")]
642    #[test]
643    fn test_serde_relations() {
644        let input = "cli (>= 0.20.21), cli (< 0.21)";
645        let parsed: Relations = input.parse().unwrap();
646        let serialized = serde_json::to_string(&parsed).unwrap();
647        assert_eq!(serialized, r#""cli (>= 0.20.21), cli (< 0.21)""#);
648        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
649        assert_eq!(deserialized, parsed);
650    }
651
652    #[cfg(feature = "serde")]
653    #[test]
654    fn test_serde_relation() {
655        let input = "cli (>= 0.20.21)";
656        let parsed: Relation = input.parse().unwrap();
657        let serialized = serde_json::to_string(&parsed).unwrap();
658        assert_eq!(serialized, r#""cli (>= 0.20.21)""#);
659        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
660        assert_eq!(deserialized, parsed);
661    }
662
663    #[test]
664    fn test_relations_is_empty() {
665        let input = "cli (>= 0.20.21)";
666        let parsed: Relations = input.parse().unwrap();
667        assert!(!parsed.is_empty());
668        let input = "";
669        let parsed: Relations = input.parse().unwrap();
670        assert!(parsed.is_empty());
671    }
672
673    #[test]
674    fn test_relations_len() {
675        let input = "cli (>= 0.20.21), cli (< 0.21)";
676        let parsed: Relations = input.parse().unwrap();
677        assert_eq!(parsed.len(), 2);
678    }
679
680    #[test]
681    fn test_relations_remove() {
682        let input = "cli (>= 0.20.21), cli (< 0.21)";
683        let mut parsed: Relations = input.parse().unwrap();
684        parsed.remove(1);
685        assert_eq!(parsed.len(), 1);
686        assert_eq!(parsed.to_string(), "cli (>= 0.20.21)");
687    }
688
689    #[test]
690    fn test_relations_satisfied_by() {
691        let input = "cli (>= 0.20.21), cli (< 0.21)";
692        let parsed: Relations = input.parse().unwrap();
693        assert!(parsed.satisfied_by(|name: &str| -> Option<Version> {
694            match name {
695                "cli" => Some("0.20.21".parse().unwrap()),
696                _ => None,
697            }
698        }));
699        assert!(!parsed.satisfied_by(|name: &str| -> Option<Version> {
700            match name {
701                "cli" => Some("0.21".parse().unwrap()),
702                _ => None,
703            }
704        }));
705    }
706
707    #[test]
708    fn test_relation_satisfied_by() {
709        let input = "cli (>= 0.20.21)";
710        let parsed: Relation = input.parse().unwrap();
711        assert!(parsed.satisfied_by(|name: &str| -> Option<Version> {
712            match name {
713                "cli" => Some("0.20.21".parse().unwrap()),
714                _ => None,
715            }
716        }));
717        assert!(!parsed.satisfied_by(|name: &str| -> Option<Version> {
718            match name {
719                "cli" => Some("0.20.20".parse().unwrap()),
720                _ => None,
721            }
722        }));
723    }
724
725    #[test]
726    fn test_parse_url_entry() {
727        let input = "https://example.com/";
728        let parsed: UrlEntry = input.parse().unwrap();
729        assert_eq!(parsed.url.as_str(), input);
730        assert_eq!(parsed.label, None);
731
732        let input = "https://example.com (Example)";
733        let parsed: UrlEntry = input.parse().unwrap();
734        assert_eq!(parsed.url.as_str(), "https://example.com/");
735        assert_eq!(parsed.label, Some("Example".to_string()));
736    }
737
738    #[test]
739    fn test_deserialize_url_list() {
740        let input = "https://example.com/, https://example.org (Example)";
741        let parsed = deserialize_url_list(input).unwrap();
742        assert_eq!(parsed.len(), 2);
743        assert_eq!(parsed[0].url.as_str(), "https://example.com/");
744        assert_eq!(parsed[0].label, None);
745        assert_eq!(parsed[1].url.as_str(), "https://example.org/");
746        assert_eq!(parsed[1].label, Some("Example".to_string()));
747    }
748
749    #[test]
750    fn test_deserialize_url_list2() {
751        let input = "https://example.com/\n https://example.org (Example)\n https://example.net";
752        let parsed = deserialize_url_list(input).unwrap();
753        assert_eq!(parsed.len(), 3);
754        assert_eq!(parsed[0].url.as_str(), "https://example.com/");
755        assert_eq!(parsed[0].label, None);
756        assert_eq!(parsed[1].url.as_str(), "https://example.org/");
757        assert_eq!(parsed[1].label, Some("Example".to_string()));
758        assert_eq!(parsed[2].url.as_str(), "https://example.net/");
759        assert_eq!(parsed[2].label, None);
760    }
761
762    #[test]
763    fn test_deserialize_repository_list() {
764        let input = "https://example.com/src/contrib,\n https://example.org/src/contrib";
765        let parsed = deserialize_repository_list(input).unwrap();
766        assert_eq!(parsed.len(), 2);
767        assert_eq!(parsed[0].as_str(), "https://example.com/src/contrib");
768        assert_eq!(parsed[1].as_str(), "https://example.org/src/contrib");
769    }
770
771    #[test]
772    fn test_parse_additional_repositories() {
773        let s = r###"Package: mypackage
774Title: What the Package Does
775Version: 1.0.0
776Description: What the package does.
777License: MIT
778Additional_repositories: https://example.com/src/contrib,
779 https://example.org/src/contrib
780"###;
781        let desc: RDescription = s.parse().unwrap();
782
783        let repositories = desc.additional_repositories.as_ref().unwrap();
784        assert_eq!(repositories.len(), 2);
785        assert_eq!(repositories[0].as_str(), "https://example.com/src/contrib");
786        assert_eq!(repositories[1].as_str(), "https://example.org/src/contrib");
787
788        assert_eq!(
789            desc.to_string(),
790            "Package: mypackage\nDescription: What the package does.\nTitle: What the Package Does\nVersion: 1.0.0\nLicense: MIT\nAdditional_repositories: https://example.com/src/contrib, https://example.org/src/contrib\n"
791        );
792    }
793}