Skip to main content

knowledge_base_crud/write/statements/
mod.rs

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