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