cargo-parcel 0.0.4

Experimental extended cargo installer
Documentation
use anyhow::anyhow;

fn cargo_toml_error(message: &str) -> anyhow::Error {
    anyhow!("could not read Cargo.toml: {}", message)
}

/// The configuration, as parsed from `Cargo.toml`.
#[derive(Debug)]
pub struct Config {
    package_name: String,
    package_version: String,
    metadata: Metadata,
}

impl Config {
    /// Constructs the config from a `Cargo.toml` manifest.
    pub fn from_manifest(manifest: &toml::value::Table) -> anyhow::Result<Self> {
        let malformed_package = |e| anyhow!("malformed package table: {}", e);
        let package = get_key(manifest, "package", toml_table)?;
        let package_name = get_key(package, "name", toml_str).map_err(malformed_package)?;
        let package_version = get_key(package, "version", toml_str).map_err(malformed_package)?;
        let parcel = match get_key_opt(package, "metadata", toml_table)? {
            Some(m) => get_key_opt(m, "parcel", toml_table)?,
            None => None,
        };
        let metadata = if let Some(metadata) = parcel {
            Metadata::from_toml(metadata)
                .map_err(|e| anyhow!("malformed parcel metadata: {}", e))?
        } else {
            Default::default()
        };
        Ok(Config {
            package_name: package_name.into(),
            package_version: package_version.into(),
            metadata,
        })
    }

    /// Returns the package name.
    ///
    /// This will return the `pkg-name` metadata entry. If that key is missing,
    /// the crate's name is returned.
    pub fn package_name(&self) -> &str {
        self.metadata
            .pkg_name
            .as_ref()
            .map(|s| s.as_str())
            .unwrap_or(&self.package_name)
    }

    /// Returns the package version string.
    pub fn package_version(&self) -> &str {
        &self.package_version
    }

    /// Returns the cargo binaries.
    pub fn cargo_binaries(&self) -> impl Iterator<Item = &str> {
        self.metadata.cargo_binaries.iter().map(|s| s.as_ref())
    }

    /// Returns the man pages.
    pub fn man_pages(&self) -> impl Iterator<Item = &str> {
        self.metadata.man_pages.iter().map(|s| s.as_ref())
    }

    /// Returns the package data resources.
    pub fn pkg_data(&self) -> impl Iterator<Item = &str> {
        self.metadata.pkg_data.iter().map(|s| s.as_ref())
    }
}

fn toml_str_array(value: &toml::Value) -> anyhow::Result<Vec<String>> {
    let array = value
        .as_array()
        .ok_or_else(|| anyhow!("must be an array"))?;
    array
        .iter()
        .map(|item| {
            item.as_str()
                .map(ToOwned::to_owned)
                .ok_or_else(|| anyhow!("contains non-string item `{}`", item))
        })
        .collect()
}

#[derive(Debug, Default)]
struct Metadata {
    pkg_name: Option<String>,
    cargo_binaries: Vec<String>,
    man_pages: Vec<String>,
    pkg_data: Vec<String>,
}

impl Metadata {
    fn from_toml(metadata: &toml::value::Table) -> anyhow::Result<Self> {
        Ok(Metadata {
            pkg_name: get_key_opt(metadata, "pkg-name", toml_str)?.map(|s| s.to_owned()),
            cargo_binaries: get_key_opt(metadata, "cargo-binaries", toml_str_array)?
                .unwrap_or_default(),
            man_pages: get_key_opt(metadata, "man-pages", toml_str_array)?.unwrap_or_default(),
            pkg_data: get_key_opt(metadata, "pkg-data", toml_str_array)?.unwrap_or_default(),
        })
    }
}

fn toml_table(value: &toml::Value) -> anyhow::Result<&toml::value::Table> {
    value.as_table().ok_or_else(|| anyhow!("must be a table"))
}

fn toml_str(value: &toml::Value) -> anyhow::Result<&str> {
    value.as_str().ok_or_else(|| anyhow!("must be a string"))
}

fn get_key<'a, F, T>(table: &'a toml::value::Table, name: &str, f: F) -> anyhow::Result<T>
where
    F: FnOnce(&'a toml::Value) -> anyhow::Result<T>,
{
    get_key_opt(table, name, f)?.ok_or_else(|| cargo_toml_error("required key `{}` missing"))
}

fn get_key_opt<'a, F, T>(
    table: &'a toml::value::Table,
    name: &str,
    f: F,
) -> anyhow::Result<Option<T>>
where
    F: FnOnce(&'a toml::Value) -> anyhow::Result<T>,
{
    table
        .get(name)
        .map(|v| f(v).map_err(|e| anyhow!("invalid value for key `{}`: {}", name, e)))
        .transpose()
}