//! El contrato [`NodeSource`] —lo mínimo que el VFS necesita de un //! repositorio Minga— y sus dos backends. Agnóstico de `fuser`. //! //! El VFS no quiere conocer `sled` ni la estructura interna del store: //! sólo necesita (a) enumerar las raíces del MST y (b) resolver un nodo //! por hash. Eso es [`NodeSource`]. [`RepoSource`] lo implementa sobre //! el [`PersistentRepo`] en disco; [`MemSource`] sobre un `MemStore` en //! RAM (tests, índices efímeros recién sincronizados). use minga_core::{ContentHash, SemanticNode, StoredNode}; use minga_store::PersistentRepo; /// Lo que el VFS necesita de un repositorio para proyectarlo. pub trait NodeSource { /// Hashes raíz: el conjunto de claves del MST, un elemento por /// archivo ingerido. Es lo que se lista bajo `roots/`. fn roots(&self) -> Vec; /// Resuelve un único nodo (un eslabón del grafo) por su hash. /// `None` si no está en el almacén. fn get(&self, hash: &ContentHash) -> Option; } /// Reconstruye el `SemanticNode` completo de un hash, resolviendo /// recursivamente sus hijos contra `source`. /// /// Devuelve `None` si el almacén está incompleto: o el propio `hash` /// falta, o lo hace algún descendiente (puede ocurrir en un repo a /// medio sincronizar). pub fn reconstruct(source: &S, hash: &ContentHash) -> Option where S: NodeSource + ?Sized, { let stored = source.get(hash)?; let mut children = Vec::with_capacity(stored.children.len()); for child in &stored.children { children.push(reconstruct(source, child)?); } Some(SemanticNode { kind: stored.kind, field_name: stored.field_name, leaf_text: stored.leaf_text, children, }) } /// [`NodeSource`] respaldado por un [`PersistentRepo`] de `minga-store` /// (almacén `sled` en disco). Es la fuente que usa `minga mount`. pub struct RepoSource { repo: PersistentRepo, } impl RepoSource { /// Envuelve un repo ya abierto. La propiedad pasa al `RepoSource`: /// el repo se cierra cuando éste se dropea. pub fn new(repo: PersistentRepo) -> Self { Self { repo } } } impl NodeSource for RepoSource { fn roots(&self) -> Vec { // Las claves del MST corruptas (si las hubiera) se descartan en // silencio: un par de entradas ilegibles no deben tirar el `ls`. self.repo.mst.iter().filter_map(Result::ok).collect() } fn get(&self, hash: &ContentHash) -> Option { self.repo.nodes.get(hash).ok().flatten() } } /// [`NodeSource`] en memoria: un `MemStore` más un conjunto explícito /// de raíces. Para tests y para montar índices que viven sólo en RAM. #[derive(Default)] pub struct MemSource { store: minga_core::MemStore, roots: Vec, } impl MemSource { pub fn new() -> Self { Self::default() } /// Inserta un árbol como raíz (un "archivo") y devuelve su hash. /// Idempotente: ingerir dos veces el mismo árbol no lo duplica. pub fn add_root(&mut self, node: &SemanticNode) -> ContentHash { use minga_core::NodeStore; let hash = self.store.put(node); if !self.roots.contains(&hash) { self.roots.push(hash); } hash } } impl NodeSource for MemSource { fn roots(&self) -> Vec { self.roots.clone() } fn get(&self, hash: &ContentHash) -> Option { use minga_core::NodeStore; self.store.get(hash).cloned() } } #[cfg(test)] mod tests { use super::*; use minga_core::ast::SemanticNode; fn leaf(kind: &str, text: &str) -> SemanticNode { SemanticNode { kind: kind.to_string(), field_name: None, leaf_text: Some(text.as_bytes().to_vec()), children: Vec::new(), } } #[test] fn mem_source_reconstructs_what_it_stored() { let tree = SemanticNode { kind: "root".to_string(), field_name: None, leaf_text: None, children: vec![leaf("a", "1"), leaf("b", "2")], }; let mut src = MemSource::new(); let hash = src.add_root(&tree); assert_eq!(src.roots(), vec![hash]); let back = reconstruct(&src, &hash).expect("debe reconstruir"); assert_eq!(back, tree); } #[test] fn add_root_is_idempotent() { let tree = leaf("only", "x"); let mut src = MemSource::new(); let h1 = src.add_root(&tree); let h2 = src.add_root(&tree); assert_eq!(h1, h2); assert_eq!(src.roots().len(), 1); } #[test] fn unknown_hash_reconstructs_to_none() { let src = MemSource::new(); assert!(reconstruct(&src, &ContentHash([0u8; 32])).is_none()); } }