pray-core 1.22.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use crate::hashing::{checksum_managed_span_content, marker_id, LOCAL_EMBED_PACKAGE};
use crate::lockfile::ManagedSpanRecord;
use crate::manifest::ManifestTarget;
use crate::resolve::{ResolvedLocalFile, ResolvedPackage};
use crate::substitute::substitute_pray_symbols;
use crate::{PrayError, PrayResult};
use std::path::Path;

pub(crate) struct ContentBuilder {
    content: String,
    next_line: usize,
}

impl ContentBuilder {
    pub(crate) fn with_capacity(capacity: usize) -> Self {
        Self {
            content: String::with_capacity(capacity),
            next_line: 1,
        }
    }

    pub(crate) fn next_line_number(&self) -> usize {
        self.next_line
    }

    pub(crate) fn append_line(&mut self, line: &str) {
        self.content.push_str(line);
        self.content.push('\n');
        self.next_line += 1;
    }

    pub(crate) fn append_empty_line(&mut self) {
        self.content.push('\n');
        self.next_line += 1;
    }

    pub(crate) fn append_body(&mut self, body: &str) {
        let trimmed = body.trim_end_matches('\n');
        if trimmed.is_empty() {
            return;
        }
        for line in trimmed.split('\n') {
            self.append_line(line);
        }
    }

    pub(crate) fn finish(mut self) -> String {
        while self.content.ends_with("\n\n") {
            self.content.pop();
        }
        if !self.content.ends_with('\n') {
            self.content.push('\n');
        }
        self.content
    }
}

pub(crate) fn should_inline_export(package: &ResolvedPackage, export_name: &str) -> bool {
    package
        .spec
        .exports
        .get(export_name)
        .is_none_or(|export| matches!(export.kind.as_str(), "fragment" | "file"))
}

pub(crate) fn append_managed_local(
    builder: &mut ContentBuilder,
    managed_spans: &mut Vec<ManagedSpanRecord>,
    local: &ResolvedLocalFile,
    target: &ManifestTarget,
    output: &Path,
    symbols: &std::collections::BTreeMap<String, String>,
) -> PrayResult<()> {
    if local.content.is_empty() && local.optional {
        return Ok(());
    }
    let body = substitute_pray_symbols(&local.content, symbols)?;
    let seed = format!("local:{}:{}", local.manifest_path, target.name);
    append_managed_span(
        builder,
        managed_spans,
        ManagedSpanInput {
            seed: &seed,
            body: &body,
            output,
            package: LOCAL_EMBED_PACKAGE,
            export: &local.manifest_path,
            source_checksum: &local.source_checksum,
        },
    );
    Ok(())
}

pub(crate) fn append_managed_export(
    builder: &mut ContentBuilder,
    managed_spans: &mut Vec<ManagedSpanRecord>,
    package: &ResolvedPackage,
    export: &str,
    target: &ManifestTarget,
    output: &Path,
    symbols: &std::collections::BTreeMap<String, String>,
) -> PrayResult<()> {
    let raw = match package.export_bodies.get(export) {
        Some(text) => text,
        None if package
            .spec
            .exports
            .get(export)
            .is_some_and(|entry| entry.kind == "file") =>
        {
            return Err(PrayError::Integrity(format!(
                "compose cannot write binary export {export}; use file: for unmarked bytes"
            )));
        }
        None => {
            return Err(PrayError::Render(format!(
                "package {} is missing cached export {}",
                package.declaration.name, export
            )));
        }
    };
    let body = substitute_pray_symbols(raw, symbols)?;
    let seed = format!("{}:{}:{}", package.declaration.name, export, target.name);
    append_managed_span(
        builder,
        managed_spans,
        ManagedSpanInput {
            seed: &seed,
            body: &body,
            output,
            package: &package.declaration.name,
            export,
            source_checksum: &package.source_checksum,
        },
    );
    Ok(())
}

struct ManagedSpanInput<'a> {
    seed: &'a str,
    body: &'a str,
    output: &'a Path,
    package: &'a str,
    export: &'a str,
    source_checksum: &'a str,
}

fn append_managed_span(
    builder: &mut ContentBuilder,
    managed_spans: &mut Vec<ManagedSpanRecord>,
    input: ManagedSpanInput<'_>,
) {
    let id = marker_id(input.seed);
    let open_line = builder.next_line_number();
    builder.append_line(&format!("<!-- pray:{id} -->"));
    builder.append_body(input.body);
    let close_line = builder.next_line_number();
    builder.append_line(&format!("<!-- pray:{id} -->"));
    managed_spans.push(ManagedSpanRecord {
        id,
        target: input.output.to_string_lossy().to_string(),
        open_line,
        close_line,
        ideal_checksum: checksum_managed_span_content(input.body),
        package: input.package.to_string(),
        export: input.export.to_string(),
        source_checksum: input.source_checksum.to_string(),
        silenced: false,
    });
    builder.append_empty_line();
}