knowledge_base_crud/write/statements/
mod.rs1mod 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}