use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
pub struct RDescriptionParser;
impl ManifestParser for RDescriptionParser {
fn filename(&self) -> &'static str {
"DESCRIPTION"
}
fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
let mut name: Option<String> = None;
let mut version: Option<String> = None;
let mut deps: Vec<DeclaredDep> = Vec::new();
#[derive(Clone, Copy, PartialEq)]
enum Field {
None,
Imports,
Suggests,
Depends,
LinkingTo,
}
let mut current_field = Field::None;
let mut field_value = String::new();
let flush = |field: Field, value: &str, deps: &mut Vec<DeclaredDep>| {
if field == Field::None || value.is_empty() {
return;
}
let kind = match field {
Field::Imports => DepKind::Normal,
Field::Depends => DepKind::Normal,
Field::Suggests => DepKind::Dev,
Field::LinkingTo => DepKind::Build,
Field::None => return,
};
for entry in value.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let dep_entry = parse_r_dep_entry(entry);
if dep_entry.pkg_name.is_empty() || dep_entry.pkg_name == "R" {
continue;
}
deps.push(DeclaredDep {
name: dep_entry.pkg_name,
version_req: dep_entry.version_req,
kind,
});
}
};
for line in content.lines() {
if line.starts_with(' ') || line.starts_with('\t') {
if current_field != Field::None {
if !field_value.is_empty() {
field_value.push(',');
}
field_value.push_str(line.trim());
}
continue;
}
flush(current_field, &field_value, &mut deps);
field_value.clear();
current_field = Field::None;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(colon) = trimmed.find(':') {
let key = trimmed[..colon].trim();
let val = trimmed[colon + 1..].trim();
match key {
"Package" => name = Some(val.to_string()),
"Version" => version = Some(val.to_string()),
"Imports" => {
current_field = Field::Imports;
field_value = val.to_string();
}
"Suggests" => {
current_field = Field::Suggests;
field_value = val.to_string();
}
"Depends" => {
current_field = Field::Depends;
field_value = val.to_string();
}
"LinkingTo" => {
current_field = Field::LinkingTo;
field_value = val.to_string();
}
_ => {}
}
}
}
flush(current_field, &field_value, &mut deps);
Ok(ParsedManifest {
ecosystem: "cran",
name,
version,
dependencies: deps,
})
}
}
struct RDepEntry {
pkg_name: String,
version_req: Option<String>,
}
fn parse_r_dep_entry(s: &str) -> RDepEntry {
if let Some(paren) = s.find('(') {
let pkg_name = s[..paren].trim().to_string();
let rest = s[paren + 1..].trim();
let ver = rest.trim_end_matches(')').trim().to_string();
let version_req = if ver.is_empty() { None } else { Some(ver) };
RDepEntry {
pkg_name,
version_req,
}
} else {
RDepEntry {
pkg_name: s.trim().to_string(),
version_req: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ManifestParser;
#[test]
fn test_parse_r_description() {
let content = r#"Package: mypackage
Title: My R Package
Version: 1.0.0
Authors@R: person("Alice", "Smith", role = c("aut", "cre"))
Description: Does things.
Imports:
dplyr (>= 1.0.0),
ggplot2,
stringr (>= 1.4.0)
Suggests:
testthat (>= 3.0.0),
knitr,
rmarkdown
Depends:
R (>= 4.0.0)
LinkingTo:
Rcpp
License: MIT
"#;
let m = RDescriptionParser.parse(content).unwrap();
assert_eq!(m.ecosystem, "cran");
assert_eq!(m.name.as_deref(), Some("mypackage"));
assert_eq!(m.version.as_deref(), Some("1.0.0"));
let dplyr = m.dependencies.iter().find(|d| d.name == "dplyr").unwrap();
assert_eq!(dplyr.kind, DepKind::Normal);
assert_eq!(dplyr.version_req.as_deref(), Some(">= 1.0.0"));
let ggplot = m.dependencies.iter().find(|d| d.name == "ggplot2").unwrap();
assert_eq!(ggplot.kind, DepKind::Normal);
assert!(ggplot.version_req.is_none());
let testthat = m
.dependencies
.iter()
.find(|d| d.name == "testthat")
.unwrap();
assert_eq!(testthat.kind, DepKind::Dev);
assert_eq!(testthat.version_req.as_deref(), Some(">= 3.0.0"));
let rcpp = m.dependencies.iter().find(|d| d.name == "Rcpp").unwrap();
assert_eq!(rcpp.kind, DepKind::Build);
assert!(!m.dependencies.iter().any(|d| d.name == "R"));
}
#[test]
fn test_inline_imports() {
let content = "Package: tiny\nVersion: 0.0.1\nImports: data.table, jsonlite\n";
let m = RDescriptionParser.parse(content).unwrap();
assert_eq!(m.dependencies.len(), 2);
assert!(m.dependencies.iter().any(|d| d.name == "data.table"));
assert!(m.dependencies.iter().any(|d| d.name == "jsonlite"));
}
}