use std::{convert::Infallible, fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use strum::{Display, EnumString};
use crate::{Error, Name};
#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize)]
pub enum PackageType {
#[strum(to_string = "debug")]
Debug,
#[strum(to_string = "pkg")]
Package,
#[strum(to_string = "src")]
Source,
#[strum(to_string = "split")]
Split,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PackageDescription(String);
impl PackageDescription {
pub fn new(description: &str) -> Self {
Self::from(description)
}
}
impl Default for PackageDescription {
fn default() -> Self {
Self::new("")
}
}
impl FromStr for PackageDescription {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl AsRef<str> for PackageDescription {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for PackageDescription {
fn from(value: &str) -> Self {
let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
let mut previous = ' ';
description.retain(|ch| {
if ch == ' ' && previous == ' ' {
return false;
};
previous = ch;
true
});
Self(description)
}
}
impl Display for PackageDescription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub type PackageBaseName = Name;
#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
pub struct ExtraDataEntry {
key: String,
value: String,
}
impl ExtraDataEntry {
pub fn new(key: String, value: String) -> Self {
Self { key, value }
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &str {
&self.value
}
}
impl FromStr for ExtraDataEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const DELIMITER: char = '=';
let mut parts = s.splitn(2, DELIMITER);
let key = parts
.next()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.ok_or(Error::MissingComponent { component: "key" })?;
let value = parts
.next()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.ok_or(Error::MissingComponent { component: "value" })?;
Ok(Self::new(key.to_string(), value.to_string()))
}
}
impl Display for ExtraDataEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}", self.key, self.value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ExtraData(Vec<ExtraDataEntry>);
impl ExtraData {
pub fn pkg_type(&self) -> PackageType {
self.0
.iter()
.find(|v| v.key() == "pkgtype")
.map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
.unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
type Error = Error;
fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
let _ = PackageType::from_str(pkg_type.value())?;
Ok(Self(value))
} else {
Err(Error::MissingComponent {
component: "extra_data with a valid \"pkgtype\" entry",
})
}
}
}
impl TryFrom<ExtraDataEntry> for ExtraData {
type Error = Error;
fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
Self::try_from(vec![value])
}
}
impl From<ExtraData> for Vec<ExtraDataEntry> {
fn from(value: ExtraData) -> Self {
value.0
}
}
impl IntoIterator for ExtraData {
type Item = ExtraDataEntry;
type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl AsRef<[ExtraDataEntry]> for ExtraData {
fn as_ref(&self) -> &[ExtraDataEntry] {
&self.0
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case("debug", Ok(PackageType::Debug))]
#[case("pkg", Ok(PackageType::Package))]
#[case("src", Ok(PackageType::Source))]
#[case("split", Ok(PackageType::Split))]
#[case("foo", Err(strum::ParseError::VariantNotFound))]
fn pkgtype_from_string(
#[case] from_str: &str,
#[case] result: Result<PackageType, strum::ParseError>,
) {
assert_eq!(PackageType::from_str(from_str), result);
}
#[rstest]
#[case(PackageType::Debug, "debug")]
#[case(PackageType::Package, "pkg")]
#[case(PackageType::Source, "src")]
#[case(PackageType::Split, "split")]
fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
assert_eq!(pkgtype_str, format!("{pkgtype}"));
}
#[rstest]
#[case("key=value", "key", "value")]
#[case("pkgtype=debug", "pkgtype", "debug")]
#[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
fn extra_data_entry_from_str(
#[case] data: &str,
#[case] key: &str,
#[case] value: &str,
) -> TestResult {
let extra_data = ExtraDataEntry::from_str(data)?;
assert_eq!(extra_data.key(), key);
assert_eq!(extra_data.value(), value);
assert_eq!(extra_data.to_string(), data);
Ok(())
}
#[rstest]
#[case("key", Err(Error::MissingComponent { component: "value" }))]
#[case("key=", Err(Error::MissingComponent { component: "value" }))]
#[case("=value", Err(Error::MissingComponent { component: "key" }))]
fn extra_data_entry_from_str_error(
#[case] extra_data: &str,
#[case] result: Result<ExtraDataEntry, Error>,
) {
assert_eq!(ExtraDataEntry::from_str(extra_data), result);
}
#[rstest]
#[case::empty_list(vec![])]
#[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
assert!(ExtraData::try_from(xdata).is_err());
Ok(())
}
#[rstest]
#[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
#[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
let xdata = ExtraData::try_from(xdata)?;
assert_eq!(xdata.pkg_type(), PackageType::Package);
Ok(())
}
#[rstest]
#[case(" trailing ", "trailing")]
#[case("in between words", "in between words")]
#[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
#[case(" \neverything\t combined\n yeah \n ", "everything combined yeah")]
fn package_description(#[case] input: &str, #[case] result: &str) {
assert_eq!(PackageDescription::new(input).to_string(), result);
}
}