Skip to main content

knowledge_base_crud/entity/
query.rs

1use super::Entities;
2use crate::Error;
3use knowledge_base_models::{Entity, PropertyId, Value};
4use serde::Serialize;
5use std::collections::BTreeSet;
6use std::fs;
7use std::path::Path;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
10pub struct EntityFilter {
11    pub property: PropertyId,
12    pub value: Value,
13}
14
15#[derive(Clone, Debug, Serialize)]
16pub struct EntitiesPage {
17    pub filters: Vec<EntityFilter>,
18    pub offset: usize,
19    pub limit: usize,
20    pub total: usize,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub next_offset: Option<usize>,
23    pub entities: Vec<Entity>,
24}
25
26impl Entities<'_> {
27    pub fn query(&self, filters: &[EntityFilter], limit: usize, offset: usize) -> Result<EntitiesPage, Error> {
28        if filters.is_empty() {
29            return Err(Error::InvalidRequest("at least one entity filter is required".to_owned()));
30        }
31        if limit == 0 {
32            return Err(Error::InvalidRequest("entity query limit must be greater than zero".to_owned()));
33        }
34
35        let mut entities = load_entities(self.knowledge_base.root())?;
36        entities.retain(|entity| {
37            filters.iter().all(|filter| {
38                entity
39                    .statements
40                    .iter()
41                    .any(|statement| statement.property == filter.property && statement.value == filter.value)
42            })
43        });
44        entities.sort_by_key(|entity| entity.id.number());
45
46        let total = entities.len();
47        let entities = entities.into_iter().skip(offset).take(limit).collect::<Vec<_>>();
48        let returned_end = offset.saturating_add(entities.len());
49        let next_offset = (returned_end < total).then_some(returned_end);
50
51        Ok(EntitiesPage {
52            filters: filters.to_vec(),
53            offset,
54            limit,
55            total,
56            next_offset,
57            entities,
58        })
59    }
60}
61
62fn load_entities(root: &Path) -> Result<Vec<Entity>, Error> {
63    let directory = root.join("entities");
64    let entries = fs::read_dir(&directory).map_err(|source| Error::Read { path: directory.clone(), source })?;
65    let mut paths = Vec::new();
66    for entry in entries {
67        let entry = entry.map_err(|source| Error::Read { path: directory.clone(), source })?;
68        let path = entry.path();
69        let file_type = entry.file_type().map_err(|source| Error::Read { path: path.clone(), source })?;
70        if file_type.is_file() && path.extension().and_then(|extension| extension.to_str()) == Some("yaml") {
71            paths.push(path);
72        }
73    }
74    paths.sort();
75
76    let mut loaded = Vec::with_capacity(paths.len());
77    for path in paths {
78        let source = fs::read_to_string(&path).map_err(|source| Error::Read { path: path.clone(), source })?;
79        let entity: Entity = serde_yaml::from_str(&source).map_err(|source| Error::ParseEntity { path: path.clone(), source })?;
80        loaded.push((path, entity));
81    }
82
83    let mut ids = BTreeSet::new();
84    for (_, entity) in &loaded {
85        if !ids.insert(entity.id.clone()) {
86            return Err(Error::InvalidRepository(format!("duplicate entity identifier {}", entity.id)));
87        }
88    }
89
90    let mut entities = Vec::with_capacity(loaded.len());
91    for (path, entity) in loaded {
92        let file_id = path.file_stem().and_then(|name| name.to_str());
93        if file_id != Some(entity.id.as_str()) {
94            return Err(Error::InvalidRepository(format!(
95                "entity file {} declares identifier {} instead of {}",
96                path.display(),
97                entity.id,
98                file_id.unwrap_or("<non-UTF-8 filename>")
99            )));
100        }
101        entities.push(entity);
102    }
103    Ok(entities)
104}