Skip to main content

crisp_manifest/
lock.rs

1//! `crisp.lock` — resolved deps + sealed pub API (spec §12.5).
2
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6use thiserror::Error;
7
8pub const LOCK_VERSION: u32 = 1;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct SealedSignature {
12    pub name: String,
13    pub rust_signature: String,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct ResolvedDependency {
18    pub name: String,
19    pub version: String,
20    #[serde(default)]
21    pub rust: bool,
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub features: Vec<String>,
24    /// Path as written in `crisp.toml` (relative to the Crisp crate root).
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub path: Option<String>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CrispLock {
31    pub version: u32,
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub dependencies: Vec<ResolvedDependency>,
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub sealed_api: Vec<SealedSignature>,
36}
37
38impl Default for CrispLock {
39    fn default() -> Self {
40        Self {
41            version: LOCK_VERSION,
42            dependencies: Vec::new(),
43            sealed_api: Vec::new(),
44        }
45    }
46}
47
48#[derive(Debug, Error)]
49pub enum LockError {
50    #[error("failed to read crisp.lock: {0}")]
51    Io(#[from] std::io::Error),
52    #[error("failed to parse crisp.lock: {0}")]
53    Parse(#[from] serde_json::Error),
54    #[error("unsupported crisp.lock version {0} (expected {LOCK_VERSION})")]
55    UnsupportedVersion(u32),
56}
57
58pub fn read_lock(crate_root: &Path) -> Result<Option<CrispLock>, LockError> {
59    let path = crate_root.join("crisp.lock");
60    if !path.is_file() {
61        return Ok(None);
62    }
63    let raw = fs::read_to_string(&path)?;
64    let lock: CrispLock = serde_json::from_str(&raw)?;
65    if lock.version != LOCK_VERSION {
66        return Err(LockError::UnsupportedVersion(lock.version));
67    }
68    Ok(Some(lock))
69}
70
71pub fn write_lock(crate_root: &Path, lock: &CrispLock) -> Result<(), LockError> {
72    let path = crate_root.join("crisp.lock");
73    let raw = serde_json::to_string_pretty(lock)?;
74    fs::write(path, raw)?;
75    Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use tempfile::TempDir;
82
83    #[test]
84    fn roundtrip_lock() {
85        let dir = TempDir::new().unwrap();
86        let lock = CrispLock {
87            version: LOCK_VERSION,
88            dependencies: vec![ResolvedDependency {
89                name: "tokio".into(),
90                version: "1".into(),
91                rust: true,
92                features: vec!["rt".into(), "macros".into()],
93                path: None,
94            }],
95            sealed_api: vec![SealedSignature {
96                name: "main::main".into(),
97                rust_signature: "pub fn main() -> ()".into(),
98            }],
99        };
100        write_lock(dir.path(), &lock).unwrap();
101        let read = read_lock(dir.path()).unwrap().expect("lock");
102        assert_eq!(read, lock);
103    }
104}