pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::manifest::{Manifest, ManifestPackage, ManifestSource};
use crate::paths::validate_project_relative_path;
use crate::resolve::implied_source_name;
use crate::{PrayError, PrayResult};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

pub use crate::publish_select::{
    resolve_path_remote, resolve_publish_destinations, PublishCliDest, PublishDestination,
};

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ManifestPublishRemote {
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub packages: Vec<String>,
}

pub fn validate_publish_remotes(manifest: &Manifest) -> PrayResult<()> {
    let mut seen = BTreeSet::new();
    for remote in &manifest.publish_remotes {
        if remote.name.trim().is_empty() {
            return Err(PrayError::Parse {
                kind: "manifest",
                message: "publish requires a name".to_string(),
            });
        }
        if !seen.insert(remote.name.clone()) {
            return Err(PrayError::Manifest(format!(
                "duplicate publish remote: {}",
                remote.name
            )));
        }
        match (&remote.path, &remote.url) {
            (Some(_), Some(_)) => {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: format!(
                        "publish \"{}\" must set path: or a URL, not both",
                        remote.name
                    ),
                });
            }
            (None, None) => {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: format!("publish \"{}\" requires path: or a URL", remote.name),
                });
            }
            (Some(path), None) => {
                validate_project_relative_path(path)?;
            }
            (None, Some(url)) => {
                if !is_publish_url(url) {
                    return Err(PrayError::Parse {
                        kind: "manifest",
                        message: format!(
                            "publish \"{}\" URL must be https://, http://, pray+ssh://, or ssh+pray://",
                            remote.name
                        ),
                    });
                }
            }
        }
        validate_remote_packages(manifest, remote)?;
    }
    Ok(())
}

fn validate_remote_packages(manifest: &Manifest, remote: &ManifestPublishRemote) -> PrayResult<()> {
    for package_name in &remote.packages {
        let Some(package) = manifest
            .packages
            .iter()
            .find(|package| package.name == *package_name)
        else {
            return Err(PrayError::Manifest(format!(
                "publish \"{}\" lists unknown package {package_name}",
                remote.name
            )));
        };
        if !package_is_path_owned(package, &manifest.sources) {
            return Err(PrayError::Manifest(format!(
                "publish \"{}\" lists {package_name}, which is not a path package",
                remote.name
            )));
        }
    }
    Ok(())
}

pub fn package_is_path_owned(package: &ManifestPackage, sources: &[ManifestSource]) -> bool {
    if package.path.is_some() {
        return true;
    }
    if package.git.is_some() || package.tarball.is_some() || package.oci.is_some() {
        return false;
    }
    let map = source_map(sources);
    match implied_source_name(package, &map) {
        Ok(Some(name)) => map.get(&name).is_some_and(|source| source.kind == "path"),
        Ok(None) | Err(_) => false,
    }
}

pub fn path_owned_package_names(manifest: &Manifest) -> Vec<String> {
    manifest
        .packages
        .iter()
        .filter(|package| package_is_path_owned(package, &manifest.sources))
        .map(|package| package.name.clone())
        .collect()
}

pub fn allowed_publish_names(manifest: &Manifest, listed: &[String]) -> Vec<String> {
    if listed.is_empty() {
        path_owned_package_names(manifest)
    } else {
        listed.to_vec()
    }
}

fn is_publish_url(url: &str) -> bool {
    url.starts_with("https://")
        || url.starts_with("http://")
        || url.starts_with("pray+ssh://")
        || url.starts_with("ssh+pray://")
}

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