mod apply;
mod edit;
mod planner;
use crate::Error;
use knowledge_base_models::{EntityId, PropertyId, ReferenceId, StatementId, Value};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ApplyMode {
Preview,
Commit,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StatementBatch {
pub statements: Vec<StatementInput>,
}
impl StatementBatch {
pub fn read(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
let source = fs::read_to_string(path).map_err(|source| Error::Read { path: path.to_path_buf(), source })?;
serde_yaml::from_str(&source).map_err(|source| Error::ParseStatementBatch { path: path.to_path_buf(), source })
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StatementInput {
pub entity: EntityId,
pub property: PropertyId,
pub value: Value,
pub references: Vec<ReferenceId>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StatementResultStatus {
WouldAdd,
Added,
AlreadyPresent,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct StatementResult {
pub index: usize,
pub entity: EntityId,
pub property: PropertyId,
pub statement: StatementId,
pub status: StatementResultStatus,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "outcome", content = "results", rename_all = "snake_case")]
pub enum ApplyStatementsOutcome {
Previewed(Vec<StatementResult>),
Applied(Vec<StatementResult>),
NotApplied(Vec<StatementResult>),
}
impl ApplyStatementsOutcome {
pub fn results(&self) -> &[StatementResult] {
match self {
Self::Previewed(results) | Self::Applied(results) | Self::NotApplied(results) => results,
}
}
pub fn was_applied(&self) -> bool {
matches!(self, Self::Applied(_))
}
pub fn was_rejected(&self) -> bool {
matches!(self, Self::NotApplied(_))
}
}
fn validate_batch(batch: &StatementBatch) -> Result<(), Error> {
if batch.statements.is_empty() {
return Err(Error::InvalidRequest("statements must not be empty".to_owned()));
}
for (offset, statement) in batch.statements.iter().enumerate() {
if statement.references.is_empty() {
return Err(Error::InvalidRequest(format!("statements[{}].references must not be empty", offset + 1)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::StatementBatch;
fn parses(value: &str) -> bool {
serde_yaml::from_str::<StatementBatch>(value).is_ok()
}
#[test]
fn statement_batches_are_strict() {
let valid = r#"
statements:
- entity: Q1
property: P2
value: { type: string, value: Q99 }
references: [R3]
"#;
assert!(parses(valid));
for invalid in [
valid.replace(" references: [R3]\n", " references: [R3]\n qualifiers: []\n"),
valid.replace("statements:\n", "statements:\nunknown: true\n"),
valid.replace(" property: P2\n", " property: P2\n property: P3\n"),
valid.replace("entity: Q1", "entity: P1"),
valid.replace("type: string", "type: unsupported"),
] {
assert!(!parses(&invalid), "invalid batch unexpectedly parsed:\n{invalid}");
}
}
}