use std::collections::HashMap;
use kopitiam_ontology::{Entity, EntityId, EntityKind, Relationship, RelationshipId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SemanticGraph {
entities: HashMap<EntityId, Entity>,
relationships: HashMap<RelationshipId, Relationship>,
#[serde(default)]
outgoing: HashMap<EntityId, Vec<RelationshipId>>,
#[serde(default)]
incoming: HashMap<EntityId, Vec<RelationshipId>>,
}
impl SemanticGraph {
pub fn new() -> Self {
Self::default()
}
pub fn insert_entity(&mut self, entity: Entity) -> EntityId {
let id = entity.id;
self.entities.insert(id, entity);
id
}
pub fn insert_relationship(&mut self, relationship: Relationship) -> RelationshipId {
let id = relationship.id;
self.outgoing.entry(relationship.from).or_default().push(id);
self.incoming.entry(relationship.to).or_default().push(id);
self.relationships.insert(id, relationship);
id
}
pub fn extend(
&mut self,
entities: impl IntoIterator<Item = Entity>,
relationships: impl IntoIterator<Item = Relationship>,
) {
for entity in entities {
self.insert_entity(entity);
}
for relationship in relationships {
self.insert_relationship(relationship);
}
}
pub fn entity(&self, id: EntityId) -> Option<&Entity> {
self.entities.get(&id)
}
pub fn relationship(&self, id: RelationshipId) -> Option<&Relationship> {
self.relationships.get(&id)
}
pub fn entity_count(&self) -> usize {
self.entities.len()
}
pub fn relationship_count(&self) -> usize {
self.relationships.len()
}
pub fn entities(&self) -> impl Iterator<Item = &Entity> {
self.entities.values()
}
pub fn entities_of_kind(&self, kind: EntityKind) -> impl Iterator<Item = &Entity> {
self.entities.values().filter(move |e| e.kind == kind)
}
pub fn relationships_from(&self, id: EntityId) -> impl Iterator<Item = &Relationship> {
self.outgoing
.get(&id)
.into_iter()
.flatten()
.filter_map(move |rel_id| self.relationships.get(rel_id))
}
pub fn relationships_to(&self, id: EntityId) -> impl Iterator<Item = &Relationship> {
self.incoming
.get(&id)
.into_iter()
.flatten()
.filter_map(move |rel_id| self.relationships.get(rel_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use kopitiam_ontology::RelationshipKind;
#[test]
fn inserts_and_queries_entities() {
let mut graph = SemanticGraph::new();
let a = Entity::new(EntityKind::Artifact, "kopitiam-knowledge", "test");
let b = Entity::new(EntityKind::Symbol, "SemanticGraph", "test");
let (a_id, b_id) = (a.id, b.id);
graph.extend([a, b], []);
assert_eq!(graph.entity_count(), 2);
assert_eq!(graph.entities_of_kind(EntityKind::Symbol).count(), 1);
assert!(graph.entity(a_id).is_some());
assert!(graph.entity(b_id).is_some());
}
#[test]
fn traverses_relationships_in_both_directions() {
let mut graph = SemanticGraph::new();
let a = Entity::new(EntityKind::Symbol, "SemanticGraph", "test");
let b = Entity::new(EntityKind::Artifact, "kopitiam-knowledge", "test");
let (a_id, b_id) = (a.id, b.id);
graph.insert_entity(a);
graph.insert_entity(b);
graph.insert_relationship(Relationship::new(a_id, b_id, RelationshipKind::LocatedIn));
let out: Vec<_> = graph.relationships_from(a_id).collect();
assert_eq!(out.len(), 1);
assert_eq!(out[0].kind, RelationshipKind::LocatedIn);
let inn: Vec<_> = graph.relationships_to(b_id).collect();
assert_eq!(inn.len(), 1);
assert_eq!(inn[0].from, a_id);
}
#[test]
fn round_trips_through_json() {
let mut graph = SemanticGraph::new();
let a = Entity::new(EntityKind::Artifact, "a", "test");
let b = Entity::new(EntityKind::Symbol, "b", "test");
let (a_id, b_id) = (a.id, b.id);
graph.extend([a, b], [Relationship::new(a_id, b_id, RelationshipKind::DependsOn)]);
let json = serde_json::to_string(&graph).unwrap();
let back: SemanticGraph = serde_json::from_str(&json).unwrap();
assert_eq!(back.entity_count(), 2);
assert_eq!(back.relationship_count(), 1);
assert_eq!(back.relationships_from(a_id).count(), 1);
}
}