197 lines
6.8 KiB
Rust
197 lines
6.8 KiB
Rust
//! Local registry mapping `OntologyId → local_path + classification`.
|
|
//!
|
|
//! Phase 1 (D28) persists this as a JSON document inside
|
|
//! `.ontoref/ontology_registry.ncl`. Real NCL contract enforcement
|
|
//! arrives once the catalog/ schema for the registry is wired in;
|
|
//! parsing the registry by hand keeps O1.8 self-contained.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::classification::OntologyType;
|
|
use crate::ontology_id::OntologyId;
|
|
use crate::{Error, Result};
|
|
|
|
/// One registry entry — an ontology this actor knows about locally.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RegistryEntry {
|
|
/// Content address of the ontology.
|
|
pub ontology_id: OntologyId,
|
|
/// Path to the oplog database file for this ontology on the local
|
|
/// filesystem.
|
|
pub oplog_path: PathBuf,
|
|
/// Classification of the ontology (governs sharing rules).
|
|
pub classification: OntologyType,
|
|
/// Most recent state root verified by this actor for this ontology.
|
|
/// `None` when the ontology has been registered but its current
|
|
/// state has not yet been confirmed.
|
|
pub verified_state_root_hex: Option<String>,
|
|
}
|
|
|
|
/// In-memory snapshot of the local ontology registry.
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct OntologyRegistry {
|
|
/// All known ontologies, in insertion order.
|
|
pub entries: Vec<RegistryEntry>,
|
|
}
|
|
|
|
impl OntologyRegistry {
|
|
/// Open an empty registry — useful for tests and for actors that
|
|
/// have never registered an ontology.
|
|
pub fn empty() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Load the registry from the JSON document at `path`. A missing
|
|
/// file is treated as an empty registry — this matches the
|
|
/// expected on-disk state of a fresh actor.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - [`Error::Io`] when the file exists but cannot be read.
|
|
/// - [`Error::RegistryParse`] when the file exists but does not
|
|
/// parse into the registry schema.
|
|
pub fn load(path: &Path) -> Result<Self> {
|
|
match std::fs::read(path) {
|
|
Ok(bytes) => {
|
|
let reg = serde_json::from_slice::<Self>(&bytes).map_err(|e| {
|
|
Error::RegistryParse {
|
|
path: path.display().to_string(),
|
|
message: e.to_string(),
|
|
}
|
|
})?;
|
|
Ok(reg)
|
|
}
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
|
Err(e) => Err(Error::Io {
|
|
context: format!("reading {}", path.display()),
|
|
source: e,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Persist the registry to `path` as pretty-printed JSON. The parent
|
|
/// directory is created on demand.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// IO error on directory creation or write.
|
|
pub fn save(&self, path: &Path) -> Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|e| Error::Io {
|
|
context: format!("creating parent of {}", path.display()),
|
|
source: e,
|
|
})?;
|
|
}
|
|
let json = serde_json::to_string_pretty(self).map_err(|e| Error::RegistryParse {
|
|
path: path.display().to_string(),
|
|
message: e.to_string(),
|
|
})?;
|
|
std::fs::write(path, json).map_err(|e| Error::Io {
|
|
context: format!("writing {}", path.display()),
|
|
source: e,
|
|
})
|
|
}
|
|
|
|
/// Look up an entry by ontology id.
|
|
pub fn lookup(&self, id: &OntologyId) -> Option<&RegistryEntry> {
|
|
self.entries.iter().find(|e| &e.ontology_id == id)
|
|
}
|
|
|
|
/// Register a new entry, replacing any existing one with the same
|
|
/// ontology id.
|
|
pub fn register(&mut self, entry: RegistryEntry) {
|
|
if let Some(existing) = self.entries.iter_mut().find(|e| e.ontology_id == entry.ontology_id) {
|
|
*existing = entry;
|
|
} else {
|
|
self.entries.push(entry);
|
|
}
|
|
}
|
|
|
|
/// Remove an entry by ontology id, returning the removed entry if
|
|
/// it existed.
|
|
pub fn unregister(&mut self, id: &OntologyId) -> Option<RegistryEntry> {
|
|
let idx = self.entries.iter().position(|e| &e.ontology_id == id)?;
|
|
Some(self.entries.remove(idx))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn sample_id(seed: u8) -> OntologyId {
|
|
OntologyId([seed; 32])
|
|
}
|
|
|
|
fn sample_entry(seed: u8, classification: OntologyType) -> RegistryEntry {
|
|
RegistryEntry {
|
|
ontology_id: sample_id(seed),
|
|
oplog_path: PathBuf::from(format!("/tmp/ontology-{seed}.oplog")),
|
|
classification,
|
|
verified_state_root_hex: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn register_and_lookup() {
|
|
let mut reg = OntologyRegistry::empty();
|
|
let entry = sample_entry(1, OntologyType::Type1Protocol);
|
|
let id = entry.ontology_id;
|
|
reg.register(entry);
|
|
assert!(reg.lookup(&id).is_some());
|
|
assert!(reg.lookup(&sample_id(2)).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn register_replaces_existing() {
|
|
let mut reg = OntologyRegistry::empty();
|
|
reg.register(sample_entry(1, OntologyType::Type1Protocol));
|
|
reg.register(sample_entry(1, OntologyType::Type3ProjectSpecific));
|
|
assert_eq!(reg.entries.len(), 1);
|
|
assert_eq!(
|
|
reg.lookup(&sample_id(1)).unwrap().classification,
|
|
OntologyType::Type3ProjectSpecific
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unregister_returns_entry() {
|
|
let mut reg = OntologyRegistry::empty();
|
|
reg.register(sample_entry(1, OntologyType::Type1Protocol));
|
|
let removed = reg.unregister(&sample_id(1)).expect("entry was registered");
|
|
assert_eq!(removed.ontology_id, sample_id(1));
|
|
assert!(reg.entries.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn save_and_load_roundtrip() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let path = tmp.path().join("registry.json");
|
|
|
|
let mut reg = OntologyRegistry::empty();
|
|
reg.register(sample_entry(1, OntologyType::Type1Protocol));
|
|
reg.register(sample_entry(2, OntologyType::Type3ProjectSpecific));
|
|
reg.save(&path).expect("save ok");
|
|
|
|
let loaded = OntologyRegistry::load(&path).expect("load ok");
|
|
assert_eq!(loaded.entries.len(), 2);
|
|
assert_eq!(
|
|
loaded.entries[0].classification,
|
|
OntologyType::Type1Protocol
|
|
);
|
|
assert_eq!(
|
|
loaded.entries[1].classification,
|
|
OntologyType::Type3ProjectSpecific
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn load_missing_file_returns_empty() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let path = tmp.path().join("never_created.json");
|
|
let reg = OntologyRegistry::load(&path).expect("missing is empty");
|
|
assert!(reg.entries.is_empty());
|
|
}
|
|
}
|