knowledge_base_crud/entity/
relationships.rs1use 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 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}