use std::collections::BTreeMap;
use crate::address::ContentAddress;
use crate::archive::Archive;
use crate::codec::CodecError;
use crate::definition::{Definition, EdgeTarget};
pub fn ground(
archive: &Archive,
lens: impl Fn(&Definition) -> Vec<(String, EdgeTarget)>,
) -> Archive {
let nodes = archive
.nodes
.iter()
.map(|node| {
let mut grounded = node.clone();
grounded.edges.extend(lens(node));
grounded
})
.collect();
Archive {
nodes,
connections: archive.connections.clone(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConnectedOntology {
pub name: String,
pub root: ContentAddress,
pub role: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConnectedOntologies(pub Vec<ConnectedOntology>);
impl ConnectedOntologies {
pub fn get(&self, name: &str) -> Option<&ConnectedOntology> {
self.0.iter().find(|c| c.name == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinkError {
UnknownOntology { ontology: String },
MissingPeerArchive { ontology: String },
RootMismatch {
ontology: String,
pinned: ContentAddress,
actual: ContentAddress,
},
AtomAbsent {
ontology: String,
atom: ContentAddress,
},
NotGrounded,
Codec(CodecError),
}
impl core::fmt::Display for LinkError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LinkError::UnknownOntology { ontology } => {
write!(f, "grounding: edge into undeclared ontology {ontology:?}")
}
LinkError::MissingPeerArchive { ontology } => write!(
f,
"grounding: declared ontology {ontology:?} has no supplied peer archive"
),
LinkError::RootMismatch {
ontology,
pinned,
actual,
} => write!(
f,
"grounding: {ontology:?} root skew — pinned {}, supplied {}",
pinned.to_hex(),
actual.to_hex()
),
LinkError::AtomAbsent { ontology, atom } => write!(
f,
"grounding: {ontology:?} holds no atom at {}",
atom.to_hex()
),
LinkError::NotGrounded => write!(f, "grounding: target is local, not a grounded edge"),
LinkError::Codec(e) => write!(f, "grounding: {e}"),
}
}
}
impl std::error::Error for LinkError {}
#[derive(Debug)]
pub struct AtomResolver<'a> {
atoms: BTreeMap<String, BTreeMap<ContentAddress, &'a Definition>>,
}
impl<'a> AtomResolver<'a> {
pub fn new(
manifest: &ConnectedOntologies,
peers: &'a BTreeMap<String, Archive>,
) -> Result<Self, LinkError> {
let mut atoms: BTreeMap<String, BTreeMap<ContentAddress, &'a Definition>> = BTreeMap::new();
for decl in &manifest.0 {
let archive = peers
.get(&decl.name)
.ok_or_else(|| LinkError::MissingPeerArchive {
ontology: decl.name.clone(),
})?;
let actual = archive.root().map_err(LinkError::Codec)?;
if actual != decl.root {
return Err(LinkError::RootMismatch {
ontology: decl.name.clone(),
pinned: decl.root,
actual,
});
}
let mut index: BTreeMap<ContentAddress, &'a Definition> = BTreeMap::new();
for node in &archive.nodes {
index.insert(node.address().map_err(LinkError::Codec)?, node);
}
atoms.insert(decl.name.clone(), index);
}
Ok(Self { atoms })
}
pub fn resolve(&self, target: &EdgeTarget) -> Result<&'a Definition, LinkError> {
let (ontology, atom) = match target {
EdgeTarget::Grounded { ontology, atom } => (ontology, atom),
EdgeTarget::Local(_) => return Err(LinkError::NotGrounded),
};
let index = self
.atoms
.get(ontology)
.ok_or_else(|| LinkError::UnknownOntology {
ontology: ontology.clone(),
})?;
index
.get(atom)
.copied()
.ok_or_else(|| LinkError::AtomAbsent {
ontology: ontology.clone(),
atom: *atom,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synset(name: &str, gloss: &str) -> Definition {
Definition {
kind: "Concept".into(),
name: name.into(),
edges: vec![],
axioms: vec![],
lexical: Some(gloss.into()),
}
}
fn fixture() -> (
BTreeMap<String, Archive>,
ConnectedOntologies,
ContentAddress,
) {
let dog = synset("s-dog", "a domesticated canine");
let atom = dog.address().unwrap();
let english = Archive {
nodes: vec![dog, synset("s-animal", "a living organism")],
connections: vec![],
};
let root = english.root().unwrap();
let mut peers = BTreeMap::new();
peers.insert("english_wordnet".to_string(), english);
let manifest = ConnectedOntologies(vec![ConnectedOntology {
name: "english_wordnet".to_string(),
root,
role: "denotes".to_string(),
}]);
(peers, manifest, atom)
}
#[test]
fn resolves_a_grounded_atom_by_content_address() {
let (peers, manifest, atom) = fixture();
let resolver = AtomResolver::new(&manifest, &peers).unwrap();
let node = resolver
.resolve(&EdgeTarget::Grounded {
ontology: "english_wordnet".to_string(),
atom,
})
.expect("the atom resolves");
assert_eq!(node.name, "s-dog");
assert_eq!(node.lexical.as_deref(), Some("a domesticated canine"));
}
#[test]
fn ground_adds_lens_edges_that_then_resolve() {
let (peers, manifest, atom) = fixture();
let content = Archive {
nodes: vec![Definition {
kind: "Provision".into(),
name: "title-1-§1".into(),
edges: vec![],
axioms: vec![],
lexical: Some("a domesticated canine occurs here".into()),
}],
connections: vec![],
};
let grounded = ground(&content, |_node| {
vec![(
"denotes".to_string(),
EdgeTarget::Grounded {
ontology: "english_wordnet".to_string(),
atom,
},
)]
});
let edge = &grounded.nodes[0].edges[0];
assert_eq!(edge.0, "denotes");
let resolver = AtomResolver::new(&manifest, &peers).unwrap();
let resolved = resolver
.resolve(&edge.1)
.expect("the grounded edge resolves");
assert_eq!(resolved.name, "s-dog");
}
#[test]
fn an_absent_atom_fails_closed() {
let (peers, manifest, _) = fixture();
let resolver = AtomResolver::new(&manifest, &peers).unwrap();
let ghost = ContentAddress::of(b"a synset that was never declared");
assert_eq!(
resolver.resolve(&EdgeTarget::Grounded {
ontology: "english_wordnet".to_string(),
atom: ghost,
}),
Err(LinkError::AtomAbsent {
ontology: "english_wordnet".to_string(),
atom: ghost,
})
);
}
#[test]
fn an_undeclared_ontology_fails_closed() {
let (peers, manifest, atom) = fixture();
let resolver = AtomResolver::new(&manifest, &peers).unwrap();
assert_eq!(
resolver.resolve(&EdgeTarget::Grounded {
ontology: "klingon".to_string(),
atom,
}),
Err(LinkError::UnknownOntology {
ontology: "klingon".to_string(),
})
);
}
#[test]
fn a_root_skew_refuses_to_build() {
let (peers, _, _) = fixture();
let wrong = ConnectedOntologies(vec![ConnectedOntology {
name: "english_wordnet".to_string(),
root: ContentAddress::of(b"some other english version"),
role: "denotes".to_string(),
}]);
match AtomResolver::new(&wrong, &peers) {
Err(LinkError::RootMismatch { ontology, .. }) => {
assert_eq!(ontology, "english_wordnet");
}
other => panic!("expected a RootMismatch skew refusal; got {other:?}"),
}
}
#[test]
fn a_missing_peer_archive_fails_closed() {
let (_, manifest, _) = fixture();
let empty: BTreeMap<String, Archive> = BTreeMap::new();
assert_eq!(
AtomResolver::new(&manifest, &empty).map(|_| ()),
Err(LinkError::MissingPeerArchive {
ontology: "english_wordnet".to_string(),
})
);
}
#[test]
fn a_local_target_is_not_a_grounded_edge() {
let (peers, manifest, _) = fixture();
let resolver = AtomResolver::new(&manifest, &peers).unwrap();
assert_eq!(
resolver.resolve(&EdgeTarget::Local("s-dog".to_string())),
Err(LinkError::NotGrounded)
);
}
}