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}