use super::{
ValueKindName, ViewError, decode_optional, decode_required, duplicate, expect_list, missing,
named_values, unexpected_length, unexpected_type,
};
use crate::{RObject, RValue};
use std::{collections::BTreeMap, fmt};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageMeta {
description: BTreeMap<String, Option<String>>,
built: Option<Built>,
}
impl PackageMeta {
pub fn from_object(object: &RObject) -> Result<Self, ViewError> {
let items = expect_list(object, "PackageMeta", None)?;
require_class(object, "PackageMeta", None, "packageDescription2")?;
let names = named_values(object, "PackageMeta", None)?;
if names.len() != items.len() {
return Err(unexpected_length(
"PackageMeta",
None,
items.len().to_string(),
names.len(),
));
}
let mut positions = BTreeMap::new();
for (index, name) in names.iter().enumerate() {
let name = decode_required(name, &format!("PackageMeta[{index}]"), None)?;
if positions.insert(name.clone(), index).is_some() {
return Err(duplicate(&format!("PackageMeta.{name}"), Some(name)));
}
}
let description_index = positions
.get("DESCRIPTION")
.copied()
.ok_or_else(|| missing("PackageMeta.DESCRIPTION", Some("DESCRIPTION".to_owned())))?;
let description = parse_description(
&items[description_index],
"PackageMeta.DESCRIPTION",
Some("DESCRIPTION"),
)?;
let built = positions
.get("Built")
.copied()
.map(|index| parse_built(&items[index], "PackageMeta.Built"))
.transpose()?;
Ok(Self { description, built })
}
pub fn built(&self) -> Option<&Built> {
self.built.as_ref()
}
pub fn description(&self) -> &BTreeMap<String, Option<String>> {
&self.description
}
pub fn description_field(&self, name: &str) -> Option<Option<&str>> {
self.description.get(name).map(|value| value.as_deref())
}
}
impl TryFrom<&RObject> for PackageMeta {
type Error = ViewError;
fn try_from(value: &RObject) -> Result<Self, Self::Error> {
Self::from_object(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Built {
r_version: PackageVersion,
platform: Option<String>,
date: Option<String>,
os_type: Option<String>,
}
impl Built {
pub fn r_version(&self) -> &PackageVersion {
&self.r_version
}
pub fn platform(&self) -> Option<&str> {
self.platform.as_deref()
}
pub fn date(&self) -> Option<&str> {
self.date.as_deref()
}
pub fn os_type(&self) -> Option<&str> {
self.os_type.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageVersion {
pub(super) components: Vec<u32>,
}
impl PackageVersion {
pub fn components(&self) -> &[u32] {
&self.components
}
}
impl fmt::Display for PackageVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut components = self.components.iter();
if let Some(first) = components.next() {
write!(formatter, "{first}")?;
for component in components {
write!(formatter, ".{component}")?;
}
}
Ok(())
}
}
fn parse_description(
object: &RObject,
path: &str,
field: Option<&str>,
) -> Result<BTreeMap<String, Option<String>>, ViewError> {
let values = match &object.value() {
RValue::Character(values) => values,
value => {
return Err(unexpected_type(
path,
field,
"character vector",
value.kind_name(),
));
}
};
let names = named_values(object, path, field)?;
if names.len() != values.len() {
return Err(unexpected_length(
path,
field,
values.len().to_string(),
names.len(),
));
}
let mut description = BTreeMap::new();
for (index, (name, value)) in names.iter().zip(values).enumerate() {
let name = decode_required(name, &format!("{path}[{index}]"), field)?;
if description.contains_key(&name) {
return Err(duplicate(&format!("{path}[\"{name}\"]"), Some(name)));
}
let value = decode_optional(value, &format!("{path}[\"{name}\"]"), Some(&name))?;
description.insert(name, value);
}
Ok(description)
}
fn parse_built(object: &RObject, path: &str) -> Result<Built, ViewError> {
let items = expect_list(object, path, Some("Built"))?;
let names = named_values(object, path, Some("Built"))?;
if names.len() != items.len() {
return Err(unexpected_length(
path,
Some("Built"),
items.len().to_string(),
names.len(),
));
}
let mut positions = BTreeMap::new();
for (index, name) in names.iter().enumerate() {
let name = decode_required(name, &format!("{path}[{index}]"), Some("Built"))?;
if positions.insert(name.clone(), index).is_some() {
return Err(duplicate(&format!("{path}.{name}"), Some(name)));
}
}
let r_index = positions
.get("R")
.copied()
.ok_or_else(|| missing(format!("{path}.R"), Some("R".to_owned())))?;
let r_version = parse_version(&items[r_index], &format!("{path}.R"))?;
let platform = optional_built_string(&positions, items, "Platform", path)?;
let date = optional_built_string(&positions, items, "Date", path)?;
let os_type = optional_built_string(&positions, items, "OStype", path)?;
Ok(Built {
r_version,
platform,
date,
os_type,
})
}
fn parse_version(object: &RObject, path: &str) -> Result<PackageVersion, ViewError> {
let valid_class = match object.attributes().get("class") {
Some(attribute) => match &attribute.value() {
RValue::Character(values) => {
let mut has_package = false;
let mut has_numeric = false;
for value in values {
match value.as_str() {
Some(Ok(value)) if value == "package_version" => has_package = true,
Some(Ok(value)) if value == "numeric_version" => has_numeric = true,
Some(Err(_)) => {
return Err(ViewError::InvalidStringEncoding {
path: format!("{path}.class"),
field: Some("R".to_owned()),
row: None,
column: None,
});
}
_ => {}
}
}
has_package && has_numeric
}
_ => false,
},
None => false,
};
if !valid_class {
return Err(invalid_version(
path,
"missing package_version/numeric_version class",
));
}
let RValue::List(values) = &object.value() else {
return Err(invalid_version(path, "expected a length-one list"));
};
if values.len() != 1 {
return Err(invalid_version(path, "expected a length-one list"));
}
let RValue::Integer(components) = &values[0].value() else {
return Err(invalid_version(path, "expected an integer vector"));
};
if components.is_empty() {
return Err(invalid_version(
path,
"version components must not be empty",
));
}
let mut owned = Vec::with_capacity(components.len());
for (index, component) in components.iter().enumerate() {
let Some(component) = component else {
return Err(invalid_version(
&format!("{path}[0][{index}]"),
"component is NA",
));
};
if *component < 0 {
return Err(invalid_version(
&format!("{path}[0][{index}]"),
"component is negative",
));
}
owned.push(*component as u32);
}
Ok(PackageVersion { components: owned })
}
fn optional_built_string(
positions: &BTreeMap<String, usize>,
items: &[RObject],
name: &str,
path: &str,
) -> Result<Option<String>, ViewError> {
match positions.get(name) {
Some(index) => {
decode_character_scalar(&items[*index], &format!("{path}.{name}"), Some(name))
}
None => Ok(None),
}
}
fn require_class(
object: &RObject,
path: &str,
field: Option<&str>,
expected: &str,
) -> Result<(), ViewError> {
let Some(attribute) = object.attributes().get("class") else {
return Err(missing(format!("{path}.class"), field.map(str::to_owned)));
};
let RValue::Character(values) = &attribute.value() else {
return Err(unexpected_type(
&format!("{path}.class"),
field,
"character vector",
attribute.value().kind_name(),
));
};
for value in values {
match value.as_str() {
Some(Ok(value)) if value == expected => return Ok(()),
Some(Err(_)) => {
return Err(ViewError::InvalidStringEncoding {
path: format!("{path}.class"),
field: field.map(str::to_owned),
row: None,
column: None,
});
}
_ => {}
}
}
Err(unexpected_type(
path,
field,
"expected class",
"different class",
))
}
fn decode_character_scalar(
object: &RObject,
path: &str,
field: Option<&str>,
) -> Result<Option<String>, ViewError> {
let RValue::Character(values) = &object.value() else {
return Err(unexpected_type(
path,
field,
"character scalar",
object.value().kind_name(),
));
};
if values.len() != 1 {
return Err(unexpected_length(path, field, "1".to_owned(), values.len()));
}
decode_optional(&values[0], path, field)
}
fn invalid_version(path: &str, reason: &str) -> ViewError {
ViewError::InvalidPackageVersion {
path: path.to_owned(),
field: Some("R".to_owned()),
reason: reason.to_owned(),
}
}