pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::publish_remote::ManifestPublishRemote;
use crate::{PrayError, PrayResult};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishDestination {
    pub name: String,
    pub root: Option<PathBuf>,
    pub server: Option<String>,
    pub packages: Vec<String>,
}

#[derive(Debug, Clone, Default)]
pub struct PublishCliDest {
    pub to: Vec<String>,
    pub roots: Vec<PathBuf>,
    pub servers: Vec<String>,
}

pub fn resolve_publish_destinations(
    remotes: &[ManifestPublishRemote],
    cli: &PublishCliDest,
    project_root: &Path,
) -> PrayResult<Vec<PublishDestination>> {
    if !cli.to.is_empty() && (!cli.roots.is_empty() || !cli.servers.is_empty()) {
        return Err(PrayError::Usage(
            "publish --to cannot be combined with --root or --server".to_string(),
        ));
    }
    if remotes.is_empty() {
        return resolve_undeclared_destinations(cli);
    }
    let selected = select_remotes(remotes, cli, project_root)?;
    Ok(selected
        .into_iter()
        .map(|remote| PublishDestination {
            name: remote.name.clone(),
            root: remote.path.as_ref().map(|path| project_root.join(path)),
            server: remote.url.clone(),
            packages: remote.packages.clone(),
        })
        .collect())
}

pub fn resolve_path_remote(
    remotes: &[ManifestPublishRemote],
    to: Option<&str>,
    root: Option<&Path>,
    project_root: &Path,
) -> PrayResult<PathBuf> {
    let path_remotes: Vec<&ManifestPublishRemote> = remotes
        .iter()
        .filter(|remote| remote.path.is_some())
        .collect();
    if let Some(name) = to {
        let remote = remotes
            .iter()
            .find(|remote| remote.name == name)
            .ok_or_else(|| PrayError::Usage(format!("unknown publish remote: {name}")))?;
        let path = remote.path.as_ref().ok_or_else(|| {
            PrayError::Usage(format!(
                "publish remote {name} is a URL; yank, serve, and token need a path remote"
            ))
        })?;
        return Ok(project_root.join(path));
    }
    if remotes.is_empty() {
        return root
            .map(Path::to_path_buf)
            .ok_or_else(|| PrayError::Usage("requires --root PATH".to_string()));
    }
    if let Some(root) = root {
        let matched = remotes.iter().find(|remote| {
            remote
                .path
                .as_ref()
                .is_some_and(|path| root_matches(root, path, project_root))
        });
        if matched.is_none() {
            return Err(PrayError::Usage(format!(
                "path {} is not a declared publish remote",
                root.display()
            )));
        }
        return Ok(root.to_path_buf());
    }
    if path_remotes.len() == 1 {
        if let Some(path) = &path_remotes[0].path {
            return Ok(project_root.join(path));
        }
    }
    Err(PrayError::Usage(
        "say which path remote with --to NAME or --root PATH".to_string(),
    ))
}

fn resolve_undeclared_destinations(cli: &PublishCliDest) -> PrayResult<Vec<PublishDestination>> {
    if !cli.to.is_empty() {
        return Err(PrayError::Usage(
            "Prayfile has no publish remotes; pass --root PATH or --server URL".to_string(),
        ));
    }
    if cli.roots.is_empty() && cli.servers.is_empty() {
        return Err(PrayError::Unsupported(
            "publish requires at least one --root PATH or --server URL".to_string(),
        ));
    }
    let mut dests = Vec::new();
    for root in &cli.roots {
        dests.push(PublishDestination {
            name: root.display().to_string(),
            root: Some(root.clone()),
            server: None,
            packages: Vec::new(),
        });
    }
    for server in &cli.servers {
        dests.push(PublishDestination {
            name: server.clone(),
            root: None,
            server: Some(server.clone()),
            packages: Vec::new(),
        });
    }
    Ok(dests)
}

fn select_remotes<'a>(
    remotes: &'a [ManifestPublishRemote],
    cli: &PublishCliDest,
    project_root: &Path,
) -> PrayResult<Vec<&'a ManifestPublishRemote>> {
    if !cli.to.is_empty() {
        let mut selected = Vec::new();
        for name in &cli.to {
            let remote = remotes
                .iter()
                .find(|remote| remote.name == *name)
                .ok_or_else(|| PrayError::Usage(format!("unknown publish remote: {name}")))?;
            selected.push(remote);
        }
        return Ok(selected);
    }
    if cli.roots.is_empty() && cli.servers.is_empty() {
        return Ok(remotes.iter().collect());
    }
    let mut selected = Vec::new();
    for root in &cli.roots {
        let remote = remotes
            .iter()
            .find(|remote| {
                remote
                    .path
                    .as_ref()
                    .is_some_and(|path| root_matches(root, path, project_root))
            })
            .ok_or_else(|| {
                PrayError::Usage(format!(
                    "path {} is not a declared publish remote",
                    root.display()
                ))
            })?;
        selected.push(remote);
    }
    for server in &cli.servers {
        let remote = remotes
            .iter()
            .find(|remote| remote.url.as_deref() == Some(server.as_str()))
            .ok_or_else(|| {
                PrayError::Usage(format!("server {server} is not a declared publish remote"))
            })?;
        selected.push(remote);
    }
    Ok(selected)
}

pub(crate) fn root_matches(cli_root: &Path, remote_path: &str, project_root: &Path) -> bool {
    let declared = project_root.join(remote_path);
    if cli_root == declared || cli_root == Path::new(remote_path) {
        return true;
    }
    strip_dot_slash(&cli_root.to_string_lossy()) == strip_dot_slash(remote_path)
}

fn strip_dot_slash(value: &str) -> String {
    value
        .trim()
        .trim_start_matches("./")
        .trim_end_matches('/')
        .replace('\\', "/")
}