r-description-0.3.1/.cargo_vcs_info.json0000644000000001360000000000100136170ustar { "git": { "sha1": "363bca53f3735792ef5c299effe9933f24adb64e" }, "path_in_vcs": "" }r-description-0.3.1/.github/CODEOWNERS000064400000000000000000000000121046102023000153330ustar 00000000000000* @jelmer r-description-0.3.1/.github/FUNDING.yml000064400000000000000000000000171046102023000155620ustar 00000000000000github: jelmer r-description-0.3.1/.github/dependabot.yml000064400000000000000000000006251046102023000166020ustar 00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly r-description-0.3.1/.github/workflows/rust.yml000064400000000000000000000010051046102023000175200ustar 00000000000000name: Rust on: push: branches: [ "master" ] pull_request: branches: [ "master" ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install all-features run: cargo install cargo-all-features - name: Build run: cargo build-all-features --verbose --all env: RUSTFLAGS: -Dwarnings - name: Run tests run: cargo test-all-features --verbose --all env: RUSTFLAGS: -Dwarnings r-description-0.3.1/.gitignore000064400000000000000000000000131046102023000143710ustar 00000000000000/target *~ r-description-0.3.1/Cargo.toml0000644000000024440000000000100116210ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "r-description" version = "0.3.1" build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Parsing and editor for R DESCRIPTION files" homepage = "https://github.com/jelmer/r-description-rs" readme = "README.md" keywords = [ "r-description", "rfc822", "lossless", "edit", "r", ] categories = ["parser-implementations"] license = "Apache-2.0" repository = "https://github.com/jelmer/r-description-rs" [lib] name = "r_description" path = "src/lib.rs" [dependencies.deb822-lossless] version = ">=0.2" features = ["derive"] [dependencies.rowan] version = ">=0.15.16" [dependencies.serde] version = "1" optional = true [dependencies.url] version = "2" [dev-dependencies.serde_json] version = "1" [features] serde = ["dep:serde"] r-description-0.3.1/Cargo.toml.orig000064400000000000000000000011171046102023000152760ustar 00000000000000[package] name = "r-description" description = "Parsing and editor for R DESCRIPTION files" edition = "2021" version = "0.3.1" repository = "https://github.com/jelmer/r-description-rs" homepage = "https://github.com/jelmer/r-description-rs" license = "Apache-2.0" keywords = ["r-description", "rfc822", "lossless", "edit", "r"] categories = ["parser-implementations"] [dependencies] deb822-lossless = { version = ">=0.2", features = ["derive"] } rowan = ">=0.15.16" url = "2" serde = { version = "1", optional = true } [features] serde = ["dep:serde"] [dev-dependencies] serde_json = "1" r-description-0.3.1/README.md000064400000000000000000000024241046102023000136700ustar 00000000000000# R DESCRIPTION parser This crate provides a parser and editor for the `DESCRIPTION` files used in R packages. See and for more information on the format. Besides parsing the control files it also supports parsing and comparison of version strings according to the R package versioning scheme as well as relations between versions. ## Example ```rust use std::str::FromStr; use r_description::lossy::RDescription; let mut desc = RDescription::from_str(r###"Package: foo Version: 1.0 Depends: R (>= 3.0.0) Description: A foo package Title: A foo package License: GPL-3 "###).unwrap(); assert_eq!(desc.name, "foo"); assert_eq!(desc.version, "1.0".parse().unwrap()); assert_eq!(desc.depends, Some("R (>= 3.0.0)".parse().unwrap())); desc.license = "MIT".to_string(); ``` ```rust use r_description::Version; let v1: Version = "1.2.3-alpha".parse().unwrap(); let v2: Version = "1.2.3".parse().unwrap(); assert!(v1 < v2); ``` ```rust use std::str::FromStr; use r_description::lossy::Relations; let v1 = r_description::Version::from_str("1.2.3").unwrap(); let rels: Relations = "cli (>= 2.0), crayon (= 1.3.4), testthat".parse().unwrap(); assert_eq!(3, rels.len()); assert_eq!(rels[0].name, "cli"); ``` r-description-0.3.1/disperse.conf000064400000000000000000000000461046102023000150740ustar 00000000000000timeout_days: 5 tag_name: "v$VERSION" r-description-0.3.1/src/lib.rs000064400000000000000000000014031046102023000143100ustar 00000000000000#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] pub mod lossless; pub mod lossy; mod relations; pub use relations::{VersionConstraint, VersionLookup}; pub use lossy::RDescription; mod version; pub use version::Version; #[derive(Debug, PartialEq, Eq)] /// A block of R code /// /// This is a simple wrapper around a string that represents a block of R code, as used in e.g. the /// Authors@R field. pub struct RCode(String); impl std::str::FromStr for RCode { type Err = std::num::ParseIntError; fn from_str(s: &str) -> Result { Ok(Self(s.to_string())) } } impl std::fmt::Display for RCode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.0) } } r-description-0.3.1/src/lossless.rs000064400000000000000000001560101046102023000154160ustar 00000000000000//! A library for parsing and manipulating R DESCRIPTION files. //! //! This module allows losslessly parsing R DESCRIPTION files into a structured representation. //! This allows modification of individual fields while preserving the //! original formatting of the file. //! //! This parser also allows for syntax errors in the input, and will attempt to parse as much as //! possible. //! //! See https://r-pkgs.org/description.html for more information. use crate::RCode; use deb822_lossless::Paragraph; pub use relations::{Relation, Relations}; pub struct RDescription(Paragraph); impl std::fmt::Display for RDescription { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Default for RDescription { fn default() -> Self { Self(Paragraph::new()) } } #[derive(Debug)] pub enum Error { Io(std::io::Error), Parse(deb822_lossless::ParseError), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::Io(e) => write!(f, "IO error: {}", e), Self::Parse(e) => write!(f, "Parse error: {}", e), } } } impl std::error::Error for Error {} impl From for Error { fn from(e: deb822_lossless::ParseError) -> Self { Self::Parse(e) } } impl From for Error { fn from(e: std::io::Error) -> Self { Self::Io(e) } } impl std::str::FromStr for RDescription { type Err = Error; fn from_str(s: &str) -> Result { Ok(Self(Paragraph::from_str(s)?)) } } impl RDescription { pub fn new() -> Self { Self(Paragraph::new()) } pub fn package(&self) -> Option { self.0.get("Package") } pub fn set_package(&mut self, package: &str) { self.0.insert("Package", package); } /// One line description of the package, and is often shown in a package listing /// /// It should be plain text (no markup), capitalised like a title, and NOT end in a period. /// Keep it short: listings will often truncate the title to 65 characters. pub fn title(&self) -> Option { self.0.get("Title") } pub fn maintainer(&self) -> Option { self.0.get("Maintainer") } pub fn set_maintainer(&mut self, maintainer: &str) { self.0.insert("Maintainer", maintainer); } pub fn authors(&self) -> Option { self.0.get("Authors@R").map(|s| s.parse().unwrap()) } pub fn set_authors(&mut self, authors: &RCode) { self.0.insert("Authors@R", &authors.to_string()); } pub fn set_title(&mut self, title: &str) { self.0.insert("Title", title); } pub fn description(&self) -> Option { self.0.get("Description") } pub fn set_description(&mut self, description: &str) { self.0.insert("Description", description); } pub fn version(&self) -> Option { self.0.get("Version") } pub fn set_version(&mut self, version: &str) { self.0.insert("Version", version); } pub fn encoding(&self) -> Option { self.0.get("Encoding") } pub fn set_encoding(&mut self, encoding: &str) { self.0.insert("Encoding", encoding); } pub fn license(&self) -> Option { self.0.get("License") } pub fn set_license(&mut self, license: &str) { self.0.insert("License", license); } pub fn roxygen_note(&self) -> Option { self.0.get("RoxygenNote") } pub fn set_roxygen_note(&mut self, roxygen_note: &str) { self.0.insert("RoxygenNote", roxygen_note); } pub fn roxygen(&self) -> Option { self.0.get("Roxygen") } pub fn set_roxygen(&mut self, roxygen: &str) { self.0.insert("Roxygen", roxygen); } /// The URL of the package's homepage. pub fn url(&self) -> Option { // TODO: parse list of URLs, separated by commas self.0.get("URL") } pub fn set_url(&mut self, url: &str) { // TODO: parse list of URLs, separated by commas self.0.insert("URL", url); } pub fn bug_reports(&self) -> Option { self.0 .get("BugReports") .map(|s| url::Url::parse(s.as_str()).unwrap()) } pub fn set_bug_reports(&mut self, bug_reports: &url::Url) { self.0.insert("BugReports", bug_reports.as_str()); } pub fn imports(&self) -> Option> { self.0 .get("Imports") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } pub fn set_imports(&mut self, imports: &[&str]) { self.0.insert("Imports", &imports.join(", ")); } pub fn suggests(&self) -> Option { self.0.get("Suggests").map(|s| s.parse().unwrap()) } pub fn set_suggests(&mut self, suggests: Relations) { self.0.insert("Suggests", &suggests.to_string()); } pub fn depends(&self) -> Option { self.0.get("Depends").map(|s| s.parse().unwrap()) } pub fn set_depends(&mut self, depends: Relations) { self.0.insert("Depends", &depends.to_string()); } pub fn linking_to(&self) -> Option> { self.0 .get("LinkingTo") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } pub fn set_linking_to(&mut self, linking_to: &[&str]) { self.0.insert("LinkingTo", &linking_to.join(", ")); } pub fn lazy_data(&self) -> Option { self.0.get("LazyData").map(|s| s == "true") } pub fn set_lazy_data(&mut self, lazy_data: bool) { self.0 .insert("LazyData", if lazy_data { "true" } else { "false" }); } pub fn collate(&self) -> Option { self.0.get("Collate") } pub fn set_collate(&mut self, collate: &str) { self.0.insert("Collate", collate); } pub fn vignette_builder(&self) -> Option> { self.0 .get("VignetteBuilder") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } pub fn set_vignette_builder(&mut self, vignette_builder: &[&str]) { self.0 .insert("VignetteBuilder", &vignette_builder.join(", ")); } pub fn system_requirements(&self) -> Option> { self.0 .get("SystemRequirements") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } pub fn set_system_requirements(&mut self, system_requirements: &[&str]) { self.0 .insert("SystemRequirements", &system_requirements.join(", ")); } pub fn date(&self) -> Option { self.0.get("Date") } pub fn set_date(&mut self, date: &str) { self.0.insert("Date", date); } /// The R Repository to use for this package. /// /// E.g. "CRAN" or "Bioconductor" pub fn repository(&self) -> Option { self.0.get("Repository") } /// Set the R Repository to use for this package. pub fn set_repository(&mut self, repository: &str) { self.0.insert("Repository", repository); } } pub mod relations { //! Parser for relationship fields like `Depends`, `Recommends`, etc. //! //! # Example //! ``` //! use r_description::lossless::{Relations, Relation}; //! use r_description::VersionConstraint; //! //! let mut relations: Relations = r"cli (>= 0.19.0), R".parse().unwrap(); //! assert_eq!(relations.to_string(), "cli (>= 0.19.0), R"); //! assert!(relations.satisfied_by(|name: &str| -> Option { //! match name { //! "cli" => Some("0.19.0".parse().unwrap()), //! "R" => Some("2.25.1".parse().unwrap()), //! _ => None //! }})); //! relations.remove_relation(1); //! assert_eq!(relations.to_string(), "cli (>= 0.19.0)"); //! ``` use crate::relations::SyntaxKind::{self, *}; use crate::relations::VersionConstraint; use crate::version::Version; use rowan::{Direction, NodeOrToken}; /// Error type for parsing relations fields #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ParseError(Vec); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for err in &self.0 { writeln!(f, "{}", err)?; } Ok(()) } } impl std::error::Error for ParseError {} /// Second, implementing the `Language` trait teaches rowan to convert between /// these two SyntaxKind types, allowing for a nicer SyntaxNode API where /// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Lang {} impl rowan::Language for Lang { type Kind = SyntaxKind; fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { unsafe { std::mem::transmute::(raw.0) } } fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { kind.into() } } /// GreenNode is an immutable tree, which is cheap to change, /// but doesn't contain offsets and parent pointers. use rowan::{GreenNode, GreenToken}; /// You can construct GreenNodes by hand, but a builder /// is helpful for top-down parsers: it maintains a stack /// of currently in-progress nodes use rowan::GreenNodeBuilder; /// The parse results are stored as a "green tree". /// We'll discuss working with the results later struct Parse { green_node: GreenNode, #[allow(unused)] errors: Vec, } fn parse(text: &str) -> Parse { struct Parser { /// input tokens, including whitespace, /// in *reverse* order. tokens: Vec<(SyntaxKind, String)>, /// the in-progress tree. builder: GreenNodeBuilder<'static>, /// the list of syntax errors we've accumulated /// so far. errors: Vec, } impl Parser { fn error(&mut self, error: String) { self.errors.push(error); self.builder.start_node(SyntaxKind::ERROR.into()); if self.current().is_some() { self.bump(); } self.builder.finish_node(); } fn parse_relation(&mut self) { self.builder.start_node(SyntaxKind::RELATION.into()); if self.current() == Some(IDENT) { self.bump(); } else { self.error("Expected package name".to_string()); } match self.peek_past_ws() { Some(COMMA) => {} None | Some(L_PARENS) => { self.skip_ws(); } e => { self.skip_ws(); self.error(format!( "Expected ':' or '|' or '[' or '<' or ',' but got {:?}", e )); } } if self.peek_past_ws() == Some(L_PARENS) { self.skip_ws(); self.builder.start_node(VERSION.into()); self.bump(); self.skip_ws(); self.builder.start_node(CONSTRAINT.into()); while self.current() == Some(L_ANGLE) || self.current() == Some(R_ANGLE) || self.current() == Some(EQUAL) { self.bump(); } self.builder.finish_node(); self.skip_ws(); if self.current() == Some(IDENT) { self.bump(); } else { self.error("Expected version".to_string()); } if self.current() == Some(R_PARENS) { self.bump(); } else { self.error("Expected ')'".to_string()); } self.builder.finish_node(); } self.builder.finish_node(); } fn parse(mut self) -> Parse { self.builder.start_node(SyntaxKind::ROOT.into()); self.skip_ws(); while self.current().is_some() { match self.current() { Some(IDENT) => self.parse_relation(), Some(COMMA) => { // Empty relation, but that's okay - probably? } Some(c) => { self.error(format!("expected identifier or comma but got {:?}", c)); } None => { self.error("expected identifier but got end of file".to_string()); } } self.skip_ws(); match self.current() { Some(COMMA) => { self.bump(); } None => { break; } c => { self.error(format!("expected comma or end of file but got {:?}", c)); } } self.skip_ws(); } self.builder.finish_node(); // Turn the builder into a GreenNode Parse { green_node: self.builder.finish(), errors: self.errors, } } /// Advance one token, adding it to the current branch of the tree builder. fn bump(&mut self) { let (kind, text) = self.tokens.pop().unwrap(); self.builder.token(kind.into(), text.as_str()); } /// Peek at the first unprocessed token fn current(&self) -> Option { self.tokens.last().map(|(kind, _)| *kind) } fn skip_ws(&mut self) { while self.current() == Some(WHITESPACE) || self.current() == Some(NEWLINE) { self.bump() } } fn peek_past_ws(&self) -> Option { let mut i = self.tokens.len(); while i > 0 { i -= 1; match self.tokens[i].0 { WHITESPACE | NEWLINE => {} _ => return Some(self.tokens[i].0), } } None } } let mut tokens = crate::relations::lex(text); tokens.reverse(); Parser { tokens, builder: GreenNodeBuilder::new(), errors: Vec::new(), } .parse() } /// To work with the parse results we need a view into the /// green tree - the Syntax tree. /// It is also immutable, like a GreenNode, /// but it contains parent pointers, offsets, and /// has identity semantics. type SyntaxNode = rowan::SyntaxNode; #[allow(unused)] type SyntaxToken = rowan::SyntaxToken; #[allow(unused)] type SyntaxElement = rowan::NodeOrToken; impl Parse { fn root_mut(&self) -> Relations { Relations::cast(SyntaxNode::new_root_mut(self.green_node.clone())).unwrap() } } macro_rules! ast_node { ($ast:ident, $kind:ident) => { /// A node in the syntax tree representing a $ast #[repr(transparent)] pub struct $ast(SyntaxNode); impl $ast { #[allow(unused)] fn cast(node: SyntaxNode) -> Option { if node.kind() == $kind { Some(Self(node)) } else { None } } } impl std::fmt::Display for $ast { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0.text().to_string()) } } }; } ast_node!(Relations, ROOT); ast_node!(Relation, RELATION); impl PartialEq for Relations { fn eq(&self, other: &Self) -> bool { self.relations().collect::>() == other.relations().collect::>() } } impl PartialEq for Relation { fn eq(&self, other: &Self) -> bool { self.name() == other.name() && self.version() == other.version() } } #[cfg(feature = "serde")] impl serde::Serialize for Relations { fn serialize(&self, serializer: S) -> Result { let rep = self.to_string(); serializer.serialize_str(&rep) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Relations { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; let relations = s.parse().map_err(serde::de::Error::custom)?; Ok(relations) } } impl std::fmt::Debug for Relations { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("Relations"); for relation in self.relations() { s.field("relation", &relation); } s.finish() } } impl std::fmt::Debug for Relation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("Relation"); s.field("name", &self.name()); if let Some((vc, version)) = self.version() { s.field("version", &vc); s.field("version", &version); } s.finish() } } #[cfg(feature = "serde")] impl serde::Serialize for Relation { fn serialize(&self, serializer: S) -> Result { let rep = self.to_string(); serializer.serialize_str(&rep) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Relation { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; let relation = s.parse().map_err(serde::de::Error::custom)?; Ok(relation) } } impl Default for Relations { fn default() -> Self { Self::new() } } impl Relations { /// Create a new relations field pub fn new() -> Self { Self::from(vec![]) } /// Wrap and sort this relations field #[must_use] pub fn wrap_and_sort(self) -> Self { let mut entries = self .relations() .map(|e| e.wrap_and_sort()) .collect::>(); entries.sort(); // TODO: preserve comments Self::from(entries) } /// Iterate over the entries in this relations field pub fn relations(&self) -> impl Iterator + '_ { self.0.children().filter_map(Relation::cast) } /// Iterate over the entries in this relations field pub fn iter(&self) -> impl Iterator + '_ { self.relations() } /// Remove the entry at the given index pub fn get_relation(&self, idx: usize) -> Option { self.relations().nth(idx) } /// Remove the relation at the given index pub fn remove_relation(&mut self, idx: usize) -> Relation { let mut relation = self.get_relation(idx).unwrap(); relation.remove(); relation } /// Insert a new relation at the given index pub fn insert(&mut self, idx: usize, relation: Relation) { let is_empty = !self.0.children_with_tokens().any(|n| n.kind() == COMMA); let (position, new_children) = if let Some(current_relation) = self.relations().nth(idx) { let to_insert: Vec> = if idx == 0 && is_empty { vec![relation.0.green().into()] } else { vec![ relation.0.green().into(), NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")), NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")), ] }; (current_relation.0.index(), to_insert) } else { let child_count = self.0.children_with_tokens().count(); ( child_count, if idx == 0 { vec![relation.0.green().into()] } else { vec![ NodeOrToken::Token(GreenToken::new(COMMA.into(), ",")), NodeOrToken::Token(GreenToken::new(WHITESPACE.into(), " ")), relation.0.green().into(), ] }, ) }; // We can safely replace the root here since Relations is a root node self.0 = SyntaxNode::new_root_mut( self.0.replace_with( self.0 .green() .splice_children(position..position, new_children), ), ); } /// Replace the relation at the given index pub fn replace(&mut self, idx: usize, relation: Relation) { let current_relation = self.get_relation(idx).unwrap(); self.0.splice_children( current_relation.0.index()..current_relation.0.index() + 1, vec![relation.0.into()], ); } /// Push a new relation to the relations field pub fn push(&mut self, relation: Relation) { let pos = self.relations().count(); self.insert(pos, relation); } /// Parse a relations field from a string, allowing syntax errors pub fn parse_relaxed(s: &str) -> (Relations, Vec) { let parse = parse(s); (parse.root_mut(), parse.errors) } /// Check if this relations field is satisfied by the given package versions. pub fn satisfied_by( &self, package_version: impl crate::relations::VersionLookup + Copy, ) -> bool { self.relations().all(|e| e.satisfied_by(package_version)) } /// Check if this relations field is empty pub fn is_empty(&self) -> bool { self.relations().count() == 0 } /// Get the number of entries in this relations field pub fn len(&self) -> usize { self.relations().count() } } impl From> for Relations { fn from(entries: Vec) -> Self { let mut builder = GreenNodeBuilder::new(); builder.start_node(ROOT.into()); for (i, relation) in entries.into_iter().enumerate() { if i > 0 { builder.token(COMMA.into(), ","); builder.token(WHITESPACE.into(), " "); } inject(&mut builder, relation.0); } builder.finish_node(); Relations(SyntaxNode::new_root_mut(builder.finish())) } } impl From for Relations { fn from(relation: Relation) -> Self { Self::from(vec![relation]) } } impl From for crate::lossy::Relation { fn from(relation: Relation) -> Self { let mut rel = crate::lossy::Relation::new(); rel.name = relation.name(); rel.version = relation.version().map(|(vc, v)| (vc, v)); rel } } impl From for crate::lossy::Relations { fn from(relations: Relations) -> Self { let mut rels = crate::lossy::Relations::new(); for relation in relations.relations() { rels.0.push(relation.into()); } rels } } impl From for Relations { fn from(relations: crate::lossy::Relations) -> Self { let mut entries = vec![]; for relation in relations.iter() { entries.push(relation.clone().into()); } Self::from(entries) } } impl From for Relation { fn from(relation: crate::lossy::Relation) -> Self { Relation::new(&relation.name, relation.version) } } fn inject(builder: &mut GreenNodeBuilder, node: SyntaxNode) { builder.start_node(node.kind().into()); for child in node.children_with_tokens() { match child { rowan::NodeOrToken::Node(child) => { inject(builder, child); } rowan::NodeOrToken::Token(token) => { builder.token(token.kind().into(), token.text()); } } } builder.finish_node(); } impl Relation { /// Create a new relation /// /// # Arguments /// * `name` - The name of the package /// * `version_constraint` - The version constraint and version to use /// /// # Example /// ``` /// use r_description::lossless::{Relation}; /// use r_description::VersionConstraint; /// let relation = Relation::new("vign", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap()))); /// assert_eq!(relation.to_string(), "vign (>= 2.0)"); /// ``` pub fn new(name: &str, version_constraint: Option<(VersionConstraint, Version)>) -> Self { let mut builder = GreenNodeBuilder::new(); builder.start_node(SyntaxKind::RELATION.into()); builder.token(IDENT.into(), name); if let Some((vc, version)) = version_constraint { builder.token(WHITESPACE.into(), " "); builder.start_node(SyntaxKind::VERSION.into()); builder.token(L_PARENS.into(), "("); builder.start_node(SyntaxKind::CONSTRAINT.into()); for c in vc.to_string().chars() { builder.token( match c { '>' => R_ANGLE.into(), '<' => L_ANGLE.into(), '=' => EQUAL.into(), _ => unreachable!(), }, c.to_string().as_str(), ); } builder.finish_node(); builder.token(WHITESPACE.into(), " "); builder.token(IDENT.into(), version.to_string().as_str()); builder.token(R_PARENS.into(), ")"); builder.finish_node(); } builder.finish_node(); Relation(SyntaxNode::new_root_mut(builder.finish())) } /// Wrap and sort this relation /// /// # Example /// ``` /// use r_description::lossless::Relation; /// let relation = " vign ( >= 2.0) ".parse::().unwrap(); /// assert_eq!(relation.wrap_and_sort().to_string(), "vign (>= 2.0)"); /// ``` #[must_use] pub fn wrap_and_sort(&self) -> Self { let mut builder = GreenNodeBuilder::new(); builder.start_node(SyntaxKind::RELATION.into()); builder.token(IDENT.into(), self.name().as_str()); if let Some((vc, version)) = self.version() { builder.token(WHITESPACE.into(), " "); builder.start_node(SyntaxKind::VERSION.into()); builder.token(L_PARENS.into(), "("); builder.start_node(SyntaxKind::CONSTRAINT.into()); builder.token( match vc { VersionConstraint::GreaterThanEqual => R_ANGLE.into(), VersionConstraint::LessThanEqual => L_ANGLE.into(), VersionConstraint::Equal => EQUAL.into(), VersionConstraint::GreaterThan => R_ANGLE.into(), VersionConstraint::LessThan => L_ANGLE.into(), }, vc.to_string().as_str(), ); builder.finish_node(); builder.token(WHITESPACE.into(), " "); builder.token(IDENT.into(), version.to_string().as_str()); builder.token(R_PARENS.into(), ")"); builder.finish_node(); } builder.finish_node(); Relation(SyntaxNode::new_root_mut(builder.finish())) } /// Create a new simple relation, without any version constraints. /// /// # Example /// ``` /// use r_description::lossless::Relation; /// let relation = Relation::simple("vign"); /// assert_eq!(relation.to_string(), "vign"); /// ``` pub fn simple(name: &str) -> Self { Self::new(name, None) } /// Remove the version constraint from the relation. /// /// # Example /// ``` /// use r_description::lossless::{Relation}; /// use r_description::VersionConstraint; /// let mut relation = Relation::new("vign", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap()))); /// relation.drop_constraint(); /// assert_eq!(relation.to_string(), "vign"); /// ``` pub fn drop_constraint(&mut self) -> bool { let version_token = self.0.children().find(|n| n.kind() == VERSION); if let Some(version_token) = version_token { // Remove any whitespace before the version token while let Some(prev) = version_token.prev_sibling_or_token() { if prev.kind() == WHITESPACE || prev.kind() == NEWLINE { prev.detach(); } else { break; } } version_token.detach(); return true; } false } /// Return the name of the package in the relation. /// /// # Example /// ``` /// use r_description::lossless::Relation; /// let relation = Relation::simple("vign"); /// assert_eq!(relation.name(), "vign"); /// ``` pub fn name(&self) -> String { self.0 .children_with_tokens() .find_map(|it| match it { SyntaxElement::Token(token) if token.kind() == IDENT => Some(token), _ => None, }) .unwrap() .text() .to_string() } /// Return the version constraint and the version it is constrained to. pub fn version(&self) -> Option<(VersionConstraint, Version)> { let vc = self.0.children().find(|n| n.kind() == VERSION); let vc = vc.as_ref()?; let constraint = vc.children().find(|n| n.kind() == CONSTRAINT); let version = vc.children_with_tokens().find_map(|it| match it { SyntaxElement::Token(token) if token.kind() == IDENT => Some(token), _ => None, }); if let (Some(constraint), Some(version)) = (constraint, version) { let vc: VersionConstraint = constraint.to_string().parse().unwrap(); return Some((vc, (version.text().to_string()).parse().unwrap())); } else { None } } /// Set the version constraint for this relation /// /// # Example /// ``` /// use r_description::lossless::{Relation}; /// use r_description::VersionConstraint; /// let mut relation = Relation::simple("vign"); /// relation.set_version(Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap()))); /// assert_eq!(relation.to_string(), "vign (>= 2.0)"); /// ``` pub fn set_version(&mut self, version_constraint: Option<(VersionConstraint, Version)>) { let current_version = self.0.children().find(|n| n.kind() == VERSION); if let Some((vc, version)) = version_constraint { let mut builder = GreenNodeBuilder::new(); builder.start_node(VERSION.into()); builder.token(L_PARENS.into(), "("); builder.start_node(CONSTRAINT.into()); match vc { VersionConstraint::GreaterThanEqual => { builder.token(R_ANGLE.into(), ">"); builder.token(EQUAL.into(), "="); } VersionConstraint::LessThanEqual => { builder.token(L_ANGLE.into(), "<"); builder.token(EQUAL.into(), "="); } VersionConstraint::Equal => { builder.token(EQUAL.into(), "="); } VersionConstraint::GreaterThan => { builder.token(R_ANGLE.into(), ">"); } VersionConstraint::LessThan => { builder.token(L_ANGLE.into(), "<"); } } builder.finish_node(); // CONSTRAINT builder.token(WHITESPACE.into(), " "); builder.token(IDENT.into(), version.to_string().as_str()); builder.token(R_PARENS.into(), ")"); builder.finish_node(); // VERSION if let Some(current_version) = current_version { self.0.splice_children( current_version.index()..current_version.index() + 1, vec![SyntaxNode::new_root_mut(builder.finish()).into()], ); } else { let name_node = self.0.children_with_tokens().find(|n| n.kind() == IDENT); let idx = if let Some(name_node) = name_node { name_node.index() + 1 } else { 0 }; let new_children = vec![ GreenToken::new(WHITESPACE.into(), " ").into(), builder.finish().into(), ]; let new_root = SyntaxNode::new_root_mut( self.0.green().splice_children(idx..idx, new_children), ); if let Some(parent) = self.0.parent() { parent.splice_children( self.0.index()..self.0.index() + 1, vec![new_root.into()], ); self.0 = parent .children_with_tokens() .nth(self.0.index()) .unwrap() .clone() .into_node() .unwrap(); } else { self.0 = new_root; } } } else if let Some(current_version) = current_version { // Remove any whitespace before the version token while let Some(prev) = current_version.prev_sibling_or_token() { if prev.kind() == WHITESPACE || prev.kind() == NEWLINE { prev.detach(); } else { break; } } current_version.detach(); } } /// Remove this relation /// /// # Example /// ``` /// use r_description::lossless::{Relation, Relations}; /// let mut relations: Relations = r"cli (>= 0.19.0), blah (<< 1.26.0)".parse().unwrap(); /// let mut relation = relations.get_relation(0).unwrap(); /// assert_eq!(relation.to_string(), "cli (>= 0.19.0)"); /// relation.remove(); /// assert_eq!(relations.to_string(), "blah (<< 1.26.0)"); /// ``` pub fn remove(&mut self) { let is_first = !self .0 .siblings(Direction::Prev) .skip(1) .any(|n| n.kind() == RELATION); if !is_first { // Not the first item in the list. Remove whitespace backwards to the previous // pipe, the pipe and any whitespace until the previous relation while let Some(n) = self.0.prev_sibling_or_token() { if n.kind() == WHITESPACE || n.kind() == NEWLINE { n.detach(); } else if n.kind() == COMMA { n.detach(); break; } else { break; } } while let Some(n) = self.0.prev_sibling_or_token() { if n.kind() == WHITESPACE || n.kind() == NEWLINE { n.detach(); } else { break; } } } else { // First item in the list. Remove whitespace up to the pipe, the pipe and anything // before the next relation while let Some(n) = self.0.next_sibling_or_token() { if n.kind() == WHITESPACE || n.kind() == NEWLINE { n.detach(); } else if n.kind() == COMMA { n.detach(); break; } else { panic!("Unexpected node: {:?}", n); } } while let Some(n) = self.0.next_sibling_or_token() { if n.kind() == WHITESPACE || n.kind() == NEWLINE { n.detach(); } else { break; } } } self.0.detach(); } /// Check if this relation is satisfied by the given package version. pub fn satisfied_by( &self, package_version: impl crate::relations::VersionLookup + Copy, ) -> bool { let name = self.name(); let version = self.version(); if let Some(version) = version { if let Some(package_version) = package_version.lookup_version(&name) { match version.0 { VersionConstraint::GreaterThanEqual => { package_version.into_owned() >= version.1 } VersionConstraint::LessThanEqual => { package_version.into_owned() <= version.1 } VersionConstraint::Equal => package_version.into_owned() == version.1, VersionConstraint::GreaterThan => package_version.into_owned() > version.1, VersionConstraint::LessThan => package_version.into_owned() < version.1, } } else { false } } else { true } } } impl PartialOrd for Relation { fn partial_cmp(&self, other: &Self) -> Option { // Compare by name first, then by version let name_cmp = self.name().cmp(&other.name()); if name_cmp != std::cmp::Ordering::Equal { return Some(name_cmp); } let self_version = self.version(); let other_version = other.version(); match (self_version, other_version) { (Some((self_vc, self_version)), Some((other_vc, other_version))) => { let vc_cmp = self_vc.cmp(&other_vc); if vc_cmp != std::cmp::Ordering::Equal { return Some(vc_cmp); } Some(self_version.cmp(&other_version)) } (Some(_), None) => Some(std::cmp::Ordering::Greater), (None, Some(_)) => Some(std::cmp::Ordering::Less), (None, None) => Some(std::cmp::Ordering::Equal), } } } impl Eq for Relation {} impl Ord for Relation { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other).unwrap() } } impl std::str::FromStr for Relations { type Err = String; fn from_str(s: &str) -> Result { let parse = parse(s); if parse.errors.is_empty() { Ok(parse.root_mut()) } else { Err(parse.errors.join("\n")) } } } impl std::str::FromStr for Relation { type Err = String; fn from_str(s: &str) -> Result { let rels = s.parse::()?; let mut relations = rels.relations(); let relation = if let Some(relation) = relations.next() { relation } else { return Err("No relation found".to_string()); }; if relations.next().is_some() { return Err("Multiple relations found".to_string()); } Ok(relation) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse() { let input = "cli"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.relations().count(), 1); let relation = parsed.relations().next().unwrap(); assert_eq!(relation.to_string(), "cli"); assert_eq!(relation.version(), None); let input = "cli (>= 0.20.21)"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.relations().count(), 1); let relation = parsed.relations().next().unwrap(); assert_eq!(relation.to_string(), "cli (>= 0.20.21)"); assert_eq!( relation.version(), Some(( VersionConstraint::GreaterThanEqual, "0.20.21".parse().unwrap() )) ); } #[test] fn test_multiple() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.relations().count(), 2); let relation = parsed.relations().next().unwrap(); assert_eq!(relation.to_string(), "cli (>= 0.20.21)"); assert_eq!( relation.version(), Some(( VersionConstraint::GreaterThanEqual, "0.20.21".parse().unwrap() )) ); let relation = parsed.relations().nth(1).unwrap(); assert_eq!(relation.to_string(), "cli (<< 0.21)"); assert_eq!( relation.version(), Some((VersionConstraint::LessThan, "0.21".parse().unwrap())) ); } #[test] fn test_new() { let r = Relation::new( "cli", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())), ); assert_eq!(r.to_string(), "cli (>= 2.0)"); } #[test] fn test_drop_constraint() { let mut r = Relation::new( "cli", Some((VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap())), ); r.drop_constraint(); assert_eq!(r.to_string(), "cli"); } #[test] fn test_simple() { let r = Relation::simple("cli"); assert_eq!(r.to_string(), "cli"); } #[test] fn test_remove_first_relation() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21)"#.parse().unwrap(); let removed = rels.remove_relation(0); assert_eq!(removed.to_string(), "cli (>= 0.20.21)"); assert_eq!(rels.to_string(), "cli (<< 0.21)"); } #[test] fn test_remove_last_relation() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21)"#.parse().unwrap(); rels.remove_relation(1); assert_eq!(rels.to_string(), "cli (>= 0.20.21)"); } #[test] fn test_remove_middle() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21), cli (<< 0.22)"#.parse().unwrap(); rels.remove_relation(1); assert_eq!(rels.to_string(), "cli (>= 0.20.21), cli (<< 0.22)"); } #[test] fn test_remove_added() { let mut rels: Relations = r#"cli (>= 0.20.21)"#.parse().unwrap(); let relation = Relation::simple("cli"); rels.push(relation); rels.remove_relation(1); assert_eq!(rels.to_string(), "cli (>= 0.20.21)"); } #[test] fn test_push() { let mut rels: Relations = r#"cli (>= 0.20.21)"#.parse().unwrap(); let relation = Relation::simple("cli"); rels.push(relation); assert_eq!(rels.to_string(), "cli (>= 0.20.21), cli"); } #[test] fn test_push_from_empty() { let mut rels: Relations = "".parse().unwrap(); let relation = Relation::simple("cli"); rels.push(relation); assert_eq!(rels.to_string(), "cli"); } #[test] fn test_insert() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21)"#.parse().unwrap(); let relation = Relation::simple("cli"); rels.insert(1, relation); assert_eq!(rels.to_string(), "cli (>= 0.20.21), cli, cli (<< 0.21)"); } #[test] fn test_insert_at_start() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21)"#.parse().unwrap(); let relation = Relation::simple("cli"); rels.insert(0, relation); assert_eq!(rels.to_string(), "cli, cli (>= 0.20.21), cli (<< 0.21)"); } #[test] fn test_insert_after_error() { let (mut rels, errors) = Relations::parse_relaxed("@foo@, debhelper (>= 1.0)"); assert_eq!( errors, vec![ "expected identifier or comma but got ERROR", "expected comma or end of file but got Some(IDENT)", "expected identifier or comma but got ERROR" ] ); let relation = Relation::simple("bar"); rels.push(relation); assert_eq!(rels.to_string(), "@foo@, debhelper (>= 1.0), bar"); } #[test] fn test_insert_before_error() { let (mut rels, errors) = Relations::parse_relaxed("debhelper (>= 1.0), @foo@, bla"); assert_eq!( errors, vec![ "expected identifier or comma but got ERROR", "expected comma or end of file but got Some(IDENT)", "expected identifier or comma but got ERROR" ] ); let relation = Relation::simple("bar"); rels.insert(0, relation); assert_eq!(rels.to_string(), "bar, debhelper (>= 1.0), @foo@, bla"); } #[test] fn test_replace() { let mut rels: Relations = r#"cli (>= 0.20.21), cli (<< 0.21)"#.parse().unwrap(); let relation = Relation::simple("cli"); rels.replace(1, relation); assert_eq!(rels.to_string(), "cli (>= 0.20.21), cli"); } #[test] fn test_parse_relation() { let parsed: Relation = "cli (>= 0.20.21)".parse().unwrap(); assert_eq!(parsed.to_string(), "cli (>= 0.20.21)"); assert_eq!( parsed.version(), Some(( VersionConstraint::GreaterThanEqual, "0.20.21".parse().unwrap() )) ); assert_eq!( "foo, bar".parse::().unwrap_err(), "Multiple relations found" ); assert_eq!("".parse::().unwrap_err(), "No relation found"); } #[test] fn test_relations_satisfied_by() { let rels: Relations = "cli (>= 0.20.21), cli (<< 0.21)".parse().unwrap(); let satisfied = |name: &str| -> Option { match name { "cli" => Some("0.20.21".parse().unwrap()), _ => None, } }; assert!(rels.satisfied_by(satisfied)); let satisfied = |name: &str| match name { "cli" => Some("0.21".parse().unwrap()), _ => None, }; assert!(!rels.satisfied_by(satisfied)); let satisfied = |name: &str| match name { "cli" => Some("0.20.20".parse().unwrap()), _ => None, }; assert!(!rels.satisfied_by(satisfied)); } #[test] fn test_wrap_and_sort_relation() { let relation: Relation = " cli (>= 11.0)".parse().unwrap(); let wrapped = relation.wrap_and_sort(); assert_eq!(wrapped.to_string(), "cli (>= 11.0)"); } #[test] fn test_wrap_and_sort_relations() { let relations: Relations = "cli (>= 0.20.21) , \n\n\n\ncli (<< 0.21)".parse().unwrap(); let wrapped = relations.wrap_and_sort(); assert_eq!(wrapped.to_string(), "cli (<< 0.21), cli (>= 0.20.21)"); } #[cfg(feature = "serde")] #[test] fn test_serialize_relations() { let relations: Relations = "cli (>= 0.20.21), cli (<< 0.21)".parse().unwrap(); let serialized = serde_json::to_string(&relations).unwrap(); assert_eq!(serialized, r#""cli (>= 0.20.21), cli (<< 0.21)""#); } #[cfg(feature = "serde")] #[test] fn test_deserialize_relations() { let relations: Relations = "cli (>= 0.20.21), cli (<< 0.21)".parse().unwrap(); let serialized = serde_json::to_string(&relations).unwrap(); let deserialized: Relations = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.to_string(), relations.to_string()); } #[cfg(feature = "serde")] #[test] fn test_serialize_relation() { let relation: Relation = "cli (>= 0.20.21)".parse().unwrap(); let serialized = serde_json::to_string(&relation).unwrap(); assert_eq!(serialized, r#""cli (>= 0.20.21)""#); } #[cfg(feature = "serde")] #[test] fn test_deserialize_relation() { let relation: Relation = "cli (>= 0.20.21)".parse().unwrap(); let serialized = serde_json::to_string(&relation).unwrap(); let deserialized: Relation = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized.to_string(), relation.to_string()); } #[test] fn test_relation_set_version() { let mut rel: Relation = "vign".parse().unwrap(); rel.set_version(None); assert_eq!("vign", rel.to_string()); rel.set_version(Some(( VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap(), ))); assert_eq!("vign (>= 2.0)", rel.to_string()); rel.set_version(None); assert_eq!("vign", rel.to_string()); rel.set_version(Some(( VersionConstraint::GreaterThanEqual, "2.0".parse().unwrap(), ))); rel.set_version(Some(( VersionConstraint::GreaterThanEqual, "1.1".parse().unwrap(), ))); assert_eq!("vign (>= 1.1)", rel.to_string()); } #[test] fn test_wrap_and_sort_removes_empty_entries() { let relations: Relations = "foo, , bar, ".parse().unwrap(); let wrapped = relations.wrap_and_sort(); assert_eq!(wrapped.to_string(), "bar, foo"); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse() { let s = r###"Package: mypackage Title: What the Package Does (One Line, Title Case) Version: 0.0.0.9000 Authors@R: person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID")) Description: What the package does (one paragraph). License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a license Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 "###; let desc: RDescription = s.parse().unwrap(); assert_eq!(desc.package(), Some("mypackage".to_string())); assert_eq!( desc.title(), Some("What the Package Does (One Line, Title Case)".to_string()) ); assert_eq!(desc.version(), Some("0.0.0.9000".to_string())); assert_eq!( desc.authors(), Some(RCode( r#"person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID"))"# .to_string() )) ); assert_eq!( desc.description(), Some("What the package does (one paragraph).".to_string()) ); assert_eq!( desc.license(), Some( "`use_mit_license()`, `use_gpl3_license()` or friends to pick a\nlicense" .to_string() ) ); assert_eq!(desc.encoding(), Some("UTF-8".to_string())); assert_eq!(desc.roxygen(), Some("list(markdown = TRUE)".to_string())); assert_eq!(desc.roxygen_note(), Some("7.3.2".to_string())); assert_eq!(desc.to_string(), s); } #[test] fn test_parse_dplyr() { let s = include_str!("../testdata/dplyr.desc"); let desc: RDescription = s.parse().unwrap(); assert_eq!("dplyr", desc.package().unwrap()); assert_eq!( "https://dplyr.tidyverse.org, https://github.com/tidyverse/dplyr", desc.url().unwrap().as_str() ); } } r-description-0.3.1/src/lossy.rs000064400000000000000000000510341046102023000147200ustar 00000000000000/// A library for parsing and manipulating R DESCRIPTION files. /// /// See https://r-pkgs.org/description.html and https://cran.r-project.org/doc/manuals/R-exts.html /// for more information /// /// See the ``lossless`` module for a lossless parser that is /// forgiving in the face of errors and preserves formatting while editing /// at the expense of a more complex API. use deb822_lossless::{FromDeb822, FromDeb822Paragraph, ToDeb822, ToDeb822Paragraph}; use crate::RCode; use std::iter::Peekable; use crate::relations::SyntaxKind::*; use crate::relations::{lex, SyntaxKind, VersionConstraint}; use crate::version::Version; #[derive(Debug, Clone, PartialEq, Eq)] pub struct UrlEntry { pub url: url::Url, pub label: Option, } impl std::fmt::Display for UrlEntry { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.url.as_str())?; if let Some(label) = &self.label { write!(f, " ({})", label)?; } Ok(()) } } impl std::str::FromStr for UrlEntry { type Err = String; fn from_str(s: &str) -> Result { if let Some(pos) = s.find('(') { let url = s[..pos].trim(); let label = s[pos + 1..s.len() - 1].trim(); Ok(UrlEntry { url: url::Url::parse(url).map_err(|e| e.to_string())?, label: Some(label.to_string()), }) } else { Ok(UrlEntry { url: url::Url::parse(s).map_err(|e| e.to_string())?, label: None, }) } } } fn serialize_url_list(urls: &[UrlEntry]) -> String { let mut s = String::new(); for (i, url) in urls.iter().enumerate() { if i > 0 { s.push_str(", "); } s.push_str(url.to_string().as_str()); } s } fn deserialize_url_list(s: &str) -> Result, String> { s.split([',', '\n'].as_ref()) .filter(|s| !s.trim().is_empty()) .map(|s| s.trim().parse()) .collect::, String>>() .map_err(|e| e.to_string()) } #[derive(FromDeb822, ToDeb822, Debug, PartialEq, Eq)] pub struct RDescription { #[deb822(field = "Package")] pub name: String, #[deb822(field = "Description")] pub description: String, #[deb822(field = "Title")] pub title: String, #[deb822(field = "Maintainer")] pub maintainer: Option, #[deb822(field = "Author")] /// Who wrote the the package pub author: Option, // 'Authors@R' is a special field that can contain R code // that is evaluated to get the authors and maintainers. #[deb822(field = "Authors@R")] pub authors: Option, #[deb822(field = "Version")] pub version: Version, /// If the DESCRIPTION file is not written in pure ASCII, the encoding /// field must be used to specify the encoding. #[deb822(field = "Encoding")] pub encoding: Option, #[deb822(field = "License")] pub license: String, #[deb822(field = "URL", serialize_with = serialize_url_list, deserialize_with = deserialize_url_list)] // TODO: parse this as a list of URLs, separated by commas pub url: Option>, #[deb822(field = "BugReports")] pub bug_reports: Option, #[deb822(field = "Imports")] pub imports: Option, #[deb822(field = "Suggests")] pub suggests: Option, #[deb822(field = "Depends")] pub depends: Option, #[deb822(field = "LinkingTo")] pub linking_to: Option, #[deb822(field = "LazyData")] pub lazy_data: Option, #[deb822(field = "Collate")] pub collate: Option, #[deb822(field = "VignetteBuilder")] pub vignette_builder: Option, #[deb822(field = "SystemRequirements")] pub system_requirements: Option, #[deb822(field = "Date")] /// The release date of the current version of the package. /// Strongly recommended to use the ISO 8601 format: YYYY-MM-DD pub date: Option, #[deb822(field = "Language")] /// Indicates the package documentation is not in English. /// This should be a comma-separated list of IETF language /// tags as defined by RFC5646 pub language: Option, #[deb822(field = "Repository")] /// The R Repository to use for this package. E.g. "CRAN" or "Bioconductor" pub repository: Option, } /// A relation entry in a relationship field. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Relation { /// Package name. pub name: String, /// Version constraint and version. pub version: Option<(VersionConstraint, Version)>, } impl Default for Relation { fn default() -> Self { Self::new() } } impl Relation { /// Create an empty relation. pub fn new() -> Self { Self { name: String::new(), version: None, } } /// Check if this entry is satisfied by the given package versions. /// /// # Arguments /// * `package_version` - A function that returns the version of a package. /// /// # Example /// ``` /// use r_description::lossy::Relation; /// use r_description::Version; /// let entry: Relation = "cli (>= 2.0)".parse().unwrap(); /// assert!(entry.satisfied_by(|name: &str| -> Option { /// match name { /// "cli" => Some("2.0".parse().unwrap()), /// _ => None /// }})); /// ``` pub fn satisfied_by(&self, package_version: impl crate::relations::VersionLookup) -> bool { let actual = package_version.lookup_version(self.name.as_str()); if let Some((vc, version)) = &self.version { if let Some(actual) = actual { match vc { VersionConstraint::GreaterThanEqual => actual.as_ref() >= version, VersionConstraint::LessThanEqual => actual.as_ref() <= version, VersionConstraint::Equal => actual.as_ref() == version, VersionConstraint::GreaterThan => actual.as_ref() > version, VersionConstraint::LessThan => actual.as_ref() < version, } } else { false } } else { actual.is_some() } } } impl std::fmt::Display for Relation { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.name)?; if let Some((constraint, version)) = &self.version { write!(f, " ({} {})", constraint, version)?; } Ok(()) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Relation { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; s.parse().map_err(serde::de::Error::custom) } } #[cfg(feature = "serde")] impl serde::Serialize for Relation { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.to_string().serialize(serializer) } } /// A collection of relation entries in a relationship field. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Relations(pub Vec); impl std::ops::Index for Relations { type Output = Relation; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } impl std::ops::IndexMut for Relations { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } } impl FromIterator for Relations { fn from_iter>(iter: I) -> Self { Self(iter.into_iter().collect()) } } impl Default for Relations { fn default() -> Self { Self::new() } } impl Relations { /// Create an empty relations. pub fn new() -> Self { Self(Vec::new()) } /// Remove an entry from the relations. pub fn remove(&mut self, index: usize) { self.0.remove(index); } /// Iterate over the entries in the relations. pub fn iter(&self) -> impl Iterator { self.0.iter() } /// Number of entries in the relations. pub fn len(&self) -> usize { self.0.len() } /// Check if the relations are empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Check if the relations are satisfied by the given package versions. pub fn satisfied_by( &self, package_version: impl crate::relations::VersionLookup + Copy, ) -> bool { self.0.iter().all(|r| r.satisfied_by(package_version)) } } impl std::fmt::Display for Relations { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for (i, relation) in self.0.iter().enumerate() { if i > 0 { f.write_str(", ")?; } write!(f, "{}", relation)?; } Ok(()) } } impl std::str::FromStr for Relation { type Err = String; fn from_str(s: &str) -> Result { let tokens = lex(s); let mut tokens = tokens.into_iter().peekable(); fn eat_whitespace(tokens: &mut Peekable>) { while let Some((k, _)) = tokens.peek() { match k { WHITESPACE | NEWLINE => { tokens.next(); } _ => break, } } } let name = match tokens.next() { Some((IDENT, name)) => name, _ => return Err("Expected package name".to_string()), }; eat_whitespace(&mut tokens); let version = if let Some((L_PARENS, _)) = tokens.peek() { tokens.next(); eat_whitespace(&mut tokens); let mut constraint = String::new(); while let Some((kind, t)) = tokens.peek() { match kind { EQUAL | L_ANGLE | R_ANGLE => { constraint.push_str(t); tokens.next(); } _ => break, } } let constraint = constraint.parse()?; eat_whitespace(&mut tokens); // Read IDENT and COLON tokens until we see R_PARENS let version_string = match tokens.next() { Some((IDENT, s)) => s, _ => return Err("Expected version string".to_string()), }; let version: Version = version_string.parse().map_err(|e: String| e.to_string())?; eat_whitespace(&mut tokens); if let Some((R_PARENS, _)) = tokens.next() { } else { return Err(format!("Expected ')', found {:?}", tokens.next())); } Some((constraint, version)) } else { None }; eat_whitespace(&mut tokens); if let Some((kind, _)) = tokens.next() { return Err(format!("Unexpected token: {:?}", kind)); } Ok(Relation { name, version }) } } impl std::str::FromStr for Relations { type Err = String; fn from_str(s: &str) -> Result { let mut relations = Vec::new(); if s.is_empty() { return Ok(Relations(relations)); } for relation in s.split(',') { let relation = relation.trim(); if relation.is_empty() { // Ignore empty entries. continue; } relations.push(relation.parse()?); } Ok(Relations(relations)) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Relations { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; s.parse().map_err(serde::de::Error::custom) } } #[cfg(feature = "serde")] impl serde::Serialize for Relations { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.to_string().serialize(serializer) } } impl std::str::FromStr for RDescription { type Err = String; fn from_str(s: &str) -> Result { let para: deb822_lossless::Paragraph = s .parse() .map_err(|e: deb822_lossless::ParseError| e.to_string())?; Self::from_paragraph(¶) } } impl std::fmt::Display for RDescription { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let para: deb822_lossless::lossy::Paragraph = self.to_paragraph(); f.write_str(¶.to_string())?; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse() { let s = r###"Package: mypackage Title: What the Package Does (One Line, Title Case) Version: 0.0.0.9000 Authors@R: person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID")) Description: What the package does (one paragraph). License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a license Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 "###; let desc: RDescription = s.parse().unwrap(); assert_eq!(desc.name, "mypackage".to_string()); assert_eq!( desc.title, "What the Package Does (One Line, Title Case)".to_string() ); assert_eq!(desc.version, "0.0.0.9000".parse().unwrap()); assert_eq!( desc.authors, Some(RCode( r#"person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID"))"# .to_string() )) ); assert_eq!( desc.description, "What the package does (one paragraph).".to_string() ); assert_eq!( desc.license, "`use_mit_license()`, `use_gpl3_license()` or friends to pick a\nlicense".to_string() ); assert_eq!(desc.encoding, Some("UTF-8".to_string())); assert_eq!( desc.to_string(), r###"Package: mypackage Description: What the package does (one paragraph). Title: What the Package Does (One Line, Title Case) Authors@R: person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID")) Version: 0.0.0.9000 Encoding: UTF-8 License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a license "### ); } #[test] fn test_parse_dplyr() { let s = include_str!("../testdata/dplyr.desc"); let desc: RDescription = s.parse().unwrap(); assert_eq!(desc.name, "dplyr".to_string()); } #[test] fn test_parse_relations() { let input = "cli"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.len(), 1); let relation = &parsed[0]; assert_eq!(relation.to_string(), "cli"); assert_eq!(relation.version, None); let input = "cli (>= 0.20.21)"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.len(), 1); let relation = &parsed[0]; assert_eq!(relation.to_string(), "cli (>= 0.20.21)"); assert_eq!( relation.version, Some(( VersionConstraint::GreaterThanEqual, "0.20.21".parse().unwrap() )) ); } #[test] fn test_multiple() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.to_string(), input); assert_eq!(parsed.len(), 2); let relation = &parsed[0]; assert_eq!(relation.to_string(), "cli (>= 0.20.21)"); assert_eq!( relation.version, Some(( VersionConstraint::GreaterThanEqual, "0.20.21".parse().unwrap() )) ); let relation = &parsed[1]; assert_eq!(relation.to_string(), "cli (<< 0.21)"); assert_eq!( relation.version, Some((VersionConstraint::LessThan, "0.21".parse().unwrap())) ); } #[cfg(feature = "serde")] #[test] fn test_serde_relations() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let parsed: Relations = input.parse().unwrap(); let serialized = serde_json::to_string(&parsed).unwrap(); assert_eq!(serialized, r#""cli (>= 0.20.21), cli (<< 0.21)""#); let deserialized: Relations = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, parsed); } #[cfg(feature = "serde")] #[test] fn test_serde_relation() { let input = "cli (>= 0.20.21)"; let parsed: Relation = input.parse().unwrap(); let serialized = serde_json::to_string(&parsed).unwrap(); assert_eq!(serialized, r#""cli (>= 0.20.21)""#); let deserialized: Relation = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, parsed); } #[test] fn test_relations_is_empty() { let input = "cli (>= 0.20.21)"; let parsed: Relations = input.parse().unwrap(); assert!(!parsed.is_empty()); let input = ""; let parsed: Relations = input.parse().unwrap(); assert!(parsed.is_empty()); } #[test] fn test_relations_len() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let parsed: Relations = input.parse().unwrap(); assert_eq!(parsed.len(), 2); } #[test] fn test_relations_remove() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let mut parsed: Relations = input.parse().unwrap(); parsed.remove(1); assert_eq!(parsed.len(), 1); assert_eq!(parsed.to_string(), "cli (>= 0.20.21)"); } #[test] fn test_relations_satisfied_by() { let input = "cli (>= 0.20.21), cli (<< 0.21)"; let parsed: Relations = input.parse().unwrap(); assert!(parsed.satisfied_by(|name: &str| -> Option { match name { "cli" => Some("0.20.21".parse().unwrap()), _ => None, } })); assert!(!parsed.satisfied_by(|name: &str| -> Option { match name { "cli" => Some("0.21".parse().unwrap()), _ => None, } })); } #[test] fn test_relation_satisfied_by() { let input = "cli (>= 0.20.21)"; let parsed: Relation = input.parse().unwrap(); assert!(parsed.satisfied_by(|name: &str| -> Option { match name { "cli" => Some("0.20.21".parse().unwrap()), _ => None, } })); assert!(!parsed.satisfied_by(|name: &str| -> Option { match name { "cli" => Some("0.20.20".parse().unwrap()), _ => None, } })); } #[test] fn test_parse_url_entry() { let input = "https://example.com/"; let parsed: UrlEntry = input.parse().unwrap(); assert_eq!(parsed.url.as_str(), input); assert_eq!(parsed.label, None); let input = "https://example.com (Example)"; let parsed: UrlEntry = input.parse().unwrap(); assert_eq!(parsed.url.as_str(), "https://example.com/"); assert_eq!(parsed.label, Some("Example".to_string())); } #[test] fn test_deserialize_url_list() { let input = "https://example.com/, https://example.org (Example)"; let parsed = deserialize_url_list(input).unwrap(); assert_eq!(parsed.len(), 2); assert_eq!(parsed[0].url.as_str(), "https://example.com/"); assert_eq!(parsed[0].label, None); assert_eq!(parsed[1].url.as_str(), "https://example.org/"); assert_eq!(parsed[1].label, Some("Example".to_string())); } #[test] fn test_deserialize_url_list2() { let input = "https://example.com/\n https://example.org (Example)\n https://example.net"; let parsed = deserialize_url_list(input).unwrap(); assert_eq!(parsed.len(), 3); assert_eq!(parsed[0].url.as_str(), "https://example.com/"); assert_eq!(parsed[0].label, None); assert_eq!(parsed[1].url.as_str(), "https://example.org/"); assert_eq!(parsed[1].label, Some("Example".to_string())); assert_eq!(parsed[2].url.as_str(), "https://example.net/"); assert_eq!(parsed[2].label, None); } } r-description-0.3.1/src/relations.rs000064400000000000000000000140111046102023000155410ustar 00000000000000//! Parsing of Debian relations strings. use crate::version::Version; use std::borrow::Cow; use std::iter::Peekable; use std::str::Chars; /// Constraint on a Debian package version. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum VersionConstraint { /// << LessThan, // << /// <= LessThanEqual, // <= /// = Equal, // = /// >> GreaterThan, // >> /// >= GreaterThanEqual, // >= } impl std::str::FromStr for VersionConstraint { type Err = String; fn from_str(s: &str) -> Result { match s { ">=" => Ok(VersionConstraint::GreaterThanEqual), "<=" => Ok(VersionConstraint::LessThanEqual), "=" => Ok(VersionConstraint::Equal), ">>" => Ok(VersionConstraint::GreaterThan), "<<" => Ok(VersionConstraint::LessThan), _ => Err(format!("Invalid version constraint: {}", s)), } } } impl std::fmt::Display for VersionConstraint { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { VersionConstraint::GreaterThanEqual => f.write_str(">="), VersionConstraint::LessThanEqual => f.write_str("<="), VersionConstraint::Equal => f.write_str("="), VersionConstraint::GreaterThan => f.write_str(">>"), VersionConstraint::LessThan => f.write_str("<<"), } } } /// Let's start with defining all kinds of tokens and /// composite nodes. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] #[repr(u16)] #[allow(missing_docs)] pub(crate) enum SyntaxKind { IDENT = 0, // package name COMMA, // , L_PARENS, // ( R_PARENS, // ) L_ANGLE, // < R_ANGLE, // > EQUAL, // = WHITESPACE, // whitespace NEWLINE, // newline ERROR, // as well as errors // composite nodes ROOT, // The entire file RELATION, // An alternative in a dependency VERSION, // A version constraint CONSTRAINT, // (">=", "<=", "=", ">>", "<<") } /// Convert our `SyntaxKind` into the rowan `SyntaxKind`. impl From for rowan::SyntaxKind { fn from(kind: SyntaxKind) -> Self { Self(kind as u16) } } /// A lexer for relations strings. pub(crate) struct Lexer<'a> { input: Peekable>, } impl<'a> Lexer<'a> { /// Create a new lexer for the given input. pub fn new(input: &'a str) -> Self { Lexer { input: input.chars().peekable(), } } fn is_whitespace(c: char) -> bool { c == ' ' || c == '\t' || c == '\r' } fn is_valid_ident_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '-' || c == '.' } fn read_while(&mut self, predicate: F) -> String where F: Fn(char) -> bool, { let mut result = String::new(); while let Some(&c) = self.input.peek() { if predicate(c) { result.push(c); self.input.next(); } else { break; } } result } fn next_token(&mut self) -> Option<(SyntaxKind, String)> { if let Some(&c) = self.input.peek() { match c { ',' => { self.input.next(); Some((SyntaxKind::COMMA, ",".to_owned())) } '(' => { self.input.next(); Some((SyntaxKind::L_PARENS, "(".to_owned())) } ')' => { self.input.next(); Some((SyntaxKind::R_PARENS, ")".to_owned())) } '<' => { self.input.next(); Some((SyntaxKind::L_ANGLE, "<".to_owned())) } '>' => { self.input.next(); Some((SyntaxKind::R_ANGLE, ">".to_owned())) } '=' => { self.input.next(); Some((SyntaxKind::EQUAL, "=".to_owned())) } '\n' => { self.input.next(); Some((SyntaxKind::NEWLINE, "\n".to_owned())) } _ if Self::is_whitespace(c) => { let whitespace = self.read_while(Self::is_whitespace); Some((SyntaxKind::WHITESPACE, whitespace)) } // TODO: separate handling for package names and versions? _ if Self::is_valid_ident_char(c) => { let key = self.read_while(Self::is_valid_ident_char); Some((SyntaxKind::IDENT, key)) } _ => { self.input.next(); Some((SyntaxKind::ERROR, c.to_string())) } } } else { None } } } impl Iterator for Lexer<'_> { type Item = (SyntaxKind, String); fn next(&mut self) -> Option { self.next_token() } } pub(crate) fn lex(input: &str) -> Vec<(SyntaxKind, String)> { let mut lexer = Lexer::new(input); lexer.by_ref().collect::>() } /// A trait for looking up versions of packages. pub trait VersionLookup { /// Look up the version of a package. fn lookup_version<'a>(&'a self, package: &'_ str) -> Option>; } impl VersionLookup for std::collections::HashMap { fn lookup_version<'a>(&'a self, package: &str) -> Option> { self.get(package).map(Cow::Borrowed) } } impl VersionLookup for F where F: Fn(&str) -> Option, { fn lookup_version<'a>(&'a self, name: &str) -> Option> { self(name).map(Cow::Owned) } } impl VersionLookup for (String, Version) { fn lookup_version<'a>(&'a self, name: &str) -> Option> { if name == self.0 { Some(Cow::Borrowed(&self.1)) } else { None } } } r-description-0.3.1/src/version.rs000064400000000000000000000110001046102023000152210ustar 00000000000000//! R Version strings use std::cmp::Ordering; // Struct to represent a version with major, minor, patch, and an optional pre-release tag #[derive(Debug, PartialEq, Eq, std::hash::Hash, Clone)] pub struct Version { components: Vec, pre_release: Option, // Pre-release version like "alpha", "beta", etc. } impl std::fmt::Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Format the version string as "major.minor.patch" or "major.minor.patch-pre_release" f.write_str( &self .components .iter() .map(|c| c.to_string()) .collect::>() .join("."), )?; if let Some(pre_release) = &self.pre_release { f.write_str("-")?; f.write_str(pre_release)?; } Ok(()) } } impl Version { /// Create a new version pub fn new(major: u32, minor: u32, patch: Option, pre_release: Option<&str>) -> Self { Self { components: if let Some(patch) = patch { vec![major, minor, patch] } else { vec![major, minor] }, pre_release: pre_release.map(|s| s.to_string()), } } } impl std::str::FromStr for Version { type Err = String; fn from_str(s: &str) -> Result { // Split the version string by '.' and '-' to get major, minor, patch, and pre-release let mut parts = s.splitn(2, '-'); let version = parts .next() .ok_or(format!("Invalid version string: {}", s))?; let pre_release = parts.next().map(|s| s.to_string()); let components = version .split('.') .map(|part| { part.parse() .map_err(|_| format!("Invalid version component: {}", s)) }) .collect::, _>>()?; Ok(Self { components, pre_release, }) } } impl Ord for Version { fn cmp(&self, other: &Self) -> Ordering { // Compare components in order, and then compare pre-release tags for (a, b) in self.components.iter().zip(other.components.iter()) { match a.cmp(b) { Ordering::Equal => continue, ordering => return ordering, } } if self.components.len() < other.components.len() { Ordering::Less } else if self.components.len() > other.components.len() { Ordering::Greater } else { self.compare_pre_release(other) } } } impl PartialOrd for Version { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Version { fn compare_pre_release(&self, other: &Self) -> Ordering { match (&self.pre_release, &other.pre_release) { (None, None) => Ordering::Equal, (None, Some(_)) => Ordering::Greater, (Some(_), None) => Ordering::Less, (Some(a), Some(b)) => a.cmp(b), } } } #[cfg(test)] mod tests { use super::Version; use std::str::FromStr; #[test] fn test_version_from_str() { use std::str::FromStr; let version = Version::from_str("1.2.3").unwrap(); assert_eq!(version, Version::new(1, 2, Some(3), None)); let version = Version::from_str("1.2.3-alpha").unwrap(); assert_eq!(version, Version::new(1, 2, Some(3), Some("alpha"))); let version = Version::from_str("1.2.3-beta").unwrap(); assert_eq!(version, Version::new(1, 2, Some(3), Some("beta"))); } #[test] fn test_version_cmp() { use std::cmp::Ordering; let v1 = Version::from_str("1.2.3").unwrap(); let v2 = Version::from_str("1.2.3").unwrap(); assert_eq!(v1.cmp(&v2), Ordering::Equal); let v1 = Version::from_str("1.2.3").unwrap(); let v2 = Version::from_str("1.2.4").unwrap(); assert_eq!(v1.cmp(&v2), Ordering::Less); let v1 = Version::from_str("1.2.3").unwrap(); let v2 = Version::from_str("1.2.3-alpha").unwrap(); assert_eq!(v1.cmp(&v2), Ordering::Greater); let v1 = Version::from_str("1.2.3-alpha").unwrap(); let v2 = Version::from_str("1.2.3-beta").unwrap(); assert_eq!(v1.cmp(&v2), Ordering::Less); } #[test] fn test_version_invalid() { assert!(Version::from_str("a").is_err()); assert!(Version::from_str("a1-b").is_err()); } } r-description-0.3.1/testdata/dplyr.desc000064400000000000000000000042531046102023000162160ustar 00000000000000Type: Package Package: dplyr Title: A Grammar of Data Manipulation Version: 1.1.4 Authors@R: c( person("Hadley", "Wickham", , "hadley@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-4757-117X")), person("Romain", "François", role = "aut", comment = c(ORCID = "0000-0002-2444-4226")), person("Lionel", "Henry", role = "aut"), person("Kirill", "Müller", role = "aut", comment = c(ORCID = "0000-0002-1416-3412")), person("Davis", "Vaughan", , "davis@posit.co", role = "aut", comment = c(ORCID = "0000-0003-4777-038X")), person("Posit Software, PBC", role = c("cph", "fnd")) ) Description: A fast, consistent tool for working with data frame like objects, both in memory and out of memory. License: MIT + file LICENSE URL: https://dplyr.tidyverse.org, https://github.com/tidyverse/dplyr BugReports: https://github.com/tidyverse/dplyr/issues Depends: R (>= 3.5.0) Imports: cli (>= 3.4.0), generics, glue (>= 1.3.2), lifecycle (>= 1.0.3), magrittr (>= 1.5), methods, pillar (>= 1.9.0), R6, rlang (>= 1.1.0), tibble (>= 3.2.0), tidyselect (>= 1.2.0), utils, vctrs (>= 0.6.4) Suggests: bench, broom, callr, covr, DBI, dbplyr (>= 2.2.1), ggplot2, knitr, Lahman, lobstr, microbenchmark, nycflights13, purrr, rmarkdown, RMySQL, RPostgreSQL, RSQLite, stringi (>= 1.7.6), testthat (>= 3.1.5), tidyr (>= 1.3.0), withr VignetteBuilder: knitr Config/Needs/website: tidyverse, shiny, pkgdown, tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 LazyData: true RoxygenNote: 7.2.3 NeedsCompilation: yes Packaged: 2023-11-16 21:48:56 UTC; hadleywickham Author: Hadley Wickham [aut, cre] (), Romain François [aut] (), Lionel Henry [aut], Kirill Müller [aut] (), Davis Vaughan [aut] (), Posit Software, PBC [cph, fnd] Maintainer: Hadley Wickham Repository: RSPM Date/Publication: 2023-11-17 16:50:02 UTC Built: R 4.3.0; x86_64-pc-linux-gnu; 2023-11-20 12:40:25 UTC; unix