pub mod cli;
pub mod mcp;
mod render;
mod store;
pub use store::Store;
use std::collections::{BTreeMap, BTreeSet};
use anyhow::{anyhow, Result};
use triblespace::core::blob::Blob;
use triblespace::core::metadata;
use triblespace::core::query::TriblePattern;
use triblespace::core::repo::BlobStoreGet;
use triblespace::prelude::blobencodings::{UTF8String, WasmCode};
use triblespace::prelude::inlineencodings::Handle;
use triblespace::prelude::*;
pub type TextHandle = Inline<Handle<UTF8String>>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AtlasEntry {
pub id: Id,
pub names: Vec<String>,
pub descriptions: Vec<String>,
pub source_modules: Vec<String>,
pub tags: Vec<Id>,
pub members: Vec<Id>,
}
impl AtlasEntry {
pub fn names_label(&self) -> String {
self.names.join(" / ")
}
}
fn read_text<R: BlobStoreGet>(
reader: &R,
handle: TextHandle,
field: &str,
id: Id,
) -> Result<String> {
let value: anybytes::View<str> = reader
.get(handle)
.map_err(|error| anyhow!("read Atlas {field} for {id:x}: {error:?}"))?;
Ok(value.to_string())
}
fn read_text_variants<R: BlobStoreGet>(
reader: &R,
handles: BTreeSet<TextHandle>,
field: &str,
id: Id,
) -> Result<Vec<String>> {
handles
.into_iter()
.map(|handle| read_text(reader, handle, field, id))
.collect::<Result<BTreeSet<_>>>()
.map(|values| values.into_iter().collect())
}
pub fn validate_known_payloads<R: BlobStoreGet>(reader: &R, facts: &TribleSet) -> Result<()> {
let text_attributes = [
metadata::name.id(),
metadata::description.id(),
metadata::iri.id(),
metadata::source.id(),
metadata::source_module.id(),
]
.into_iter()
.collect::<BTreeSet<_>>();
for fact in facts {
if text_attributes.contains(fact.a()) {
let handle = *fact.v::<Handle<UTF8String>>();
let _: anybytes::View<str> = reader.get(handle).map_err(|error| {
anyhow!(
"read Atlas text payload {}: {error:?}",
hex::encode_upper(handle.raw)
)
})?;
} else if fact.a() == &metadata::value_formatter.id() {
let handle = *fact.v::<Handle<WasmCode>>();
let _: Blob<WasmCode> = reader.get(handle).map_err(|error| {
anyhow!(
"read Atlas value formatter {}: {error:?}",
hex::encode_upper(handle.raw)
)
})?;
}
}
Ok(())
}
pub fn named_entries<R, P>(reader: &R, facts: &P) -> Result<Vec<AtlasEntry>>
where
R: BlobStoreGet,
P: TriblePattern,
{
let mut name_handles = BTreeMap::<Id, BTreeSet<TextHandle>>::new();
for (id, handle) in find!(
(id: Id, handle: TextHandle),
pattern!(facts, [{ ?id @ metadata::name: ?handle }])
) {
name_handles.entry(id).or_default().insert(handle);
}
let mut description_handles = BTreeMap::<Id, BTreeSet<TextHandle>>::new();
for (id, handle) in find!(
(id: Id, handle: TextHandle),
pattern!(facts, [{ ?id @ metadata::description: ?handle }])
) {
description_handles.entry(id).or_default().insert(handle);
}
let mut source_module_handles = BTreeMap::<Id, BTreeSet<TextHandle>>::new();
for (id, handle) in find!(
(id: Id, handle: TextHandle),
pattern!(facts, [{ ?id @ metadata::source_module: ?handle }])
) {
source_module_handles.entry(id).or_default().insert(handle);
}
let mut tags = BTreeMap::<Id, BTreeSet<Id>>::new();
let mut members = BTreeMap::<Id, BTreeSet<Id>>::new();
for (entity, tag) in find!(
(entity: Id, tag: Id),
pattern!(facts, [{ ?entity @ metadata::tag: ?tag }])
) {
tags.entry(entity).or_default().insert(tag);
members.entry(tag).or_default().insert(entity);
}
let mut entries = Vec::with_capacity(name_handles.len());
for (id, handles) in name_handles {
let names = read_text_variants(reader, handles, "name", id)?;
let descriptions = read_text_variants(
reader,
description_handles.remove(&id).unwrap_or_default(),
"description",
id,
)?;
let source_modules = read_text_variants(
reader,
source_module_handles.remove(&id).unwrap_or_default(),
"source module",
id,
)?;
entries.push(AtlasEntry {
id,
names,
descriptions,
source_modules,
tags: tags.remove(&id).unwrap_or_default().into_iter().collect(),
members: members
.remove(&id)
.unwrap_or_default()
.into_iter()
.collect(),
});
}
Ok(entries)
}
pub fn named_entry<R, P>(reader: &R, facts: &P, id: Id) -> Result<Option<AtlasEntry>>
where
R: BlobStoreGet,
P: TriblePattern,
{
let names = read_text_variants(
reader,
find!(
handle: TextHandle,
pattern!(facts, [{ id @ metadata::name: ?handle }])
)
.collect(),
"name",
id,
)?;
if names.is_empty() {
return Ok(None);
}
let descriptions = read_text_variants(
reader,
find!(
handle: TextHandle,
pattern!(facts, [{ id @ metadata::description: ?handle }])
)
.collect(),
"description",
id,
)?;
let source_modules = read_text_variants(
reader,
find!(
handle: TextHandle,
pattern!(facts, [{ id @ metadata::source_module: ?handle }])
)
.collect(),
"source module",
id,
)?;
let tags = find!(
tag: Id,
pattern!(facts, [{ id @ metadata::tag: ?tag }])
)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let members = find!(
member: Id,
pattern!(facts, [{ ?member @ metadata::tag: id }])
)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
Ok(Some(AtlasEntry {
id,
names,
descriptions,
source_modules,
tags,
members,
}))
}
pub fn validate_catalog<R: BlobStoreGet>(reader: &R, facts: &TribleSet) -> Result<()> {
validate_known_payloads(reader, facts)?;
named_entries(reader, facts).map(drop)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn projection_preserves_every_name_and_description_variant() {
let id = Id::new([0x41; 16]).unwrap();
let member = Id::new([0x42; 16]).unwrap();
let mut fragment = Fragment::empty();
let alpha = fragment.put::<UTF8String, _>("Alpha".to_owned());
let beta = fragment.put::<UTF8String, _>("Beta".to_owned());
let first = fragment.put::<UTF8String, _>("First description".to_owned());
let second = fragment.put::<UTF8String, _>("Second description".to_owned());
fragment += entity! { ExclusiveId::force_ref(&id) @
metadata::name: beta,
metadata::description: second,
};
fragment += entity! { ExclusiveId::force_ref(&id) @
metadata::name: alpha,
metadata::description: first,
};
fragment += entity! { ExclusiveId::force_ref(&member) @ metadata::tag: &id };
let mut blobs = fragment.blobs().clone();
let reader = blobs.snapshot().unwrap();
let entries = named_entries(&reader, fragment.facts()).unwrap();
let entry = entries.iter().find(|entry| entry.id == id).unwrap();
assert_eq!(entry.names, ["Alpha", "Beta"]);
assert_eq!(
entry.descriptions,
["First description", "Second description"]
);
assert_eq!(entry.members, [member]);
assert_eq!(
named_entry(&reader, fragment.facts(), id).unwrap(),
Some(entry.clone())
);
}
#[test]
fn ordinary_projection_ignores_unrequested_open_world_payloads() {
let named = Id::new([0x51; 16]).unwrap();
let unrelated = Id::new([0x52; 16]).unwrap();
let mut fragment = Fragment::empty();
let name = fragment.put::<UTF8String, _>("Readable".to_owned());
fragment += entity! { ExclusiveId::force_ref(&named) @ metadata::name: name };
fragment += entity! { ExclusiveId::force_ref(&unrelated) @
metadata::value_formatter: Inline::<Handle<WasmCode>>::new([0x53; 32]),
};
let mut blobs = fragment.blobs().clone();
let reader = blobs.snapshot().unwrap();
let entries = named_entries(&reader, fragment.facts()).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].id, named);
assert!(validate_catalog(&reader, fragment.facts()).is_err());
}
}