pray-core 1.8.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use super::call::{parse_call, string_from_literal, string_from_value};
use super::decls::{parse_local_decl, parse_package_decl};
use super::BlockParser;
use crate::manifest::{DestinationMode, ExportRole, Manifest, ManifestLocal};
use crate::statement_surface::split_symbol_assignment;
use crate::{PrayError, PrayResult};

impl BlockParser<'_> {
    pub(super) fn parse_destination_block(
        &mut self,
        manifest: &mut Manifest,
        rest: &str,
        mode: DestinationMode,
    ) -> PrayResult<()> {
        let is_block = rest.trim_end().ends_with("do");
        if !is_block {
            return Err(PrayError::Parse {
                kind: "manifest",
                message: format!(
                    "{} must use a block",
                    match mode {
                        DestinationMode::Compose => "compose",
                        DestinationMode::Tree => "tree",
                        DestinationMode::Legacy => "destination",
                    }
                ),
            });
        }
        let header = rest.trim_end_matches("do").trim();
        let (values, _) = parse_call(header)?;
        let path = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
            kind: "manifest",
            message: "destination missing path".to_string(),
        })?)?;
        let target = crate::destination::new_destination_target(mode, &path);
        manifest.targets.push(target);
        let index = manifest.targets.len() - 1;
        while let Some(statement) = self.next_statement()? {
            if statement == "end" {
                return Ok(());
            }
            if let Some(pray_rest) = statement
                .strip_prefix("pray ")
                .or_else(|| statement.strip_prefix("use "))
                .or_else(|| statement.strip_prefix("include "))
                .or_else(|| statement.strip_prefix("agent "))
                .or_else(|| statement.strip_prefix("package "))
            {
                self.apply_pray_statement(manifest, pray_rest, Some(index))?;
                continue;
            }
            if mode == DestinationMode::Compose {
                if let Some(local_rest) = statement.strip_prefix("local ") {
                    let mut local = parse_local_decl(local_rest)?;
                    local.bound = true;
                    crate::destination::bind_local_entry(&mut manifest.targets[index], &local.path);
                    crate::destination::upsert_local(manifest, local);
                    continue;
                }
            }
            return Err(PrayError::Parse {
                kind: "manifest",
                message: format!("unsupported statement inside destination block: {statement}"),
            });
        }
        Err(PrayError::Parse {
            kind: "manifest",
            message: "missing 'end' for destination block".to_string(),
        })
    }

    pub(super) fn parse_symbols_block(&mut self, manifest: &mut Manifest) -> PrayResult<()> {
        while let Some(statement) = self.next_statement()? {
            if statement == "end" {
                return Ok(());
            }
            let Some((key, value_literal)) = split_symbol_assignment(&statement) else {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: format!(
                        "unsupported statement inside pray/template block: {statement}"
                    ),
                });
            };
            if !crate::substitute::is_pray_symbol_key(&key) {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: format!("invalid pray symbol key `{key}`"),
                });
            }
            if manifest.symbols.contains_key(&key) {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: format!("duplicate pray symbol `{key}`"),
                });
            }
            manifest
                .symbols
                .insert(key, string_from_literal(&value_literal)?);
        }
        Err(PrayError::Parse {
            kind: "manifest",
            message: "missing 'end' for pray/template block".to_string(),
        })
    }

    pub(super) fn parse_file_block(&mut self, manifest: &mut Manifest, rest: &str) -> PrayResult<()> {
        let is_block = rest.trim_end().ends_with("do");
        if !is_block {
            return Err(PrayError::Parse {
                kind: "manifest",
                message: "file must use a block (or use pray ..., file: \"path\")".to_string(),
            });
        }
        let header = rest.trim_end_matches("do").trim();
        let (values, _) = parse_call(header)?;
        let file_path = string_from_value(values.first().ok_or_else(|| PrayError::Parse {
            kind: "manifest",
            message: "file block missing path".to_string(),
        })?)?;
        let mut saw_package = false;
        while let Some(statement) = self.next_statement()? {
            if statement == "end" {
                if !saw_package {
                    return Err(PrayError::Parse {
                        kind: "manifest",
                        message: "file block requires a pray package declaration".to_string(),
                    });
                }
                return Ok(());
            }
            if let Some(pray_rest) = statement
                .strip_prefix("pray ")
                .or_else(|| statement.strip_prefix("use "))
                .or_else(|| statement.strip_prefix("include "))
                .or_else(|| statement.strip_prefix("agent "))
                .or_else(|| statement.strip_prefix("package "))
            {
                let mut package = self.parse_package_with_groups(pray_rest)?;
                if package.file.is_some() {
                    return Err(PrayError::Parse {
                        kind: "manifest",
                        message: "file: keyword is invalid inside a file block".to_string(),
                    });
                }
                package.file = Some(file_path.clone());
                package.bound = true;
                if !package.roles.contains(&ExportRole::File) {
                    package.roles.push(ExportRole::File);
                }
                crate::destination::upsert_package(manifest, package)?;
                saw_package = true;
                continue;
            }
            return Err(PrayError::Parse {
                kind: "manifest",
                message: format!("unsupported statement inside file block: {statement}"),
            });
        }
        Err(PrayError::Parse {
            kind: "manifest",
            message: "missing 'end' for file block".to_string(),
        })
    }

    pub(super) fn apply_pray_statement(
        &mut self,
        manifest: &mut Manifest,
        rest: &str,
        destination_index: Option<usize>,
    ) -> PrayResult<()> {
        let (values, keywords) = parse_call(rest)?;
        if values.is_empty() {
            return Err(PrayError::Parse {
                kind: "manifest",
                message: "pray missing package or path".to_string(),
            });
        }
        let first = string_from_value(&values[0])?;
        let has_package_signal = values.len() > 1
            || keywords.contains_key("source")
            || keywords.contains_key("export")
            || keywords.contains_key("exports")
            || keywords.contains_key("file")
            || keywords.contains_key("optional")
            || keywords.contains_key("path")
            || keywords.contains_key("git")
            || keywords.contains_key("tag")
            || keywords.contains_key("rev")
            || keywords.contains_key("tarball")
            || keywords.contains_key("oci")
            || keywords.contains_key("targets")
            || keywords.contains_key("features");

        let in_compose = destination_index.is_some_and(|index| {
            manifest
                .targets
                .get(index)
                .is_some_and(|target| target.mode == DestinationMode::Compose)
        });

        if !has_package_signal && crate::destination::is_local_path_form(&first) {
            if !in_compose {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: "local pray paths are only valid inside compose blocks".to_string(),
                });
            }
            let local = ManifestLocal {
                path: first,
                position: "after".to_string(),
                optional: false,
                bound: true,
            };
            if let Some(index) = destination_index {
                crate::destination::bind_local_entry(&mut manifest.targets[index], &local.path);
            }
            crate::destination::upsert_local(manifest, local);
            return Ok(());
        }

        let mut package = parse_package_decl(rest)?;
        package.groups = self.group_stack.last().cloned().unwrap_or_default();
        if package.file.is_some() {
            if destination_index.is_some() {
                return Err(PrayError::Parse {
                    kind: "manifest",
                    message: "file: is mutually exclusive with compose/tree nesting".to_string(),
                });
            }
            package.bound = true;
            if !package.roles.contains(&ExportRole::File) {
                package.roles.push(ExportRole::File);
            }
        }
        if let Some(index) = destination_index {
            let mode = manifest.targets[index].mode;
            package.bound = true;
            if let Some(role) = crate::destination::role_for_destination(mode) {
                if !package.roles.contains(&role) {
                    package.roles.push(role);
                }
            }
            crate::destination::bind_package_entry(&mut manifest.targets[index], &package.name);
        }
        crate::destination::upsert_package(manifest, package)?;
        Ok(())
    }

    pub(super) fn parse_group_block(&mut self, manifest: &mut Manifest) -> PrayResult<()> {
        while let Some(statement) = self.next_statement()? {
            if statement == "end" {
                return Ok(());
            }
            if let Some(rest) = statement.strip_prefix("agent ") {
                manifest.note_deprecated_keyword(crate::deprecation::DEPRECATED_AGENT);
                crate::destination::upsert_package(
                    manifest,
                    self.parse_package_with_groups(rest)?,
                )?;
                continue;
            }
            if let Some(rest) = statement
                .strip_prefix("package ")
                .or_else(|| statement.strip_prefix("pray "))
                .or_else(|| statement.strip_prefix("use "))
            {
                crate::destination::upsert_package(
                    manifest,
                    self.parse_package_with_groups(rest)?,
                )?;
                continue;
            }
            return Err(PrayError::Parse {
                kind: "manifest",
                message: format!(
                    "group blocks only support agent, package, or pray declarations: {statement}"
                ),
            });
        }
        Err(PrayError::Parse {
            kind: "manifest",
            message: "missing 'end' for group block".to_string(),
        })
    }

}