Skip to main content

knowledge_base_crud/entity/
relationships.rs

1use super::Entities;
2use crate::{Error, resource};
3use knowledge_base_models::{Entity, EntityId, PropertyId, StatementId, Value};
4use serde::Serialize;
5use std::collections::BTreeMap;
6use std::fs;
7use std::path::Path;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
10pub struct EntityRelationshipsPage {
11    pub entity: EntityId,
12    pub offset: usize,
13    pub limit: usize,
14    pub total: usize,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub next_offset: Option<usize>,
17    pub relationships: Vec<EntityRelationship>,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
21pub struct EntityRelationship {
22    pub direction: RelationshipDirection,
23    pub entity: RelatedEntity,
24    pub property: PropertyId,
25    pub statement: StatementId,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
29pub struct RelatedEntity {
30    pub id: EntityId,
31    pub labels: BTreeMap<String, String>,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
35#[serde(rename_all = "lowercase")]
36pub enum RelationshipDirection {
37    Incoming,
38    Outgoing,
39    #[serde(rename = "self")]
40    SelfReference,
41}
42
43#[derive(Clone, Debug)]
44struct Edge {
45    source: EntityId,
46    property: PropertyId,
47    statement: StatementId,
48    target: EntityId,
49}
50
51impl Entities<'_> {
52    pub fn relationships(&self, id: &EntityId, limit: usize, offset: usize) -> Result<EntityRelationshipsPage, Error> {
53        if limit == 0 {
54            return Err(Error::InvalidRequest("relationship limit must be greater than zero".to_owned()));
55        }
56
57        // Resolve the canonical file first so a missing requested entity remains a
58        // normal resource-read error, even if another file declares the same ID.
59        let requested_path = resource::path(self.knowledge_base.root(), "entities", id.as_str(), "yaml");
60        let requested = parse_entity(&requested_path)?;
61        if &requested.id != id {
62            return Err(Error::InvalidRepository(format!(
63                "entity file {} declares identifier {} instead of {}",
64                requested_path.display(),
65                requested.id,
66                id
67            )));
68        }
69
70        let entities = load_entities(self.knowledge_base.root())?;
71        let mut index = BTreeMap::new();
72        for entity in &entities {
73            if index.insert(entity.id.clone(), entity).is_some() {
74                return Err(Error::InvalidRepository(format!("duplicate entity identifier {}", entity.id)));
75            }
76        }
77
78        let mut edges = Vec::new();
79        for entity in &entities {
80            for statement in &entity.statements {
81                let Value::Entity { value: target } = &statement.value else {
82                    continue;
83                };
84                if &entity.id == id || target == id {
85                    edges.push(Edge {
86                        source: entity.id.clone(),
87                        property: statement.property.clone(),
88                        statement: statement.id.clone(),
89                        target: target.clone(),
90                    });
91                }
92            }
93        }
94        edges.sort_by_key(|edge| (edge.source.number(), edge.property.number(), edge.statement.number(), edge.target.number()));
95
96        let total = edges.len();
97        let relationships = edges
98            .into_iter()
99            .skip(offset)
100            .take(limit)
101            .map(|edge| relationship(id, &index, edge))
102            .collect::<Result<Vec<_>, _>>()?;
103        let returned_end = offset.saturating_add(relationships.len());
104        let next_offset = (returned_end < total).then_some(returned_end);
105
106        Ok(EntityRelationshipsPage {
107            entity: id.clone(),
108            offset,
109            limit,
110            total,
111            next_offset,
112            relationships,
113        })
114    }
115}
116
117fn load_entities(root: &Path) -> Result<Vec<Entity>, Error> {
118    let directory = root.join("entities");
119    let entries = fs::read_dir(&directory).map_err(|source| Error::Read { path: directory.clone(), source })?;
120    let mut paths = Vec::new();
121    for entry in entries {
122        let entry = entry.map_err(|source| Error::Read { path: directory.clone(), source })?;
123        let path = entry.path();
124        let file_type = entry.file_type().map_err(|source| Error::Read { path: path.clone(), source })?;
125        if file_type.is_file() && path.extension().and_then(|extension| extension.to_str()) == Some("yaml") {
126            paths.push(path);
127        }
128    }
129    paths.sort();
130    paths.iter().map(|path| parse_entity(path.as_path())).collect()
131}
132
133fn parse_entity(path: &Path) -> Result<Entity, Error> {
134    let source = fs::read_to_string(path).map_err(|source| Error::Read { path: path.to_path_buf(), source })?;
135    serde_yaml::from_str(&source).map_err(|source| Error::ParseEntity { path: path.to_path_buf(), source })
136}
137
138fn relationship(id: &EntityId, index: &BTreeMap<EntityId, &Entity>, edge: Edge) -> Result<EntityRelationship, Error> {
139    let (direction, related_id) = if &edge.source == id && &edge.target == id {
140        (RelationshipDirection::SelfReference, id)
141    } else if &edge.source == id {
142        (RelationshipDirection::Outgoing, &edge.target)
143    } else {
144        (RelationshipDirection::Incoming, &edge.source)
145    };
146    let related = index
147        .get(related_id)
148        .ok_or_else(|| Error::InvalidRepository(format!("relationship {} on {} targets missing entity {}", edge.statement, edge.source, edge.target)))?;
149    let labels = related.labels.iter().map(|(language, label)| (language.clone(), label.text.clone())).collect();
150
151    Ok(EntityRelationship {
152        direction,
153        entity: RelatedEntity { id: related_id.clone(), labels },
154        property: edge.property,
155        statement: edge.statement,
156    })
157}