knowledge_base_crud/entity/statement/
mod.rs1mod 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}