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, Qualifier, 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    #[serde(default)]
38    pub qualifiers: Vec<Qualifier>,
39    pub references: Vec<ReferenceId>,
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum StatementResultStatus {
45    WouldAdd,
46    Added,
47    AlreadyPresent,
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
51pub struct StatementResult {
52    pub index: usize,
53    pub entity: EntityId,
54    pub property: PropertyId,
55    pub statement: StatementId,
56    pub status: StatementResultStatus,
57}
58
59#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
60#[serde(tag = "outcome", content = "results", rename_all = "snake_case")]
61pub enum ApplyStatementsOutcome {
62    Previewed(Vec<StatementResult>),
63    Applied(Vec<StatementResult>),
64    NotApplied(Vec<StatementResult>),
65}
66
67impl ApplyStatementsOutcome {
68    pub fn results(&self) -> &[StatementResult] {
69        match self {
70            Self::Previewed(results) | Self::Applied(results) | Self::NotApplied(results) => results,
71        }
72    }
73
74    pub fn was_applied(&self) -> bool {
75        matches!(self, Self::Applied(_))
76    }
77
78    pub fn was_rejected(&self) -> bool {
79        matches!(self, Self::NotApplied(_))
80    }
81}
82
83fn validate_batch(batch: &StatementBatch) -> Result<(), Error> {
84    if batch.statements.is_empty() {
85        return Err(Error::InvalidRequest("statements must not be empty".to_owned()));
86    }
87    for (offset, statement) in batch.statements.iter().enumerate() {
88        if statement.references.is_empty() {
89            return Err(Error::InvalidRequest(format!("statements[{}].references must not be empty", offset + 1)));
90        }
91        for (qualifier_offset, qualifier) in statement.qualifiers.iter().enumerate() {
92            if statement.qualifiers[..qualifier_offset].contains(qualifier) {
93                return Err(Error::InvalidRequest(format!(
94                    "statements[{}].qualifiers contains duplicate property/value entry",
95                    offset + 1
96                )));
97            }
98        }
99    }
100    Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::{StatementBatch, validate_batch};
106
107    fn parses(value: &str) -> bool {
108        serde_yaml::from_str::<StatementBatch>(value).is_ok()
109    }
110
111    #[test]
112    fn statement_batches_are_strict() {
113        let valid = r#"
114statements:
115  - entity: Q1
116    property: P2
117    value: { type: string, value: Q99 }
118    references: [R3]
119"#;
120        assert!(parses(valid));
121
122        for invalid in [
123            valid.replace("statements:\n", "statements:\nunknown: true\n"),
124            valid.replace("    property: P2\n", "    property: P2\n    property: P3\n"),
125            valid.replace("entity: Q1", "entity: P1"),
126            valid.replace("type: string", "type: unsupported"),
127        ] {
128            assert!(!parses(&invalid), "invalid batch unexpectedly parsed:\n{invalid}");
129        }
130
131        assert!(parses(&valid.replace("    references: [R3]\n", "    qualifiers: []\n    references: [R3]\n")));
132    }
133
134    #[test]
135    fn statement_batches_reject_duplicate_qualifiers() {
136        let source = r#"
137statements:
138  - entity: Q1
139    property: P1
140    value: { type: integer, value: 7 }
141    qualifiers:
142      - property: P2
143        value: { type: date, value: "2024-01-01" }
144      - property: P2
145        value: { type: date, value: "2024-01-01" }
146    references: [R1]
147"#;
148        let batch: StatementBatch = serde_yaml::from_str(source).expect("manifest parsing is separate from request validation");
149        assert!(validate_batch(&batch).is_err());
150    }
151}