lex-syntax 0.11.36

Tokenizer + parser for the Lex programming language.
Documentation
//! `lex.lock` — the resolved, reproducible pin file for registry
//! dependencies (#893).
//!
//! A `lex.toml` registry dependency states a *constraint*
//! (`{ registry = "…", version = "^1.2" }`); the registry publishes a set of
//! immutable releases (#911). Resolution (`lex pkg lock`, in `lex-cli`) picks
//! the highest release satisfying the constraint and records the exact choice
//! — version *and* the op-log head it points at — here, so a later
//! `resolve_package_import` fetches that precise version instead of drifting
//! to whatever is newest.
//!
//! The *format* lives in `lex-syntax` (next to [`crate::workspace`] and
//! [`crate::semver`]) because it is the resolved form of the manifest: package
//! resolution reads it, and the CLI writes it. The *resolution policy* (which
//! release to pick, how to fetch the version list) stays in `lex-cli`.

use serde::{Deserialize, Serialize};
use std::path::Path;

/// Current lockfile format version.
pub const LOCK_FORMAT_VERSION: u32 = 1;

/// One resolved dependency: the constraint that produced it and the exact
/// release it pins to.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LockEntry {
    pub name: String,
    pub registry: String,
    /// The `version = "…"` constraint from `lex.toml`, recorded so `lex pkg
    /// lock` can tell whether an existing pin still satisfies its declaration.
    pub constraint: String,
    /// The concrete `MAJOR.MINOR.PATCH` release chosen.
    pub version: String,
    /// The op-log head that release points at (#911). `None` if the registry
    /// served a release with no head — resolution still pins the version.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub head_op: Option<String>,
}

/// The whole `lex.lock` file.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LockFile {
    /// Lockfile format version, so a future change can migrate rather than
    /// silently misread.
    pub version: u32,
    #[serde(default, rename = "package")]
    pub packages: Vec<LockEntry>,
}

impl LockFile {
    /// The recorded pin for `name`, if any.
    pub fn entry(&self, name: &str) -> Option<&LockEntry> {
        self.packages.iter().find(|e| e.name == name)
    }

    /// Load `<dir>/lex.lock`, or `None` if it is absent or unreadable. A
    /// malformed lock is treated as absent — resolution falls back to the
    /// declared version and reports if that is itself unusable.
    pub fn load_dir(dir: &Path) -> Option<LockFile> {
        let raw = std::fs::read_to_string(dir.join("lex.lock")).ok()?;
        Self::from_toml(&raw).ok()
    }

    /// Parse a `lex.lock` from a TOML string.
    pub fn from_toml(s: &str) -> Result<Self, String> {
        toml::from_str(s).map_err(|e| format!("parsing lex.lock: {e}"))
    }

    /// Render to the on-disk TOML, with a do-not-edit header. Entries are
    /// sorted by name so the file is stable across runs (a churning lock is
    /// noise in review).
    pub fn to_toml(&self) -> Result<String, String> {
        let mut sorted = self.clone();
        sorted.packages.sort_by(|a, b| a.name.cmp(&b.name));
        let body = toml::to_string(&sorted).map_err(|e| format!("serializing lex.lock: {e}"))?;
        Ok(format!(
            "# lex.lock — resolved dependency pins, generated by `lex pkg lock`.\n\
             # Do not edit by hand; run `lex pkg lock` (or `lex pkg update`) instead.\n\n{body}"
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn toml_round_trips_and_sorts_by_name() {
        let lf = LockFile {
            version: LOCK_FORMAT_VERSION,
            packages: vec![
                LockEntry {
                    name: "zeta".into(),
                    registry: "reg".into(),
                    constraint: "^1".into(),
                    version: "1.0.0".into(),
                    head_op: Some("op_z".into()),
                },
                LockEntry {
                    name: "alpha".into(),
                    registry: "reg".into(),
                    constraint: "^2".into(),
                    version: "2.1.0".into(),
                    head_op: None,
                },
            ],
        };
        let toml = lf.to_toml().unwrap();
        assert!(toml.find("alpha").unwrap() < toml.find("zeta").unwrap());
        let back = LockFile::from_toml(&toml).unwrap();
        assert_eq!(back.version, LOCK_FORMAT_VERSION);
        assert_eq!(back.entry("alpha").unwrap().head_op, None);
        assert_eq!(back.entry("zeta").unwrap().version, "1.0.0");
    }
}