Skip to main content

knowledge_base_models/
entity.rs

1use crate::{EntityId, EntityTypeId, LocalizedMap, PropertyId, ReferenceId, StatementId, ValueType};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Deserialize, Serialize)]
5#[serde(deny_unknown_fields)]
6pub struct Classification {
7    pub value: EntityTypeId,
8    pub references: Vec<ReferenceId>,
9}
10
11#[derive(Clone, Debug, Deserialize, Serialize)]
12#[serde(deny_unknown_fields)]
13pub struct Statement {
14    pub id: StatementId,
15    pub property: PropertyId,
16    pub value: Value,
17    #[serde(default)]
18    pub qualifiers: Vec<Qualifier>,
19    pub references: Vec<ReferenceId>,
20}
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct Qualifier {
25    pub property: PropertyId,
26    pub value: Value,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
31pub enum Value {
32    Entity {
33        value: EntityId,
34    },
35    String {
36        value: String,
37    },
38    Integer {
39        value: i64,
40    },
41    Decimal {
42        value: String,
43    },
44    Quantity {
45        amount: String,
46        unit: String,
47    },
48    Boolean {
49        value: bool,
50    },
51    Date {
52        value: String,
53    },
54    Datetime {
55        value: String,
56    },
57    Url {
58        value: String,
59    },
60    Coordinate {
61        latitude: String,
62        longitude: String,
63        #[serde(skip_serializing_if = "Option::is_none")]
64        precision: Option<String>,
65    },
66}
67
68impl Value {
69    pub fn value_type(&self) -> ValueType {
70        match self {
71            Self::Entity { .. } => ValueType::Entity,
72            Self::String { .. } => ValueType::String,
73            Self::Integer { .. } => ValueType::Integer,
74            Self::Decimal { .. } => ValueType::Decimal,
75            Self::Quantity { .. } => ValueType::Quantity,
76            Self::Boolean { .. } => ValueType::Boolean,
77            Self::Date { .. } => ValueType::Date,
78            Self::Datetime { .. } => ValueType::Datetime,
79            Self::Url { .. } => ValueType::Url,
80            Self::Coordinate { .. } => ValueType::Coordinate,
81        }
82    }
83}
84
85#[derive(Clone, Debug, Deserialize, Serialize)]
86#[serde(deny_unknown_fields)]
87pub struct Image {
88    pub url: String,
89    pub alt: String,
90    pub source_url: String,
91    pub creator: String,
92    pub license: String,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub references: Vec<ReferenceId>,
95}
96
97#[derive(Clone, Debug, Deserialize, Serialize)]
98#[serde(deny_unknown_fields)]
99pub struct Entity {
100    pub id: EntityId,
101    pub labels: LocalizedMap,
102    #[serde(default)]
103    pub descriptions: LocalizedMap,
104    pub entity_types: Vec<Classification>,
105    #[serde(default)]
106    pub images: Vec<Image>,
107    pub statements: Vec<Statement>,
108}
109
110#[cfg(test)]
111mod tests {
112    use super::{Image, Value};
113    use crate::{EntityId, ValueType};
114
115    #[test]
116    fn images_use_lossless_metadata_and_optional_references() {
117        let image: Image =
118            serde_yaml::from_str("url: https://example.org/image.jpg\nalt: Example image\nsource_url: https://example.org/source\ncreator: Example creator\nlicense: CC BY 4.0\n")
119                .expect("canonical image parses");
120
121        assert!(image.references.is_empty());
122        assert!(!serde_yaml::to_string(&image).expect("image serializes").contains("references:"));
123
124        let cited: Image = serde_yaml::from_str(
125            "url: https://example.org/image.jpg\nalt: Example image\nsource_url: https://example.org/source\ncreator: Example creator\nlicense: CC BY 4.0\nreferences: [R1]\n",
126        )
127        .expect("cited canonical image parses");
128        assert_eq!(cited.references[0].as_str(), "R1");
129    }
130
131    #[test]
132    fn images_reject_legacy_attribution_fields() {
133        assert!(
134            serde_yaml::from_str::<Image>("url: https://example.org/image.jpg\nattribution: Example Archive\nattribution_url: https://example.org/source\nreferences: [R1]\n")
135                .is_err()
136        );
137    }
138
139    #[test]
140    fn values_round_trip_with_all_supported_shapes() {
141        let values = [
142            (
143                "entity",
144                Value::Entity {
145                    value: "Q1".parse::<EntityId>().unwrap(),
146                },
147            ),
148            ("string", Value::String { value: "Bilecik".to_owned() }),
149            ("integer", Value::Integer { value: 42 }),
150            ("decimal", Value::Decimal { value: "-0.25".to_owned() }),
151            (
152                "quantity",
153                Value::Quantity {
154                    amount: "12.5".to_owned(),
155                    unit: "km".to_owned(),
156                },
157            ),
158            ("boolean", Value::Boolean { value: true }),
159            ("date", Value::Date { value: "2024-02".to_owned() }),
160            (
161                "datetime",
162                Value::Datetime {
163                    value: "2024-02-29T12:34:56Z".to_owned(),
164                },
165            ),
166            (
167                "url",
168                Value::Url {
169                    value: "https://example.org/".to_owned(),
170                },
171            ),
172            (
173                "coordinate",
174                Value::Coordinate {
175                    latitude: "40.1419".to_owned(),
176                    longitude: "29.9793".to_owned(),
177                    precision: Some("10".to_owned()),
178                },
179            ),
180        ];
181
182        for (name, value) in values {
183            let serialized = serde_yaml::to_string(&value).expect("value serializes");
184            let parsed: Value = serde_yaml::from_str(&serialized).expect("serialized value parses");
185            assert_eq!(parsed, value, "{name}");
186            assert_eq!(
187                value.value_type(),
188                match name {
189                    "entity" => ValueType::Entity,
190                    "string" => ValueType::String,
191                    "integer" => ValueType::Integer,
192                    "decimal" => ValueType::Decimal,
193                    "quantity" => ValueType::Quantity,
194                    "boolean" => ValueType::Boolean,
195                    "date" => ValueType::Date,
196                    "datetime" => ValueType::Datetime,
197                    "url" => ValueType::Url,
198                    "coordinate" => ValueType::Coordinate,
199                    _ => unreachable!(),
200                }
201            );
202        }
203    }
204
205    #[test]
206    fn coordinate_precision_defaults_to_none_and_is_omitted_when_serialized() {
207        let value: Value = serde_yaml::from_str("type: coordinate\nlatitude: \"40.1419\"\nlongitude: \"29.9793\"\n").expect("coordinate parses");
208        assert_eq!(
209            value,
210            Value::Coordinate {
211                latitude: "40.1419".to_owned(),
212                longitude: "29.9793".to_owned(),
213                precision: None
214            }
215        );
216        assert!(!serde_yaml::to_string(&value).expect("coordinate serializes").contains("precision:"));
217    }
218}