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