use std::num::NonZeroU64;
use thiserror::Error;
use crate::{
EMLError, EMLValueResultExt, NS_KR,
io::{EMLElement, EMLElementReader, EMLElementWriter, QualifiedName, collect_struct},
utils::{ContestId, PublicationLanguage, StringValue, StringValueData},
};
#[derive(Debug, Clone)]
pub struct ListData {
pub publish_gender: StringValue<bool>,
pub publication_language: Option<StringValue<PublicationLanguage>>,
pub belongs_to_set: Option<StringValue<NonZeroU64>>,
pub belongs_to_combination: Option<StringValue<ListDataBelongsToCombination>>,
pub contests: Vec<ListDataContest>,
}
impl ListData {
pub fn new(publish_gender: bool) -> Self {
ListData {
publish_gender: StringValue::Parsed(publish_gender),
publication_language: None,
belongs_to_set: None,
belongs_to_combination: None,
contests: Vec::new(),
}
}
pub fn get_publication_language(&self) -> PublicationLanguage {
self.publication_language
.as_ref()
.map(|s| match s {
StringValue::Parsed(v) => *v,
StringValue::Raw(r) => PublicationLanguage::from_eml_value(r).unwrap_or_default(),
})
.unwrap_or_default()
}
pub fn with_publication_language(mut self, language: PublicationLanguage) -> Self {
self.publication_language = Some(StringValue::Parsed(language));
self
}
pub fn with_belongs_to_set(mut self, set_id: NonZeroU64) -> Self {
self.belongs_to_set = Some(StringValue::Parsed(set_id));
self
}
pub fn with_belongs_to_combination(
mut self,
combination_id: ListDataBelongsToCombination,
) -> Self {
self.belongs_to_combination = Some(StringValue::Parsed(combination_id));
self
}
}
impl EMLElement for ListData {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("ListData", Some(NS_KR));
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
let publish_gender = elem.string_value_attr("PublishGender", None)?;
let publication_language = elem.string_value_attr_opt("PublicationLanguage")?;
let belongs_to_set = elem.string_value_attr_opt("BelongsToSet")?;
let belongs_to_combination = elem.string_value_attr_opt("BelongsToCombination")?;
struct ListDataContests {
contests: Option<Vec<ListDataContest>>,
}
let tmp = collect_struct!(elem, ListDataContests {
contests as Option: ("Contests", NS_KR) => |elem| {
struct Contests {
contests: Vec<ListDataContest>,
}
let res = collect_struct!(elem, Contests {
contests as Vec: ListDataContest::EML_NAME => |elem| ListDataContest::read_eml(elem)?,
});
res.contests
},
});
Ok(ListData {
publish_gender,
publication_language,
belongs_to_set,
belongs_to_combination,
contests: tmp.contests.unwrap_or_default(),
})
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
let writer = writer
.attr("PublishGender", &self.publish_gender.raw())?
.attr_opt(
"PublicationLanguage",
self.publication_language.as_ref().map(|s| s.raw()),
)?
.attr_opt(
"BelongsToSet",
self.belongs_to_set.as_ref().map(|s| s.raw()),
)?
.attr_opt(
"BelongsToCombination",
self.belongs_to_combination.as_ref().map(|s| s.raw()),
)?;
if self.contests.is_empty() {
writer.empty()
} else {
writer
.child(("Contests", NS_KR), |writer| {
writer
.child_elems(ListDataContest::EML_NAME, &self.contests)?
.finish()
})?
.finish()
}
}
}
#[derive(Debug, Clone)]
pub struct ListDataContest {
pub id: StringValue<ContestId>,
pub name: Option<String>,
}
impl ListDataContest {
pub fn new(id: ContestId) -> Self {
ListDataContest {
id: StringValue::from_value(id),
name: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
impl EMLElement for ListDataContest {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contest", Some(NS_KR));
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
Ok(ListDataContest {
id: elem.string_value_attr("Id", None)?,
name: elem.text_without_children_opt()?,
})
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
let writer = writer.attr("Id", &self.id.raw())?;
if let Some(name) = &self.name {
writer.text(name)?.finish()
} else {
writer.empty()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListDataBelongsToCombination(String);
impl ListDataBelongsToCombination {
pub fn new(combination_id: impl AsRef<str>) -> Result<Self, EMLError> {
ListDataBelongsToCombination::parse_from_str(combination_id.as_ref()).wrap_value_error()
}
pub fn value(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Error)]
#[error("Invalid list data belongs to combination type: {0}")]
pub struct InvalidListDataBelongsToCombinationError(String);
impl StringValueData for ListDataBelongsToCombination {
type Error = InvalidListDataBelongsToCombinationError;
fn parse_from_str(s: &str) -> Result<Self, Self::Error>
where
Self: Sized,
{
if s.len() == 1
&& s.chars()
.next()
.map(|c| c.is_ascii_alphabetic())
.unwrap_or(false)
{
Ok(ListDataBelongsToCombination(s.to_string()))
} else {
Err(InvalidListDataBelongsToCombinationError(s.to_string()))
}
}
fn to_raw_value(&self) -> String {
self.0.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::{EMLRead as _, test_write_eml_element, test_xml_fragment};
#[test]
fn test_list_data_construction() {
let list_data = ListData::new(true)
.with_publication_language(PublicationLanguage::Frisian)
.with_belongs_to_set(NonZeroU64::new(1).unwrap())
.with_belongs_to_combination(ListDataBelongsToCombination("A".to_string()));
assert_eq!(list_data.publish_gender.raw(), "true");
assert_eq!(
list_data.get_publication_language(),
PublicationLanguage::Frisian
);
assert_eq!(list_data.belongs_to_set.as_ref().unwrap().raw(), "1");
assert_eq!(
list_data.belongs_to_combination.as_ref().unwrap().raw(),
"A"
);
}
#[test]
fn test_list_data_contest_construction() {
let contest =
ListDataContest::new(ContestId::new("1234").unwrap()).with_name("Test Contest");
assert_eq!(contest.id.raw(), "1234");
assert_eq!(contest.name.as_ref().unwrap(), "Test Contest");
}
#[test]
fn test_list_data_parsing() {
let xml = test_xml_fragment(
r#"
<kr:ListData xmlns:kr="http://www.kiesraad.nl/extensions" PublishGender="true" PublicationLanguage="nl" BelongsToSet="1" BelongsToCombination="A">
<kr:Contests>
<kr:Contest Id="1234">Test Contest 1</kr:Contest>
<kr:Contest Id="5678">Test Contest 2</kr:Contest>
</kr:Contests>
</kr:ListData>
"#,
);
let list_data = ListData::parse_eml(&xml, crate::io::EMLParsingMode::Strict).unwrap();
assert_eq!(
list_data.belongs_to_combination,
Some(StringValue::Parsed(ListDataBelongsToCombination(
"A".to_string()
)))
);
assert_eq!(
list_data.publication_language,
Some(StringValue::Parsed(PublicationLanguage::Dutch))
);
assert_eq!(
list_data.belongs_to_set,
Some(StringValue::Parsed(NonZeroU64::new(1).unwrap()))
);
assert_eq!(list_data.contests.len(), 2);
assert_eq!(list_data.contests[0].id.raw(), "1234");
assert_eq!(
list_data.contests[0].name.as_deref(),
Some("Test Contest 1")
);
assert_eq!(list_data.contests[1].id.raw(), "5678");
assert_eq!(
list_data.contests[1].name.as_deref(),
Some("Test Contest 2")
);
let xml_output = test_write_eml_element(&list_data, &[NS_KR]).unwrap();
assert_eq!(xml_output, xml);
}
#[test]
fn test_list_data_simple_parsing() {
let xml = test_xml_fragment(
r#"<kr:ListData xmlns:kr="http://www.kiesraad.nl/extensions" PublishGender="false"/>"#,
);
let list_data = ListData::parse_eml(&xml, crate::io::EMLParsingMode::Strict).unwrap();
assert!(!list_data.publish_gender.value().unwrap().into_owned());
assert_eq!(
list_data.get_publication_language(),
PublicationLanguage::Dutch
);
let xml_output = test_write_eml_element(&list_data, &[NS_KR]).unwrap();
assert_eq!(xml_output, xml);
}
}