r-description-0.3.7/.cargo_vcs_info.json0000644000000001360000000000100136250ustar { "git": { "sha1": "21dbe8e4f9250679cc9facd47ef3c0a88a02c862" }, "path_in_vcs": "" }r-description-0.3.7/.codespellrc000064400000000000000000000000461046102023000147150ustar 00000000000000[codespell] ignore-words-list = crate r-description-0.3.7/.github/CODEOWNERS000064400000000000000000000000121046102023000153410ustar 00000000000000* @jelmer r-description-0.3.7/.github/FUNDING.yml000064400000000000000000000000171046102023000155700ustar 00000000000000github: jelmer r-description-0.3.7/.github/dependabot.yml000064400000000000000000000006251046102023000166100ustar 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.7/.github/workflows/disperse.yml000064400000000000000000000002741046102023000203560ustar 00000000000000--- name: Disperse configuration "on": - push jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: jelmer/action-disperse-validate@v2 r-description-0.3.7/.github/workflows/rust.yml000064400000000000000000000015241046102023000175340ustar 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@v6 - name: Install all-features run: cargo install cargo-all-features - name: Build run: cargo build-all-features -- --workspace env: RUSTFLAGS: -Dwarnings - name: Run tests run: cargo test-all-features -- --workspace env: RUSTFLAGS: -Dwarnings minimal-versions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Install cargo-minimal-versions run: cargo install cargo-minimal-versions - name: Install cargo-hack run: cargo install cargo-hack - name: Test with minimal versions run: cargo minimal-versions --direct test r-description-0.3.7/.gitignore000064400000000000000000000000131046102023000143770ustar 00000000000000/target *~ r-description-0.3.7/CODE_OF_CONDUCT.md000064400000000000000000000125451046102023000152230ustar 00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations r-description-0.3.7/Cargo.lock0000644000000300330000000000100115770ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "countme" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" [[package]] name = "deb822-derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bf2d0fa4ce2457e94bd7efb15aeadc115297f04b660bd0da706729e0d91442" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "deb822-fast" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f410ccb5cbd9b81d56b290131bad4350ecf8b46416fb901e759dc1e6916a8198" dependencies = [ "deb822-derive", ] [[package]] name = "deb822-lossless" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdcadf12851ddb37dc938e724beeb50e83bfe1a1fda3c15b997dc1105ec49e3d" dependencies = [ "regex", "rowan", "serde", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-description" version = "0.3.7" dependencies = [ "deb822-derive", "deb822-fast", "deb822-lossless", "rowan", "serde", "serde_json", "url", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rowan" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" dependencies = [ "countme", "hashbrown", "rustc-hash", "text-size", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] r-description-0.3.7/Cargo.toml0000644000000026030000000000100116240ustar # 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.7" 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" [features] serde = ["dep:serde"] [lib] name = "r_description" path = "src/lib.rs" [dependencies.deb822-derive] version = "0.3.0" [dependencies.deb822-fast] version = "0.2.2" features = ["derive"] [dependencies.deb822-lossless] version = ">=0.3, <0.6" [dependencies.rowan] version = "0.16" [dependencies.serde] version = "1" optional = true [dependencies.url] version = "2" [dev-dependencies.serde_json] version = "1" r-description-0.3.7/Cargo.toml.orig000064400000000000000000000012151046102023000153030ustar 00000000000000[package] name = "r-description" description = "Parsing and editor for R DESCRIPTION files" edition = "2021" version = "0.3.7" 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.3, <0.6" } deb822-fast = { version = "0.2.2", features = ["derive"] } deb822-derive = "0.3.0" rowan = "0.16" url = "2" serde = { version = "1", optional = true } [features] serde = ["dep:serde"] [dev-dependencies] serde_json = "1" r-description-0.3.7/README.md000064400000000000000000000024241046102023000136760ustar 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.7/disperse.toml000064400000000000000000000000531046102023000151260ustar 00000000000000tag-name = "v$VERSION" release-timeout = 5 r-description-0.3.7/src/lib.rs000064400000000000000000000014321046102023000143200ustar 00000000000000#![deny(missing_docs)] #![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.7/src/lossless.rs000064400000000000000000001607111046102023000154270ustar 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}; /// R DESCRIPTION file 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)] /// Error type for parsing DESCRIPTION files pub enum Error { /// I/O error Io(std::io::Error), /// Parse 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 { /// Create a new empty R DESCRIPTION file pub fn new() -> Self { Self(Paragraph::new()) } /// Return the package name pub fn package(&self) -> Option { self.0.get("Package") } /// Set the package name 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") } /// Return the maintainer of the package pub fn maintainer(&self) -> Option { self.0.get("Maintainer") } /// Set the maintainer of the package pub fn set_maintainer(&mut self, maintainer: &str) { self.0.insert("Maintainer", maintainer); } /// Return the authors of the package pub fn authors(&self) -> Option { self.0.get("Authors@R").map(|s| s.parse().unwrap()) } /// Set the authors of the package pub fn set_authors(&mut self, authors: &RCode) { self.0.insert("Authors@R", &authors.to_string()); } /// Set the title of the package pub fn set_title(&mut self, title: &str) { self.0.insert("Title", title); } /// Return the description of the package pub fn description(&self) -> Option { self.0.get("Description") } /// Set the description of the package pub fn set_description(&mut self, description: &str) { self.0.insert("Description", description); } /// Return the version of the package pub fn version(&self) -> Option { self.0.get("Version") } /// Set the version of the package pub fn set_version(&mut self, version: &str) { self.0.insert("Version", version); } /// Return the encoding of the description file pub fn encoding(&self) -> Option { self.0.get("Encoding") } /// Set the encoding of the description file pub fn set_encoding(&mut self, encoding: &str) { self.0.insert("Encoding", encoding); } /// Return the license of the package pub fn license(&self) -> Option { self.0.get("License") } /// Set the license of the package pub fn set_license(&mut self, license: &str) { self.0.insert("License", license); } /// Return the roxygen note pub fn roxygen_note(&self) -> Option { self.0.get("RoxygenNote") } /// Set the roxygen note pub fn set_roxygen_note(&mut self, roxygen_note: &str) { self.0.insert("RoxygenNote", roxygen_note); } /// Return the roxygen version pub fn roxygen(&self) -> Option { self.0.get("Roxygen") } /// Set the roxygen version pub fn set_roxygen(&mut self, roxygen: &str) { self.0.insert("Roxygen", roxygen); } /// Return the URL field pub fn url(&self) -> Option { // TODO: parse list of URLs, separated by commas self.0.get("URL") } /// Set the URL field pub fn set_url(&mut self, url: &str) { // TODO: parse list of URLs, separated by commas self.0.insert("URL", url); } /// Return the bug reports URL pub fn bug_reports(&self) -> Option { self.0 .get("BugReports") .map(|s| url::Url::parse(s.as_str()).unwrap()) } /// Set the bug reports URL pub fn set_bug_reports(&mut self, bug_reports: &url::Url) { self.0.insert("BugReports", bug_reports.as_str()); } /// Return the imports field pub fn imports(&self) -> Option> { self.0 .get("Imports") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } /// Set the imports field pub fn set_imports(&mut self, imports: &[&str]) { self.0.insert("Imports", &imports.join(", ")); } /// Return the suggests field pub fn suggests(&self) -> Option { self.0.get("Suggests").map(|s| s.parse().unwrap()) } /// Set the suggests field pub fn set_suggests(&mut self, suggests: Relations) { self.0.insert("Suggests", &suggests.to_string()); } /// Return the depends field pub fn depends(&self) -> Option { self.0.get("Depends").map(|s| s.parse().unwrap()) } /// Set the depends field pub fn set_depends(&mut self, depends: Relations) { self.0.insert("Depends", &depends.to_string()); } /// Return the linking-to field pub fn linking_to(&self) -> Option> { self.0 .get("LinkingTo") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } /// Set the linking-to field pub fn set_linking_to(&mut self, linking_to: &[&str]) { self.0.insert("LinkingTo", &linking_to.join(", ")); } /// Return the lazy data field pub fn lazy_data(&self) -> Option { self.0.get("LazyData").map(|s| s == "true") } /// Set the lazy data field pub fn set_lazy_data(&mut self, lazy_data: bool) { self.0 .insert("LazyData", if lazy_data { "true" } else { "false" }); } /// Return the collate field pub fn collate(&self) -> Option { self.0.get("Collate") } /// Set the collate field pub fn set_collate(&mut self, collate: &str) { self.0.insert("Collate", collate); } /// Return the vignette builder field pub fn vignette_builder(&self) -> Option> { self.0 .get("VignetteBuilder") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } /// Set the vignette builder field pub fn set_vignette_builder(&mut self, vignette_builder: &[&str]) { self.0 .insert("VignetteBuilder", &vignette_builder.join(", ")); } /// Return the system requirements field pub fn system_requirements(&self) -> Option> { self.0 .get("SystemRequirements") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()) } /// Set the system requirements field pub fn set_system_requirements(&mut self, system_requirements: &[&str]) { self.0 .insert("SystemRequirements", &system_requirements.join(", ")); } /// Return the date field pub fn date(&self) -> Option { self.0.get("Date") } /// Set the date field 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(); 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(); 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 { Some(self.cmp(other)) } } impl Eq for Relation {} impl Ord for Relation { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // Compare by name first, then by version let name_cmp = self.name().cmp(&other.name()); if name_cmp != std::cmp::Ordering::Equal { return 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 vc_cmp; } self_version.cmp(&other_version) } (Some(_), None) => std::cmp::Ordering::Greater, (None, Some(_)) => std::cmp::Ordering::Less, (None, None) => std::cmp::Ordering::Equal, } } } 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.7/src/lossy.rs000064400000000000000000000524271046102023000147350ustar 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_derive::{FromDeb822, ToDeb822}; use deb822_fast::{FromDeb822Paragraph, 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)] /// A URL entry in the URL field. pub struct UrlEntry { /// URL pub url: url::Url, /// Optional label for the 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)] /// A DESCRIPTION file. pub struct RDescription { /// The name of the package. #[deb822(field = "Package")] pub name: String, /// A short description of the package. #[deb822(field = "Description")] pub description: String, #[deb822(field = "Title")] /// The title of the package. pub title: String, #[deb822(field = "Maintainer")] /// The maintainer of the package. 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")] /// The version of the package. 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")] /// The license of the package. 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 /// URLs related to the package. pub url: Option>, #[deb822(field = "BugReports")] /// The URL or email address where bug reports should be sent. pub bug_reports: Option, #[deb822(field = "Imports")] /// The packages that this package depends on. pub imports: Option, #[deb822(field = "Suggests")] /// The packages that this package suggests. pub suggests: Option, #[deb822(field = "Depends")] /// The packages that this package depends on. pub depends: Option, #[deb822(field = "LinkingTo")] /// The packages that this package links to. pub linking_to: Option, #[deb822(field = "LazyData")] /// Whether the package has lazy data. pub lazy_data: Option, #[deb822(field = "Collate")] /// The order in which R scripts are loaded. pub collate: Option, #[deb822(field = "VignetteBuilder")] /// The package used to build vignettes. pub vignette_builder: Option, #[deb822(field = "SystemRequirements")] /// The system requirements for the package. 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_fast::Paragraph::from_str(s).map_err(|e| 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_fast::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.7/src/relations.rs000064400000000000000000000140441046102023000155550ustar 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, clippy::upper_case_acronyms)] #[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.7/src/version.rs000064400000000000000000000110561046102023000152420ustar 00000000000000//! R Version strings use std::cmp::Ordering; #[derive(Debug, PartialEq, Eq, std::hash::Hash, Clone)] /// Represents a version string like "1.2.3" or "1.2.3-alpha" pub struct Version { /// Version components like [1, 2, 3] pub components: Vec, /// Pre-release version like "alpha", "beta", etc. pub 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.7/testdata/dplyr.desc000064400000000000000000000042531046102023000162240ustar 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