pray-core 1.13.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::lockfile::read_lockfile;
use crate::PrayError;
use std::path::Path;

pub fn resolution_may_benefit_from_git_source_refresh(message: &str) -> bool {
    message.contains("no registry version")
        || message.contains("not found in distribution")
        || message.contains("not found in git source")
        || message.contains("v1/packages/")
}

pub fn annotate_missing_git_catalog(
    error: PrayError,
    package_name: &str,
    source_name: &str,
    revision: &str,
) -> PrayError {
    match error {
        PrayError::Resolution(message)
            if !revision.is_empty()
                && resolution_may_benefit_from_git_source_refresh(&message)
                && !message.contains("not found in git source") =>
        {
            PrayError::Resolution(format!(
                "package {package_name} was not found in git source {source_name} at revision {revision}. \
                 Run `pray update` to advance the source pin."
            ))
        }
        other => other,
    }
}

pub fn annotate_failed_git_refresh(lockfile_path: &Path, error: PrayError) -> PrayError {
    match error {
        PrayError::Resolution(message)
            if resolution_may_benefit_from_git_source_refresh(&message) =>
        {
            match locked_git_revision_guidance(lockfile_path) {
                Some(pins) => {
                    let package =
                        package_name_from_catalog_miss(&message).unwrap_or("the declared package");
                    PrayError::Resolution(format!(
                        "package {package} was not found in locked git source ({pins}). \
                         Run `pray update` to advance the source pin."
                    ))
                }
                None => PrayError::Resolution(message),
            }
        }
        other => other,
    }
}

fn locked_git_revision_guidance(lockfile_path: &Path) -> Option<String> {
    let lockfile = read_lockfile(lockfile_path).ok()?;
    let pins: Vec<String> = lockfile
        .source
        .iter()
        .filter(|source| source.kind == "git")
        .filter_map(|source| {
            source
                .revision
                .as_ref()
                .map(|revision| format!("{} at {revision}", source.name))
        })
        .collect();
    if pins.is_empty() {
        None
    } else {
        Some(pins.join(", "))
    }
}

fn package_name_from_catalog_miss(message: &str) -> Option<&str> {
    let rest = message.strip_prefix("package ")?;
    let end = rest
        .find(" was not found")
        .or_else(|| rest.find(" not found"))?;
    let name = rest.get(..end)?.trim();
    if name.is_empty() {
        None
    } else {
        Some(name)
    }
}

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

    #[test]
    fn matches_catalog_miss_messages() {
        assert!(resolution_may_benefit_from_git_source_refresh(
            "no registry version for sample/base satisfies ~> 2.0"
        ));
        assert!(resolution_may_benefit_from_git_source_refresh(
            "package sample/extra not found in distribution /tmp/prayers. Missing /tmp/prayers/v1/packages/sample/extra.json. Check the package name."
        ));
        assert!(resolution_may_benefit_from_git_source_refresh(
            "package sample/extra was not found in git source dist at revision abc. Run `pray update` to advance the source pin."
        ));
        assert!(!resolution_may_benefit_from_git_source_refresh(
            "failed to read package metadata"
        ));
    }

    #[test]
    fn annotates_git_catalog_miss_with_update_guidance() {
        let error = PrayError::Resolution(
            "package sample/extra not found in distribution /cache. \
             Missing /cache/v1/packages/sample/extra.json. Check the package name, version constraint `~> 1.0`, and that the source publishes registry metadata."
                .to_string(),
        );
        let annotated = annotate_missing_git_catalog(error, "sample/extra", "dist", "abc123");
        let text = annotated.to_string();
        assert!(text.contains("sample/extra"));
        assert!(text.contains("dist"));
        assert!(text.contains("abc123"));
        assert!(text.contains("pray update"));
        assert!(!text.contains("check the package name"));
    }
}