rustmotion 0.8.0

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};

use serde_json::Value;

#[derive(Clone)]
pub struct Baseline {
    pub source: String,
    pub raw: Value,
}

pub type SharedBaselines = Arc<Mutex<HashMap<PathBuf, Baseline>>>;

pub fn baseline_slot() -> SharedBaselines {
    static SLOT: OnceLock<SharedBaselines> = OnceLock::new();
    SLOT.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
        .clone()
}

pub fn ensure_baseline(slot: &SharedBaselines, path: &Path, source: &str, raw: &Value) {
    if raw.is_null() {
        return;
    }
    let mut map = slot.lock().unwrap_or_else(|e| e.into_inner());
    map.entry(path.to_path_buf()).or_insert_with(|| Baseline {
        source: source.to_string(),
        raw: raw.clone(),
    });
}

pub fn set_baseline(slot: &SharedBaselines, path: &Path, source: String, raw: Value) {
    let mut map = slot.lock().unwrap_or_else(|e| e.into_inner());
    map.insert(path.to_path_buf(), Baseline { source, raw });
}

pub fn get_baseline(slot: &SharedBaselines, path: &Path) -> Option<Baseline> {
    let map = slot.lock().unwrap_or_else(|e| e.into_inner());
    map.get(path).cloned()
}

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

    fn local() -> SharedBaselines {
        Arc::new(Mutex::new(HashMap::new()))
    }

    #[test]
    fn ensure_keeps_the_original_snapshot() {
        let slot = local();
        let p = Path::new("/w/a.json");
        ensure_baseline(&slot, p, "v1", &json!({ "v": 1 }));
        ensure_baseline(&slot, p, "v2", &json!({ "v": 2 }));
        let b = get_baseline(&slot, p).unwrap();
        assert_eq!(b.source, "v1");
        assert_eq!(b.raw, json!({ "v": 1 }));
    }

    #[test]
    fn set_baseline_overwrites() {
        let slot = local();
        let p = Path::new("/w/a.json");
        ensure_baseline(&slot, p, "v1", &json!(1));
        set_baseline(&slot, p, "v2".into(), json!(2));
        assert_eq!(get_baseline(&slot, p).unwrap().source, "v2");
    }

    #[test]
    fn null_raw_is_not_snapshotted_and_missing_is_none() {
        let slot = local();
        let p = Path::new("/w/broken.json");
        ensure_baseline(&slot, p, "{ bad", &Value::Null);
        assert!(get_baseline(&slot, p).is_none());
    }
}