pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::manifest::{
    DestinationEntry, DestinationMode, ExportRole, Manifest, ManifestLocal, ManifestPackage,
    ManifestTarget,
};
use crate::{PrayError, PrayResult};

pub fn is_local_path_form(value: &str) -> bool {
    value.starts_with('.')
        || value.starts_with('/')
        || value.ends_with(".md")
        || value.ends_with(".txt")
        || value.ends_with(".markdown")
}

pub fn destination_target_name(mode: DestinationMode, path: &str) -> String {
    let prefix = match mode {
        DestinationMode::Compose => "compose",
        DestinationMode::Tree => "tree",
        DestinationMode::Legacy => "legacy",
    };
    format!("{prefix}:{path}")
}

pub fn new_destination_target(mode: DestinationMode, path: &str) -> ManifestTarget {
    let mut target = ManifestTarget {
        name: destination_target_name(mode, path),
        mode,
        scoped: true,
        ..ManifestTarget::default()
    };
    match mode {
        DestinationMode::Compose => target.outputs.push(path.to_string()),
        DestinationMode::Tree => target.skills.push(path.to_string()),
        DestinationMode::Legacy => {}
    }
    target
}

pub fn upsert_package(manifest: &mut Manifest, package: ManifestPackage) -> PrayResult<()> {
    if let Some(existing) = manifest
        .packages
        .iter_mut()
        .find(|candidate| candidate.name == package.name)
    {
        if existing.constraint != package.constraint
            && existing.constraint != "*"
            && package.constraint != "*"
        {
            return Err(PrayError::Manifest(format!(
                "package {} declared with conflicting constraints ({} vs {})",
                package.name, existing.constraint, package.constraint
            )));
        }
        if existing.constraint == "*" && package.constraint != "*" {
            existing.constraint = package.constraint;
        }
        if existing.source.is_none() {
            existing.source = package.source;
        } else if package.source.is_some() && existing.source != package.source {
            return Err(PrayError::Manifest(format!(
                "package {} declared with conflicting sources",
                package.name
            )));
        }
        for export in package.exports {
            if !existing.exports.contains(&export) {
                existing.exports.push(export);
            }
        }
        for role in package.roles {
            if !existing.roles.contains(&role) {
                existing.roles.push(role);
            }
        }
        if package.file.is_some() {
            if existing.file.is_some() && existing.file != package.file {
                return Err(PrayError::Manifest(format!(
                    "package {} declared with conflicting file: destinations",
                    package.name
                )));
            }
            existing.file = package.file;
        }
        existing.bound = existing.bound || package.bound;
        existing.optional = existing.optional || package.optional;
        if existing.path.is_none() {
            existing.path = package.path;
        }
        if existing.git.is_none() {
            existing.git = package.git;
        }
        if existing.tag.is_none() {
            existing.tag = package.tag;
        }
        if existing.rev.is_none() {
            existing.rev = package.rev;
        }
        if existing.tarball.is_none() {
            existing.tarball = package.tarball;
        }
        if existing.oci.is_none() {
            existing.oci = package.oci;
        }
        for group in package.groups {
            if !existing.groups.contains(&group) {
                existing.groups.push(group);
            }
        }
        return Ok(());
    }
    manifest.packages.push(package);
    Ok(())
}

pub fn upsert_local(manifest: &mut Manifest, local: ManifestLocal) {
    if let Some(existing) = manifest
        .local
        .iter_mut()
        .find(|candidate| candidate.path == local.path)
    {
        existing.bound = existing.bound || local.bound;
        existing.optional = existing.optional || local.optional;
        if existing.position == "after" && local.position != "after" {
            existing.position = local.position;
        }
        if existing.file.is_none() {
            existing.file = local.file;
        }
        return;
    }
    manifest.local.push(local);
}

pub fn bind_package_entry(target: &mut ManifestTarget, package_name: &str) {
    let entry = DestinationEntry::Package {
        name: package_name.to_string(),
    };
    if !target.entries.contains(&entry) {
        target.entries.push(entry);
    }
}

pub fn bind_local_entry(target: &mut ManifestTarget, path: &str) {
    let entry = DestinationEntry::Local {
        path: path.to_string(),
    };
    if !target.entries.contains(&entry) {
        target.entries.push(entry);
    }
}

pub fn role_for_destination(mode: DestinationMode) -> Option<ExportRole> {
    match mode {
        DestinationMode::Compose => Some(ExportRole::Fragment),
        DestinationMode::Tree => Some(ExportRole::Folder),
        DestinationMode::Legacy => None,
    }
}

pub fn package_bound_to_compose(package: &ManifestPackage, target: &ManifestTarget) -> bool {
    if target.scoped && target.mode == DestinationMode::Compose {
        return target.entries.iter().any(|entry| match entry {
            DestinationEntry::Package { name } => name == &package.name,
            DestinationEntry::Local { .. } => false,
        });
    }
    if package.bound || package.file.is_some() {
        return false;
    }
    true
}

pub fn package_bound_to_tree(package: &ManifestPackage, target: &ManifestTarget) -> bool {
    if target.scoped && target.mode == DestinationMode::Tree {
        return target.entries.iter().any(|entry| match entry {
            DestinationEntry::Package { name } => name == &package.name,
            DestinationEntry::Local { .. } => false,
        });
    }
    if package.bound || package.file.is_some() {
        return false;
    }
    true
}

pub fn export_kind_matches_role(kind: &str, role: ExportRole) -> bool {
    match role {
        ExportRole::Fragment => kind == "fragment",
        ExportRole::Folder => matches!(kind, "folder" | "skill"),
        ExportRole::File => kind == "file",
    }
}

pub fn local_is_compose_embed(manifest: &Manifest, local: &ManifestLocal) -> bool {
    if local.file.is_some() {
        return false;
    }
    !manifest.targets.iter().any(|target| {
        target.mode == DestinationMode::Tree
            && target.entries.iter().any(|entry| match entry {
                DestinationEntry::Local { path } => path == &local.path,
                DestinationEntry::Package { .. } => false,
            })
    })
}

pub fn try_apply_local_pray(
    manifest: &mut Manifest,
    path: &str,
    file_dest: Option<&str>,
    other_package_signal: bool,
    destination_index: Option<usize>,
) -> PrayResult<bool> {
    if !is_local_path_form(path) {
        return Ok(false);
    }
    if file_dest.is_some() {
        if other_package_signal {
            return Ok(false);
        }
        return Err(PrayError::Parse {
            kind: "manifest",
            message: "a local file cannot use file:".to_string(),
        });
    }
    if other_package_signal {
        return Ok(false);
    }
    let Some(index) = destination_index else {
        return Err(PrayError::Parse {
            kind: "manifest",
            message: "local pray paths are only valid inside compose".to_string(),
        });
    };
    let mode = manifest.targets[index].mode;
    if mode != DestinationMode::Compose {
        return Err(PrayError::Parse {
            kind: "manifest",
            message: "local pray paths are only valid inside compose".to_string(),
        });
    }
    let local = ManifestLocal {
        path: path.to_string(),
        position: "after".to_string(),
        optional: false,
        bound: true,
        file: None,
    };
    bind_local_entry(&mut manifest.targets[index], &local.path);
    upsert_local(manifest, local);
    Ok(true)
}