knowledge-base-crud 0.1.0

Filesystem CRUD operations for a file-based knowledge base
Documentation
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}");
        }
    }
}