Skip to main content

knowledge_base_crud/entity/statement/
mod.rs

1mod apply;
2mod edit;
3mod planner;
4
5use crate::Error;
6use knowledge_base_models::{EntityId, PropertyId, ReferenceId, StatementId, Value};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum ApplyMode {
13    Preview,
14    Commit,
15}
16
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(deny_unknown_fields)]
19pub struct StatementBatch {
20    pub statements: Vec<StatementInput>,
21}
22
23impl StatementBatch {
24    pub fn read(path: impl AsRef<Path>) -> Result<Self, Error> {
25        let path = path.as_ref();
26        let source = fs::read_to_string(path).map_err(|source| Error::Read { path: path.to_path_buf(), source })?;
27        serde_yaml::from_str(&source).map_err(|source| Error::ParseStatementBatch { path: path.to_path_buf(), source })
28    }
29}
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33pub struct StatementInput {
34    pub entity: EntityId,
35    pub property: PropertyId,
36    pub value: Value,
37    pub references: Vec<ReferenceId>,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum StatementResultStatus {
43    WouldAdd,
44    Added,
45    AlreadyPresent,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
49pub struct StatementResult {
50    pub index: usize,
51    pub entity: EntityId,
52    pub property: PropertyId,
53    pub statement: StatementId,
54    pub status: StatementResultStatus,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
58#[serde(tag = "outcome", content = "results", rename_all = "snake_case")]
59pub enum ApplyStatementsOutcome {
60    Previewed(Vec<StatementResult>),
61    Applied(Vec<StatementResult>),
62    NotApplied(Vec<StatementResult>),
63}
64
65impl ApplyStatementsOutcome {
66    pub fn results(&self) -> &[StatementResult] {
67        match self {
68            Self::Previewed(results) | Self::Applied(results) | Self::NotApplied(results) => results,
69        }
70    }
71
72    pub fn was_applied(&self) -> bool {
73        matches!(self, Self::Applied(_))
74    }
75
76    pub fn was_rejected(&self) -> bool {
77        matches!(self, Self::NotApplied(_))
78    }
79}
80
81fn validate_batch(batch: &StatementBatch) -> Result<(), Error> {
82    if batch.statements.is_empty() {
83        return Err(Error::InvalidRequest("statements must not be empty".to_owned()));
84    }
85    for (offset, statement) in batch.statements.iter().enumerate() {
86        if statement.references.is_empty() {
87            return Err(Error::InvalidRequest(format!("statements[{}].references must not be empty", offset + 1)));
88        }
89    }
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::StatementBatch;
96
97    fn parses(value: &str) -> bool {
98        serde_yaml::from_str::<StatementBatch>(value).is_ok()
99    }
100
101    #[test]
102    fn statement_batches_are_strict() {
103        let valid = r#"
104statements:
105  - entity: Q1
106    property: P2
107    value: { type: string, value: Q99 }
108    references: [R3]
109"#;
110        assert!(parses(valid));
111
112        for invalid in [
113            valid.replace("    references: [R3]\n", "    references: [R3]\n    qualifiers: []\n"),
114            valid.replace("statements:\n", "statements:\nunknown: true\n"),
115            valid.replace("    property: P2\n", "    property: P2\n    property: P3\n"),
116            valid.replace("entity: Q1", "entity: P1"),
117            valid.replace("type: string", "type: unsupported"),
118        ] {
119            assert!(!parses(&invalid), "invalid batch unexpectedly parsed:\n{invalid}");
120        }
121    }
122}