Skip to main content

knowledge_base_models/
property.rs

1use crate::{EntityTypeId, LocalizedMap, PropertyId, deserialize_optional_non_null};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5#[derive(Clone, Debug, Deserialize, Serialize)]
6#[serde(deny_unknown_fields)]
7pub struct EntityType {
8    pub id: EntityTypeId,
9    pub labels: LocalizedMap,
10    #[serde(default)]
11    pub descriptions: LocalizedMap,
12}
13
14#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "lowercase")]
16pub enum ValueType {
17    Entity,
18    String,
19    Integer,
20    Decimal,
21    Quantity,
22    Boolean,
23    Date,
24    Datetime,
25    Url,
26    Coordinate,
27}
28
29#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(rename_all = "lowercase")]
31pub enum Cardinality {
32    One,
33    #[default]
34    Many,
35}
36
37#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
38#[serde(rename_all = "lowercase")]
39pub enum PropertyUsage {
40    Statement,
41    Qualifier,
42    Both,
43}
44
45impl PropertyUsage {
46    pub fn allows_statement(self) -> bool {
47        matches!(self, Self::Statement | Self::Both)
48    }
49
50    pub fn allows_qualifier(self) -> bool {
51        matches!(self, Self::Qualifier | Self::Both)
52    }
53}
54
55#[derive(Clone, Debug, Deserialize, Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct Property {
58    pub id: PropertyId,
59    pub labels: LocalizedMap,
60    #[serde(default)]
61    pub descriptions: LocalizedMap,
62    pub subject_types: Vec<EntityTypeId>,
63    pub value_type: ValueType,
64    pub usage: PropertyUsage,
65    #[serde(default, deserialize_with = "deserialize_optional_non_null")]
66    pub target_types: Option<Vec<EntityTypeId>>,
67    #[serde(default)]
68    pub allowed_qualifiers: Vec<PropertyId>,
69    #[serde(default)]
70    pub cardinality: Cardinality,
71    #[serde(default)]
72    pub external_ids: BTreeMap<String, Vec<String>>,
73}
74
75#[cfg(test)]
76mod tests {
77    use super::{Property, PropertyUsage};
78
79    fn property(usage: &str) -> String {
80        format!("id: P1\nlabels:\n  en:\n    text: population\n    references: [R1]\nsubject_types: [T1]\nvalue_type: integer\nusage: {usage}\n")
81    }
82
83    #[test]
84    fn property_usage_serializes_and_deserializes_all_variants() {
85        for (source, usage) in [
86            ("statement", PropertyUsage::Statement),
87            ("qualifier", PropertyUsage::Qualifier),
88            ("both", PropertyUsage::Both),
89        ] {
90            let parsed: Property = serde_yaml::from_str(&property(source)).expect("property parses");
91            assert_eq!(parsed.usage, usage);
92            assert_eq!(serde_yaml::to_string(&usage).expect("usage serializes"), format!("{source}\n"));
93        }
94    }
95
96    #[test]
97    fn property_usage_is_required_and_rejects_unknown_values() {
98        let missing = property("statement").replace("usage: statement\n", "");
99        assert!(serde_yaml::from_str::<Property>(&missing).is_err());
100        assert!(serde_yaml::from_str::<Property>(&property("context")).is_err());
101    }
102
103    #[test]
104    fn external_ids_default_to_empty_and_preserve_namespaces_and_values() {
105        let parsed: Property = serde_yaml::from_str(&property("statement")).expect("property parses");
106        assert!(parsed.external_ids.is_empty());
107
108        let source = format!("{}external_ids:\n  wikidata: [P1082, P2046]\n  osm: []\n", property("statement"));
109        let parsed: Property = serde_yaml::from_str(&source).expect("property parses");
110        assert_eq!(parsed.external_ids["wikidata"], ["P1082", "P2046"]);
111        assert!(parsed.external_ids["osm"].is_empty());
112        let serialized = serde_yaml::to_string(&parsed).expect("property serializes");
113        assert!(serialized.contains("wikidata:\n  - P1082\n  - P2046"));
114        assert!(serialized.contains("osm: []"));
115    }
116}