1use std::collections::BTreeMap;
24
25use crate::address::ContentAddress;
26use crate::archive::Archive;
27use crate::codec::CodecError;
28use crate::definition::{Definition, EdgeTarget};
29
30pub fn ground(
42 archive: &Archive,
43 lens: impl Fn(&Definition) -> Vec<(String, EdgeTarget)>,
44) -> Archive {
45 let nodes = archive
46 .nodes
47 .iter()
48 .map(|node| {
49 let mut grounded = node.clone();
50 grounded.edges.extend(lens(node));
51 grounded
52 })
53 .collect();
54 Archive {
55 nodes,
56 connections: archive.connections.clone(),
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct ConnectedOntology {
65 pub name: String,
67 pub root: ContentAddress,
70 pub role: String,
72}
73
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct ConnectedOntologies(pub Vec<ConnectedOntology>);
78
79impl ConnectedOntologies {
80 pub fn get(&self, name: &str) -> Option<&ConnectedOntology> {
82 self.0.iter().find(|c| c.name == name)
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum LinkError {
89 UnknownOntology { ontology: String },
92 MissingPeerArchive { ontology: String },
96 RootMismatch {
99 ontology: String,
100 pinned: ContentAddress,
101 actual: ContentAddress,
102 },
103 AtomAbsent {
105 ontology: String,
106 atom: ContentAddress,
107 },
108 NotGrounded,
111 Codec(CodecError),
113}
114
115impl core::fmt::Display for LinkError {
116 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117 match self {
118 LinkError::UnknownOntology { ontology } => {
119 write!(f, "grounding: edge into undeclared ontology {ontology:?}")
120 }
121 LinkError::MissingPeerArchive { ontology } => write!(
122 f,
123 "grounding: declared ontology {ontology:?} has no supplied peer archive"
124 ),
125 LinkError::RootMismatch {
126 ontology,
127 pinned,
128 actual,
129 } => write!(
130 f,
131 "grounding: {ontology:?} root skew — pinned {}, supplied {}",
132 pinned.to_hex(),
133 actual.to_hex()
134 ),
135 LinkError::AtomAbsent { ontology, atom } => write!(
136 f,
137 "grounding: {ontology:?} holds no atom at {}",
138 atom.to_hex()
139 ),
140 LinkError::NotGrounded => write!(f, "grounding: target is local, not a grounded edge"),
141 LinkError::Codec(e) => write!(f, "grounding: {e}"),
142 }
143 }
144}
145
146impl std::error::Error for LinkError {}
147
148#[derive(Debug)]
156pub struct AtomResolver<'a> {
157 atoms: BTreeMap<String, BTreeMap<ContentAddress, &'a Definition>>,
159}
160
161impl<'a> AtomResolver<'a> {
162 pub fn new(
169 manifest: &ConnectedOntologies,
170 peers: &'a BTreeMap<String, Archive>,
171 ) -> Result<Self, LinkError> {
172 let mut atoms: BTreeMap<String, BTreeMap<ContentAddress, &'a Definition>> = BTreeMap::new();
173 for decl in &manifest.0 {
174 let archive = peers
175 .get(&decl.name)
176 .ok_or_else(|| LinkError::MissingPeerArchive {
177 ontology: decl.name.clone(),
178 })?;
179 let actual = archive.root().map_err(LinkError::Codec)?;
180 if actual != decl.root {
181 return Err(LinkError::RootMismatch {
182 ontology: decl.name.clone(),
183 pinned: decl.root,
184 actual,
185 });
186 }
187 let mut index: BTreeMap<ContentAddress, &'a Definition> = BTreeMap::new();
188 for node in &archive.nodes {
189 index.insert(node.address().map_err(LinkError::Codec)?, node);
190 }
191 atoms.insert(decl.name.clone(), index);
192 }
193 Ok(Self { atoms })
194 }
195
196 pub fn resolve(&self, target: &EdgeTarget) -> Result<&'a Definition, LinkError> {
202 let (ontology, atom) = match target {
203 EdgeTarget::Grounded { ontology, atom } => (ontology, atom),
204 EdgeTarget::Local(_) => return Err(LinkError::NotGrounded),
205 };
206 let index = self
207 .atoms
208 .get(ontology)
209 .ok_or_else(|| LinkError::UnknownOntology {
210 ontology: ontology.clone(),
211 })?;
212 index
213 .get(atom)
214 .copied()
215 .ok_or_else(|| LinkError::AtomAbsent {
216 ontology: ontology.clone(),
217 atom: *atom,
218 })
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn synset(name: &str, gloss: &str) -> Definition {
227 Definition {
228 kind: "Concept".into(),
229 name: name.into(),
230 edges: vec![],
231 axioms: vec![],
232 lexical: Some(gloss.into()),
233 }
234 }
235
236 fn fixture() -> (
239 BTreeMap<String, Archive>,
240 ConnectedOntologies,
241 ContentAddress,
242 ) {
243 let dog = synset("s-dog", "a domesticated canine");
244 let atom = dog.address().unwrap();
245 let english = Archive {
246 nodes: vec![dog, synset("s-animal", "a living organism")],
247 connections: vec![],
248 };
249 let root = english.root().unwrap();
250 let mut peers = BTreeMap::new();
251 peers.insert("english_wordnet".to_string(), english);
252 let manifest = ConnectedOntologies(vec![ConnectedOntology {
253 name: "english_wordnet".to_string(),
254 root,
255 role: "denotes".to_string(),
256 }]);
257 (peers, manifest, atom)
258 }
259
260 #[test]
261 fn resolves_a_grounded_atom_by_content_address() {
262 let (peers, manifest, atom) = fixture();
263 let resolver = AtomResolver::new(&manifest, &peers).unwrap();
264 let node = resolver
265 .resolve(&EdgeTarget::Grounded {
266 ontology: "english_wordnet".to_string(),
267 atom,
268 })
269 .expect("the atom resolves");
270 assert_eq!(node.name, "s-dog");
271 assert_eq!(node.lexical.as_deref(), Some("a domesticated canine"));
272 }
273
274 #[test]
275 fn ground_adds_lens_edges_that_then_resolve() {
276 let (peers, manifest, atom) = fixture();
281 let content = Archive {
282 nodes: vec![Definition {
283 kind: "Provision".into(),
284 name: "title-1-§1".into(),
285 edges: vec![],
286 axioms: vec![],
287 lexical: Some("a domesticated canine occurs here".into()),
288 }],
289 connections: vec![],
290 };
291 let grounded = ground(&content, |_node| {
294 vec![(
295 "denotes".to_string(),
296 EdgeTarget::Grounded {
297 ontology: "english_wordnet".to_string(),
298 atom,
299 },
300 )]
301 });
302 let edge = &grounded.nodes[0].edges[0];
303 assert_eq!(edge.0, "denotes");
304 let resolver = AtomResolver::new(&manifest, &peers).unwrap();
305 let resolved = resolver
306 .resolve(&edge.1)
307 .expect("the grounded edge resolves");
308 assert_eq!(resolved.name, "s-dog");
309 }
310
311 #[test]
312 fn an_absent_atom_fails_closed() {
313 let (peers, manifest, _) = fixture();
314 let resolver = AtomResolver::new(&manifest, &peers).unwrap();
315 let ghost = ContentAddress::of(b"a synset that was never declared");
317 assert_eq!(
318 resolver.resolve(&EdgeTarget::Grounded {
319 ontology: "english_wordnet".to_string(),
320 atom: ghost,
321 }),
322 Err(LinkError::AtomAbsent {
323 ontology: "english_wordnet".to_string(),
324 atom: ghost,
325 })
326 );
327 }
328
329 #[test]
330 fn an_undeclared_ontology_fails_closed() {
331 let (peers, manifest, atom) = fixture();
332 let resolver = AtomResolver::new(&manifest, &peers).unwrap();
333 assert_eq!(
334 resolver.resolve(&EdgeTarget::Grounded {
335 ontology: "klingon".to_string(),
336 atom,
337 }),
338 Err(LinkError::UnknownOntology {
339 ontology: "klingon".to_string(),
340 })
341 );
342 }
343
344 #[test]
345 fn a_root_skew_refuses_to_build() {
346 let (peers, _, _) = fixture();
349 let wrong = ConnectedOntologies(vec![ConnectedOntology {
350 name: "english_wordnet".to_string(),
351 root: ContentAddress::of(b"some other english version"),
352 role: "denotes".to_string(),
353 }]);
354 match AtomResolver::new(&wrong, &peers) {
355 Err(LinkError::RootMismatch { ontology, .. }) => {
356 assert_eq!(ontology, "english_wordnet");
357 }
358 other => panic!("expected a RootMismatch skew refusal; got {other:?}"),
359 }
360 }
361
362 #[test]
363 fn a_missing_peer_archive_fails_closed() {
364 let (_, manifest, _) = fixture();
365 let empty: BTreeMap<String, Archive> = BTreeMap::new();
366 assert_eq!(
367 AtomResolver::new(&manifest, &empty).map(|_| ()),
368 Err(LinkError::MissingPeerArchive {
369 ontology: "english_wordnet".to_string(),
370 })
371 );
372 }
373
374 #[test]
375 fn a_local_target_is_not_a_grounded_edge() {
376 let (peers, manifest, _) = fixture();
377 let resolver = AtomResolver::new(&manifest, &peers).unwrap();
378 assert_eq!(
379 resolver.resolve(&EdgeTarget::Local("s-dog".to_string())),
380 Err(LinkError::NotGrounded)
381 );
382 }
383}