knowledge_base_crud/entity/statement/
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, 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}