pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::manifest::{ManifestPackage, ManifestSource};
use crate::{PrayError, PrayResult};
use std::collections::BTreeMap;

pub(crate) fn implied_source_name(
    declaration: &ManifestPackage,
    sources: &BTreeMap<String, ManifestSource>,
) -> PrayResult<Option<String>> {
    if let Some(name) = &declaration.source {
        return Ok(Some(name.clone()));
    }
    if let Some(namespace) = declaration.name.split_once('/').map(|(name, _)| name) {
        if sources.contains_key(namespace) {
            return Ok(Some(namespace.to_string()));
        }
    }
    if !declaration.name.contains('/') {
        let path_sources: Vec<&String> = sources
            .iter()
            .filter(|(_, source)| source.kind == "path")
            .map(|(name, _)| name)
            .collect();
        if path_sources.len() == 1 {
            return Ok(Some(path_sources[0].clone()));
        }
    }
    match sources.len() {
        0 => Ok(None),
        1 => Ok(sources.keys().next().cloned()),
        _ => Err(PrayError::Resolution(format!(
            "package {} requires source: when multiple sources are declared and the package namespace does not match a source",
            declaration.name
        ))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn source(name: &str, kind: &str, url: &str) -> ManifestSource {
        ManifestSource {
            name: name.to_string(),
            kind: kind.to_string(),
            url: url.to_string(),
            subdir: None,
            rev: None,
            tag: None,
        }
    }

    fn package(name: &str) -> ManifestPackage {
        ManifestPackage {
            name: name.to_string(),
            constraint: "*".to_string(),
            ..Default::default()
        }
    }

    fn sources(entries: &[ManifestSource]) -> BTreeMap<String, ManifestSource> {
        entries
            .iter()
            .cloned()
            .map(|entry| (entry.name.clone(), entry))
            .collect()
    }

    #[test]
    fn unqualified_name_uses_the_unique_path_source() {
        let catalog = sources(&[
            source("amkisko", "git", "git+https://example.com/prayers.git"),
            source("local", "path", "prayers"),
        ]);
        let name = implied_source_name(&package("project"), &catalog).expect("source");
        assert_eq!(name.as_deref(), Some("local"));
    }

    #[test]
    fn namespace_still_matches_a_git_source() {
        let catalog = sources(&[
            source("amkisko", "git", "git+https://example.com/prayers.git"),
            source("local", "path", "prayers"),
        ]);
        let name = implied_source_name(&package("amkisko/rules"), &catalog).expect("source");
        assert_eq!(name.as_deref(), Some("amkisko"));
    }

    #[test]
    fn several_path_sources_still_require_source() {
        let catalog = sources(&[
            source("local", "path", "prayers"),
            source("vendor", "path", "vendor"),
        ]);
        let error = implied_source_name(&package("project"), &catalog).expect_err("source");
        assert!(error.to_string().contains("requires source:"));
    }

    #[test]
    fn namespaced_name_selects_a_path_source_when_several_exist() {
        let catalog = sources(&[
            source("local", "path", "prayers"),
            source("vendor", "path", "vendor"),
        ]);
        let name = implied_source_name(&package("local/project"), &catalog).expect("source");
        assert_eq!(name.as_deref(), Some("local"));
    }
}