knowledge-base-models 0.2.0

Data models for the file-based knowledge base
Documentation
use crate::{EntityTypeId, LocalizedMap, PropertyId, deserialize_optional_non_null};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EntityType {
    pub id: EntityTypeId,
    pub labels: LocalizedMap,
    #[serde(default)]
    pub descriptions: LocalizedMap,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ValueType {
    Entity,
    String,
    Integer,
    Decimal,
    Quantity,
    Boolean,
    Date,
    Datetime,
    Url,
    Coordinate,
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Cardinality {
    One,
    #[default]
    Many,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PropertyUsage {
    Statement,
    Qualifier,
    Both,
}

impl PropertyUsage {
    pub fn allows_statement(self) -> bool {
        matches!(self, Self::Statement | Self::Both)
    }

    pub fn allows_qualifier(self) -> bool {
        matches!(self, Self::Qualifier | Self::Both)
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Property {
    pub id: PropertyId,
    pub labels: LocalizedMap,
    #[serde(default)]
    pub descriptions: LocalizedMap,
    pub subject_types: Vec<EntityTypeId>,
    pub value_type: ValueType,
    pub usage: PropertyUsage,
    #[serde(default, deserialize_with = "deserialize_optional_non_null")]
    pub target_types: Option<Vec<EntityTypeId>>,
    #[serde(default)]
    pub allowed_qualifiers: Vec<PropertyId>,
    #[serde(default)]
    pub cardinality: Cardinality,
    #[serde(default)]
    pub external_ids: BTreeMap<String, Vec<String>>,
}

#[cfg(test)]
mod tests {
    use super::{Property, PropertyUsage};

    fn property(usage: &str) -> String {
        format!("id: P1\nlabels:\n  en:\n    text: population\n    references: [R1]\nsubject_types: [T1]\nvalue_type: integer\nusage: {usage}\n")
    }

    #[test]
    fn property_usage_serializes_and_deserializes_all_variants() {
        for (source, usage) in [
            ("statement", PropertyUsage::Statement),
            ("qualifier", PropertyUsage::Qualifier),
            ("both", PropertyUsage::Both),
        ] {
            let parsed: Property = serde_yaml::from_str(&property(source)).expect("property parses");
            assert_eq!(parsed.usage, usage);
            assert_eq!(serde_yaml::to_string(&usage).expect("usage serializes"), format!("{source}\n"));
        }
    }

    #[test]
    fn property_usage_is_required_and_rejects_unknown_values() {
        let missing = property("statement").replace("usage: statement\n", "");
        assert!(serde_yaml::from_str::<Property>(&missing).is_err());
        assert!(serde_yaml::from_str::<Property>(&property("context")).is_err());
    }

    #[test]
    fn external_ids_default_to_empty_and_preserve_namespaces_and_values() {
        let parsed: Property = serde_yaml::from_str(&property("statement")).expect("property parses");
        assert!(parsed.external_ids.is_empty());

        let source = format!("{}external_ids:\n  wikidata: [P1082, P2046]\n  osm: []\n", property("statement"));
        let parsed: Property = serde_yaml::from_str(&source).expect("property parses");
        assert_eq!(parsed.external_ids["wikidata"], ["P1082", "P2046"]);
        assert!(parsed.external_ids["osm"].is_empty());
        let serialized = serde_yaml::to_string(&parsed).expect("property serializes");
        assert!(serialized.contains("wikidata:\n  - P1082\n  - P2046"));
        assert!(serialized.contains("osm: []"));
    }
}