Skip to main content

knowledge_base_models/
lib.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::BTreeMap;
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct IdentifierParseError {
8    value: String,
9    prefix: &'static str,
10}
11
12impl fmt::Display for IdentifierParseError {
13    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
14        write!(
15            formatter,
16            "invalid identifier {:?}; expected canonical {}<positive integer> syntax",
17            self.value, self.prefix
18        )
19    }
20}
21
22impl std::error::Error for IdentifierParseError {}
23
24macro_rules! identifier {
25    ($name:ident, $prefix:literal) => {
26        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
27        #[serde(transparent)]
28        pub struct $name(String);
29
30        impl $name {
31            pub fn as_str(&self) -> &str {
32                &self.0
33            }
34
35            pub fn number(&self) -> u64 {
36                self.0[1..].parse().expect("validated identifiers contain a u64")
37            }
38        }
39
40        impl fmt::Display for $name {
41            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42                formatter.write_str(&self.0)
43            }
44        }
45
46        impl FromStr for $name {
47            type Err = IdentifierParseError;
48
49            fn from_str(value: &str) -> Result<Self, Self::Err> {
50                let digits = value.strip_prefix($prefix).ok_or_else(|| IdentifierParseError {
51                    value: value.to_owned(),
52                    prefix: $prefix,
53                })?;
54                let canonical = !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) && !digits.starts_with('0') && digits.parse::<u64>().is_ok();
55                if !canonical {
56                    return Err(IdentifierParseError {
57                        value: value.to_owned(),
58                        prefix: $prefix,
59                    });
60                }
61                Ok(Self(value.to_owned()))
62            }
63        }
64
65        impl<'de> Deserialize<'de> for $name {
66            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67            where
68                D: Deserializer<'de>,
69            {
70                String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
71            }
72        }
73    };
74}
75
76identifier!(EntityId, "Q");
77identifier!(PropertyId, "P");
78identifier!(ReferenceId, "R");
79identifier!(StatementId, "S");
80identifier!(EntityTypeId, "T");
81
82pub type LocalizedMap = BTreeMap<String, LocalizedText>;
83
84#[derive(Clone, Debug, Deserialize, Serialize)]
85#[serde(deny_unknown_fields)]
86pub struct LocalizedText {
87    pub text: String,
88    pub references: Vec<ReferenceId>,
89}
90
91#[derive(Clone, Debug, Deserialize, Serialize)]
92#[serde(deny_unknown_fields)]
93pub struct EntityType {
94    pub id: EntityTypeId,
95    pub labels: LocalizedMap,
96    #[serde(default)]
97    pub descriptions: LocalizedMap,
98}
99
100#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "lowercase")]
102pub enum ValueType {
103    Entity,
104    String,
105    Integer,
106    Decimal,
107    Boolean,
108    Date,
109    Datetime,
110    Url,
111    Coordinate,
112}
113
114#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(rename_all = "lowercase")]
116pub enum Cardinality {
117    One,
118    #[default]
119    Many,
120}
121
122#[derive(Clone, Debug, Deserialize, Serialize)]
123#[serde(deny_unknown_fields)]
124pub struct Property {
125    pub id: PropertyId,
126    pub labels: LocalizedMap,
127    #[serde(default)]
128    pub descriptions: LocalizedMap,
129    pub subject_types: Vec<EntityTypeId>,
130    pub value_type: ValueType,
131    #[serde(default, deserialize_with = "deserialize_optional_non_null")]
132    pub target_types: Option<Vec<EntityTypeId>>,
133    #[serde(default)]
134    pub allowed_qualifiers: Vec<PropertyId>,
135    #[serde(default)]
136    pub cardinality: Cardinality,
137}
138
139#[derive(Clone, Debug, Deserialize, Serialize)]
140#[serde(deny_unknown_fields)]
141pub struct Classification {
142    pub value: EntityTypeId,
143    pub references: Vec<ReferenceId>,
144}
145
146#[derive(Clone, Debug, Deserialize, Serialize)]
147#[serde(deny_unknown_fields)]
148pub struct Statement {
149    pub id: StatementId,
150    pub property: PropertyId,
151    pub value: Value,
152    #[serde(default)]
153    pub qualifiers: Vec<Qualifier>,
154    pub references: Vec<ReferenceId>,
155}
156
157#[derive(Clone, Debug, Deserialize, Serialize)]
158#[serde(deny_unknown_fields)]
159pub struct Qualifier {
160    pub property: PropertyId,
161    pub value: Value,
162}
163
164#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
165#[serde(tag = "type", rename_all = "lowercase", deny_unknown_fields)]
166pub enum Value {
167    Entity { value: EntityId },
168    String { value: String },
169    Integer { value: i64 },
170    Decimal { value: String },
171    Boolean { value: bool },
172    Date { value: String },
173    Datetime { value: String },
174    Url { value: String },
175    Coordinate { latitude: String, longitude: String },
176}
177
178impl Value {
179    pub fn value_type(&self) -> ValueType {
180        match self {
181            Self::Entity { .. } => ValueType::Entity,
182            Self::String { .. } => ValueType::String,
183            Self::Integer { .. } => ValueType::Integer,
184            Self::Decimal { .. } => ValueType::Decimal,
185            Self::Boolean { .. } => ValueType::Boolean,
186            Self::Date { .. } => ValueType::Date,
187            Self::Datetime { .. } => ValueType::Datetime,
188            Self::Url { .. } => ValueType::Url,
189            Self::Coordinate { .. } => ValueType::Coordinate,
190        }
191    }
192}
193
194#[derive(Clone, Debug, Deserialize, Serialize)]
195#[serde(deny_unknown_fields)]
196pub struct Image {
197    pub url: String,
198    pub attribution: String,
199    #[serde(default, deserialize_with = "deserialize_optional_non_null")]
200    pub attribution_url: Option<String>,
201    pub references: Vec<ReferenceId>,
202}
203
204#[derive(Clone, Debug, Deserialize, Serialize)]
205#[serde(deny_unknown_fields)]
206pub struct Entity {
207    pub id: EntityId,
208    pub labels: LocalizedMap,
209    #[serde(default)]
210    pub descriptions: LocalizedMap,
211    pub entity_types: Vec<Classification>,
212    #[serde(default)]
213    pub images: Vec<Image>,
214    pub statements: Vec<Statement>,
215}
216
217#[derive(Clone, Debug, Deserialize, Serialize)]
218#[serde(deny_unknown_fields)]
219pub struct Reference {
220    pub id: ReferenceId,
221    pub url: String,
222    pub retrieved_at: String,
223    #[serde(default, deserialize_with = "deserialize_optional_non_null")]
224    pub archive_url: Option<String>,
225}
226
227#[derive(Clone, Debug, Deserialize, Serialize)]
228#[serde(deny_unknown_fields)]
229pub struct IdAllocation {
230    pub version: u64,
231    pub next: NextIds,
232}
233
234#[derive(Clone, Debug, Deserialize, Serialize)]
235#[serde(deny_unknown_fields)]
236pub struct NextIds {
237    pub entity: u64,
238    pub property: u64,
239    pub reference: u64,
240    pub entity_type: u64,
241}
242
243fn deserialize_optional_non_null<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
244where
245    D: Deserializer<'de>,
246    T: Deserialize<'de>,
247{
248    T::deserialize(deserializer).map(Some)
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{EntityId, EntityTypeId, PropertyId, ReferenceId, StatementId};
254    use serde::de::DeserializeOwned;
255
256    fn parses<T: DeserializeOwned>(value: &str) -> bool {
257        serde_yaml::from_str::<T>(value).is_ok()
258    }
259
260    #[test]
261    fn typed_identifiers_accept_their_canonical_forms() {
262        assert!(parses::<EntityId>("Q1"));
263        assert!(parses::<PropertyId>("P2"));
264        assert!(parses::<ReferenceId>("R3"));
265        assert!(parses::<StatementId>("S4"));
266        assert!(parses::<EntityTypeId>("T5"));
267    }
268
269    #[test]
270    fn typed_identifiers_reject_noncanonical_forms() {
271        for value in ["Q0", "Q01", "Q-1", "Q", "P1", "q1", "1"] {
272            assert!(!parses::<EntityId>(value), "{value} unexpectedly parsed as an entity identifier");
273        }
274    }
275
276    #[test]
277    fn typed_identifiers_parse_from_strings() {
278        assert_eq!("Q42".parse::<EntityId>().expect("valid identifier").as_str(), "Q42");
279
280        for value in ["Q0", "Q01", "P1", "../Q1", "Q1.yaml"] {
281            assert!(value.parse::<EntityId>().is_err(), "{value} unexpectedly parsed as an entity identifier");
282        }
283    }
284}