1use std::num::NonZeroU64;
2
3use thiserror::Error;
4
5use crate::{
6 EMLError, EMLValueResultExt, NS_KR,
7 io::{EMLElement, EMLElementReader, EMLElementWriter, QualifiedName, collect_struct},
8 utils::{ContestId, PublicationLanguage, StringValue, StringValueData},
9};
10
11#[derive(Debug, Clone)]
13pub struct ListData {
14 pub publish_gender: StringValue<bool>,
16
17 pub publication_language: Option<StringValue<PublicationLanguage>>,
19
20 pub belongs_to_set: Option<StringValue<NonZeroU64>>,
23
24 pub belongs_to_combination: Option<StringValue<ListDataBelongsToCombination>>,
27
28 pub contests: Vec<ListDataContest>,
30}
31
32impl ListData {
33 pub fn new(publish_gender: bool) -> Self {
35 ListData {
36 publish_gender: StringValue::Parsed(publish_gender),
37 publication_language: None,
38 belongs_to_set: None,
39 belongs_to_combination: None,
40 contests: Vec::new(),
41 }
42 }
43
44 pub fn get_publication_language(&self) -> PublicationLanguage {
46 self.publication_language
47 .as_ref()
48 .map(|s| match s {
49 StringValue::Parsed(v) => *v,
50 StringValue::Raw(r) => PublicationLanguage::from_eml_value(r).unwrap_or_default(),
51 })
52 .unwrap_or_default()
53 }
54
55 pub fn with_publication_language(mut self, language: PublicationLanguage) -> Self {
57 self.publication_language = Some(StringValue::Parsed(language));
58 self
59 }
60
61 pub fn with_belongs_to_set(mut self, set_id: NonZeroU64) -> Self {
64 self.belongs_to_set = Some(StringValue::Parsed(set_id));
65 self
66 }
67
68 pub fn with_belongs_to_combination(
71 mut self,
72 combination_id: ListDataBelongsToCombination,
73 ) -> Self {
74 self.belongs_to_combination = Some(StringValue::Parsed(combination_id));
75 self
76 }
77}
78
79impl EMLElement for ListData {
80 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("ListData", Some(NS_KR));
81
82 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
83 let publish_gender = elem.string_value_attr("PublishGender", None)?;
84 let publication_language = elem.string_value_attr_opt("PublicationLanguage")?;
85 let belongs_to_set = elem.string_value_attr_opt("BelongsToSet")?;
86 let belongs_to_combination = elem.string_value_attr_opt("BelongsToCombination")?;
87
88 struct ListDataContests {
90 contests: Option<Vec<ListDataContest>>,
91 }
92
93 let tmp = collect_struct!(elem, ListDataContests {
94 contests as Option: ("Contests", NS_KR) => |elem| {
95 struct Contests {
97 contests: Vec<ListDataContest>,
98 }
99
100 let res = collect_struct!(elem, Contests {
101 contests as Vec: ListDataContest::EML_NAME => |elem| ListDataContest::read_eml(elem)?,
102 });
103
104 res.contests
105 },
106 });
107
108 Ok(ListData {
109 publish_gender,
110 publication_language,
111 belongs_to_set,
112 belongs_to_combination,
113 contests: tmp.contests.unwrap_or_default(),
114 })
115 }
116
117 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
118 let writer = writer
119 .attr("PublishGender", &self.publish_gender.raw())?
120 .attr_opt(
121 "PublicationLanguage",
122 self.publication_language.as_ref().map(|s| s.raw()),
123 )?
124 .attr_opt(
125 "BelongsToSet",
126 self.belongs_to_set.as_ref().map(|s| s.raw()),
127 )?
128 .attr_opt(
129 "BelongsToCombination",
130 self.belongs_to_combination.as_ref().map(|s| s.raw()),
131 )?;
132
133 if self.contests.is_empty() {
134 writer.empty()
135 } else {
136 writer
137 .child(("Contests", NS_KR), |writer| {
138 writer
139 .child_elems(ListDataContest::EML_NAME, &self.contests)?
140 .finish()
141 })?
142 .finish()
143 }
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct ListDataContest {
150 pub id: StringValue<ContestId>,
152
153 pub name: Option<String>,
155}
156
157impl ListDataContest {
158 pub fn new(id: ContestId) -> Self {
160 ListDataContest {
161 id: StringValue::from_value(id),
162 name: None,
163 }
164 }
165
166 pub fn with_name(mut self, name: impl Into<String>) -> Self {
168 self.name = Some(name.into());
169 self
170 }
171}
172
173impl EMLElement for ListDataContest {
174 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contest", Some(NS_KR));
175
176 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
177 Ok(ListDataContest {
178 id: elem.string_value_attr("Id", None)?,
179 name: elem.text_without_children_opt()?,
180 })
181 }
182
183 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
184 let writer = writer.attr("Id", &self.id.raw())?;
185
186 if let Some(name) = &self.name {
187 writer.text(name)?.finish()
188 } else {
189 writer.empty()
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct ListDataBelongsToCombination(String);
197
198impl ListDataBelongsToCombination {
199 pub fn new(combination_id: impl AsRef<str>) -> Result<Self, EMLError> {
201 ListDataBelongsToCombination::parse_from_str(combination_id.as_ref()).wrap_value_error()
202 }
203
204 pub fn value(&self) -> &str {
206 &self.0
207 }
208}
209
210#[derive(Debug, Clone, Error)]
212#[error("Invalid list data belongs to combination type: {0}")]
213pub struct InvalidListDataBelongsToCombinationError(String);
214
215impl StringValueData for ListDataBelongsToCombination {
216 type Error = InvalidListDataBelongsToCombinationError;
217
218 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
219 where
220 Self: Sized,
221 {
222 if s.len() == 1
224 && s.chars()
225 .next()
226 .map(|c| c.is_ascii_alphabetic())
227 .unwrap_or(false)
228 {
229 Ok(ListDataBelongsToCombination(s.to_string()))
230 } else {
231 Err(InvalidListDataBelongsToCombinationError(s.to_string()))
232 }
233 }
234
235 fn to_raw_value(&self) -> String {
236 self.0.clone()
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::io::{EMLRead as _, test_write_eml_element, test_xml_fragment};
244
245 #[test]
246 fn test_list_data_construction() {
247 let list_data = ListData::new(true)
248 .with_publication_language(PublicationLanguage::Frisian)
249 .with_belongs_to_set(NonZeroU64::new(1).unwrap())
250 .with_belongs_to_combination(ListDataBelongsToCombination("A".to_string()));
251
252 assert_eq!(list_data.publish_gender.raw(), "true");
253 assert_eq!(
254 list_data.get_publication_language(),
255 PublicationLanguage::Frisian
256 );
257 assert_eq!(list_data.belongs_to_set.as_ref().unwrap().raw(), "1");
258 assert_eq!(
259 list_data.belongs_to_combination.as_ref().unwrap().raw(),
260 "A"
261 );
262 }
263
264 #[test]
265 fn test_list_data_contest_construction() {
266 let contest =
267 ListDataContest::new(ContestId::new("1234").unwrap()).with_name("Test Contest");
268
269 assert_eq!(contest.id.raw(), "1234");
270 assert_eq!(contest.name.as_ref().unwrap(), "Test Contest");
271 }
272
273 #[test]
274 fn test_list_data_parsing() {
275 let xml = test_xml_fragment(
276 r#"
277 <kr:ListData xmlns:kr="http://www.kiesraad.nl/extensions" PublishGender="true" PublicationLanguage="nl" BelongsToSet="1" BelongsToCombination="A">
278 <kr:Contests>
279 <kr:Contest Id="1234">Test Contest 1</kr:Contest>
280 <kr:Contest Id="5678">Test Contest 2</kr:Contest>
281 </kr:Contests>
282 </kr:ListData>
283 "#,
284 );
285
286 let list_data = ListData::parse_eml(&xml, crate::io::EMLParsingMode::Strict).unwrap();
287
288 assert_eq!(
289 list_data.belongs_to_combination,
290 Some(StringValue::Parsed(ListDataBelongsToCombination(
291 "A".to_string()
292 )))
293 );
294 assert_eq!(
295 list_data.publication_language,
296 Some(StringValue::Parsed(PublicationLanguage::Dutch))
297 );
298 assert_eq!(
299 list_data.belongs_to_set,
300 Some(StringValue::Parsed(NonZeroU64::new(1).unwrap()))
301 );
302
303 assert_eq!(list_data.contests.len(), 2);
304 assert_eq!(list_data.contests[0].id.raw(), "1234");
305 assert_eq!(
306 list_data.contests[0].name.as_deref(),
307 Some("Test Contest 1")
308 );
309 assert_eq!(list_data.contests[1].id.raw(), "5678");
310 assert_eq!(
311 list_data.contests[1].name.as_deref(),
312 Some("Test Contest 2")
313 );
314
315 let xml_output = test_write_eml_element(&list_data, &[NS_KR]).unwrap();
316 assert_eq!(xml_output, xml);
317 }
318
319 #[test]
320 fn test_list_data_simple_parsing() {
321 let xml = test_xml_fragment(
322 r#"<kr:ListData xmlns:kr="http://www.kiesraad.nl/extensions" PublishGender="false"/>"#,
323 );
324
325 let list_data = ListData::parse_eml(&xml, crate::io::EMLParsingMode::Strict).unwrap();
326
327 assert!(!list_data.publish_gender.value().unwrap().into_owned());
328 assert_eq!(
329 list_data.get_publication_language(),
330 PublicationLanguage::Dutch
331 );
332
333 let xml_output = test_write_eml_element(&list_data, &[NS_KR]).unwrap();
334 assert_eq!(xml_output, xml);
335 }
336}