use std::str::FromStr;
use crate::{
EML_SCHEMA_VERSION, EMLError, EMLErrorKind, EMLResultExt as _, NS_EML,
common::ElectionDomain,
documents::{
candidate_lists::{CandidateLists, CandidateListsElectionIdentifier, CandidateListsType},
election_count::{CountType, ElectionCount, ElectionCountElectionIdentifier},
election_definition::{
EML_ELECTION_DEFINITION_ID, ElectionDefinition, ElectionDefinitionElectionIdentifier,
},
election_result::{
EML_ELECTION_RESULT_ID, ElectionResult, ElectionResultElectionIdentifier,
},
polling_stations::{
EML_POLLING_STATIONS_ID, PollingStations, PollingStationsElectionIdentifier,
},
},
io::{EMLElement, EMLElementReader, EMLElementWriter, QualifiedName},
utils::{ElectionCategory, ElectionId, ElectionSubcategory, StringValue, XsDate},
};
pub mod candidate_lists;
pub mod election_count;
pub mod election_definition;
pub mod election_result;
pub mod polling_stations;
#[derive(Debug, Clone)]
pub enum EML {
ElectionDefinition(Box<ElectionDefinition>),
PollingStations(Box<PollingStations>),
CandidateLists(Box<CandidateLists>),
ElectionCount(Box<ElectionCount>),
ElectionResult(Box<ElectionResult>),
}
impl EML {
pub fn to_eml_id(&self) -> &'static str {
match self {
EML::ElectionDefinition(_) => EML_ELECTION_DEFINITION_ID,
EML::PollingStations(_) => EML_POLLING_STATIONS_ID,
EML::CandidateLists(cl) => cl.lists_type.to_eml_id(),
EML::ElectionCount(c) => c.count_type.to_eml_id(),
EML::ElectionResult(_) => EML_ELECTION_RESULT_ID,
}
}
pub fn to_friendly_name(&self) -> &'static str {
match self {
EML::ElectionDefinition(_) => "Election Definition",
EML::PollingStations(_) => "Polling Stations",
EML::CandidateLists(cl) => cl.lists_type.to_friendly_name(),
EML::ElectionCount(c) => c.count_type.to_friendly_name(),
EML::ElectionResult(_) => "Result",
}
}
pub fn from_election_definition_doc(ed: ElectionDefinition) -> Self {
EML::ElectionDefinition(Box::new(ed))
}
pub fn is_election_definition_doc(&self) -> bool {
matches!(self, EML::ElectionDefinition(_))
}
pub fn as_election_definition_doc(&self) -> Option<&ElectionDefinition> {
match self {
EML::ElectionDefinition(ed) => Some(ed),
_ => None,
}
}
pub fn from_polling_stations_doc(ps: PollingStations) -> Self {
EML::PollingStations(Box::new(ps))
}
pub fn is_polling_stations_doc(&self) -> bool {
matches!(self, EML::PollingStations(_))
}
pub fn as_polling_stations_doc(&self) -> Option<&PollingStations> {
match self {
EML::PollingStations(ps) => Some(ps),
_ => None,
}
}
pub fn from_candidate_lists_doc(cl: CandidateLists) -> Self {
EML::CandidateLists(Box::new(cl))
}
pub fn is_candidate_lists_doc(&self) -> bool {
matches!(self, EML::CandidateLists(_))
}
pub fn as_candidate_lists_doc(&self) -> Option<&CandidateLists> {
match self {
EML::CandidateLists(cl) => Some(cl),
_ => None,
}
}
pub fn from_count_doc(count: ElectionCount) -> Self {
EML::ElectionCount(Box::new(count))
}
pub fn is_count_doc(&self) -> bool {
matches!(self, EML::ElectionCount(_))
}
pub fn as_count_doc(&self) -> Option<&ElectionCount> {
match self {
EML::ElectionCount(count) => Some(count),
_ => None,
}
}
pub fn from_result_doc(result: ElectionResult) -> Self {
EML::ElectionResult(Box::new(result))
}
pub fn is_result_doc(&self) -> bool {
matches!(self, EML::ElectionResult(_))
}
pub fn as_result_doc(&self) -> Option<&ElectionResult> {
match self {
EML::ElectionResult(result) => Some(result),
_ => None,
}
}
}
impl EMLElement for EML {
const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("EML", Some(NS_EML));
fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
accepted_root(elem)?;
let document_id = elem.attribute_value_req(("Id", None))?;
Ok(match document_id.as_ref() {
EML_ELECTION_DEFINITION_ID => {
EML::ElectionDefinition(Box::new(ElectionDefinition::read_eml(elem)?))
}
EML_POLLING_STATIONS_ID => {
EML::PollingStations(Box::new(PollingStations::read_eml(elem)?))
}
EML_ELECTION_RESULT_ID => {
EML::ElectionResult(Box::new(ElectionResult::read_eml(elem)?))
}
id if CandidateListsType::is_valid_eml_id(id) => {
EML::CandidateLists(Box::new(CandidateLists::read_eml(elem)?))
}
id if CountType::is_valid_eml_id(id) => {
EML::ElectionCount(Box::new(ElectionCount::read_eml(elem)?))
}
_ => {
return Err(EMLErrorKind::UnknownDocumentType(document_id.to_string()))
.with_span(elem.span());
}
})
}
fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
match self {
EML::ElectionDefinition(ed) => ed.write_eml(writer),
EML::PollingStations(ps) => ps.write_eml(writer),
EML::CandidateLists(cl) => cl.write_eml(writer),
EML::ElectionCount(c) => c.write_eml(writer),
EML::ElectionResult(r) => r.write_eml(writer),
}
}
}
impl From<ElectionDefinition> for EML {
fn from(ed: ElectionDefinition) -> Self {
EML::from_election_definition_doc(ed)
}
}
impl From<PollingStations> for EML {
fn from(ps: PollingStations) -> Self {
EML::from_polling_stations_doc(ps)
}
}
impl From<CandidateLists> for EML {
fn from(cl: CandidateLists) -> Self {
EML::from_candidate_lists_doc(cl)
}
}
impl From<ElectionCount> for EML {
fn from(count: ElectionCount) -> Self {
EML::from_count_doc(count)
}
}
impl From<ElectionResult> for EML {
fn from(result: ElectionResult) -> Self {
EML::from_result_doc(result)
}
}
fn accepted_root(elem: &EMLElementReader<'_, '_>) -> Result<(), EMLError> {
if !elem.has_name(("EML", Some(NS_EML)))? {
return Err(EMLErrorKind::InvalidRootElement).with_span(elem.span());
}
let schema_version = elem.attribute_value_req(("SchemaVersion", None))?;
if schema_version == EML_SCHEMA_VERSION {
Ok(())
} else {
Err(EMLErrorKind::SchemaVersionNotSupported(
schema_version.to_string(),
))
.with_span(elem.span())
}
}
impl FromStr for EML {
type Err = EMLError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use crate::io::EMLRead as _;
Self::parse_eml(s, crate::io::EMLParsingMode::Strict).ok()
}
}
impl TryFrom<&str> for EML {
type Error = EMLError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
use crate::io::EMLRead as _;
Self::parse_eml(value, crate::io::EMLParsingMode::Strict).ok()
}
}
impl TryFrom<EML> for String {
type Error = EMLError;
fn try_from(value: EML) -> Result<Self, Self::Error> {
use crate::io::EMLWrite as _;
value.write_eml_root_str(true, true)
}
}
#[derive(Debug, Clone)]
pub struct ElectionIdentifierBuilder {
id: Option<StringValue<ElectionId>>,
name: Option<String>,
category: Option<StringValue<ElectionCategory>>,
subcategory: Option<StringValue<ElectionSubcategory>>,
domain: Option<ElectionDomain>,
election_date: Option<StringValue<XsDate>>,
nomination_date: Option<StringValue<XsDate>>,
}
impl ElectionIdentifierBuilder {
pub fn new() -> Self {
ElectionIdentifierBuilder {
id: None,
name: None,
category: None,
subcategory: None,
domain: None,
election_date: None,
nomination_date: None,
}
}
pub fn id(mut self, id: impl Into<ElectionId>) -> Self {
self.id = Some(StringValue::from_value(id.into()));
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn category(mut self, category: impl Into<ElectionCategory>) -> Self {
self.category = Some(StringValue::from_value(category.into()));
self
}
pub fn subcategory(mut self, subcategory: impl Into<ElectionSubcategory>) -> Self {
self.subcategory = Some(StringValue::from_value(subcategory.into()));
self
}
pub fn domain(mut self, domain: impl Into<ElectionDomain>) -> Self {
self.domain = Some(domain.into());
self
}
pub fn election_date(mut self, date: impl Into<XsDate>) -> Self {
self.election_date = Some(StringValue::from_value(date.into()));
self
}
pub fn nomination_date(mut self, date: impl Into<XsDate>) -> Self {
self.nomination_date = Some(StringValue::from_value(date.into()));
self
}
pub fn build_for_candidate_lists(self) -> Result<CandidateListsElectionIdentifier, EMLError> {
let category = self
.category
.ok_or(EMLErrorKind::MissingBuildProperty("category").without_span())?;
validate_category_and_subcategory(&category, self.subcategory.as_ref())?;
let election_date = self
.election_date
.ok_or(EMLErrorKind::MissingBuildProperty("election_date").without_span())?;
let nomination_date = self
.nomination_date
.ok_or(EMLErrorKind::MissingBuildProperty("nomination_date").without_span())?;
validate_election_and_nomination_dates(Some(&election_date), Some(&nomination_date))?;
Ok(CandidateListsElectionIdentifier {
id: self
.id
.ok_or(EMLErrorKind::MissingBuildProperty("id").without_span())?,
name: self.name,
category,
subcategory: self.subcategory,
domain: self.domain,
election_date,
nomination_date,
})
}
pub fn build_for_count(self) -> Result<ElectionCountElectionIdentifier, EMLError> {
let category = self
.category
.ok_or(EMLErrorKind::MissingBuildProperty("category").without_span())?;
validate_category_and_subcategory(&category, self.subcategory.as_ref())?;
Ok(ElectionCountElectionIdentifier {
id: self
.id
.ok_or(EMLErrorKind::MissingBuildProperty("id").without_span())?,
name: self.name,
category,
subcategory: self.subcategory,
domain: self.domain,
election_date: self
.election_date
.ok_or(EMLErrorKind::MissingBuildProperty("election_date").without_span())?,
})
}
pub fn build_for_result(self) -> Result<ElectionResultElectionIdentifier, EMLError> {
let category = self
.category
.ok_or(EMLErrorKind::MissingBuildProperty("category").without_span())?;
validate_category_and_subcategory(&category, self.subcategory.as_ref())?;
Ok(ElectionResultElectionIdentifier {
id: self
.id
.ok_or(EMLErrorKind::MissingBuildProperty("id").without_span())?,
name: self.name,
category,
subcategory: self.subcategory,
domain: self.domain,
election_date: self
.election_date
.ok_or(EMLErrorKind::MissingBuildProperty("election_date").without_span())?,
})
}
pub fn build_for_definition(self) -> Result<ElectionDefinitionElectionIdentifier, EMLError> {
let category = self
.category
.ok_or(EMLErrorKind::MissingBuildProperty("category").without_span())?;
let subcategory = self
.subcategory
.ok_or(EMLErrorKind::MissingBuildProperty("subcategory").without_span())?;
validate_category_and_subcategory(&category, Some(&subcategory))?;
let election_date = self
.election_date
.ok_or(EMLErrorKind::MissingBuildProperty("election_date").without_span())?;
let nomination_date = self
.nomination_date
.ok_or(EMLErrorKind::MissingBuildProperty("nomination_date").without_span())?;
validate_election_and_nomination_dates(Some(&election_date), Some(&nomination_date))?;
Ok(ElectionDefinitionElectionIdentifier {
id: self
.id
.ok_or(EMLErrorKind::MissingBuildProperty("id").without_span())?,
name: self
.name
.ok_or(EMLErrorKind::MissingBuildProperty("name").without_span())?,
category,
subcategory,
domain: self.domain,
election_date,
nomination_date,
})
}
pub fn build_for_polling_stations(self) -> Result<PollingStationsElectionIdentifier, EMLError> {
let category = self
.category
.ok_or(EMLErrorKind::MissingBuildProperty("category").without_span())?;
validate_category_and_subcategory(&category, self.subcategory.as_ref())?;
Ok(PollingStationsElectionIdentifier {
id: self
.id
.ok_or(EMLErrorKind::MissingBuildProperty("id").without_span())?,
name: self.name,
category,
subcategory: self.subcategory,
domain: self.domain,
election_date: self
.election_date
.ok_or(EMLErrorKind::MissingBuildProperty("election_date").without_span())?,
})
}
}
impl Default for ElectionIdentifierBuilder {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn validate_election_and_nomination_dates(
election_date: Option<&StringValue<XsDate>>,
nomination_date: Option<&StringValue<XsDate>>,
) -> Result<(), EMLError> {
let election_date = election_date.and_then(|ed| ed.copied_value().ok());
let nomination_date = nomination_date.and_then(|nd| nd.copied_value().ok());
if let (Some(ed), Some(nd)) = (election_date, nomination_date)
&& nd.date >= ed.date
{
return Err(EMLErrorKind::NominationDateNotBeforeElectionDate).without_span();
}
Ok(())
}
pub(crate) fn validate_category_and_subcategory(
category: &StringValue<ElectionCategory>,
subcategory: Option<&StringValue<ElectionSubcategory>>,
) -> Result<(), EMLError> {
let category = category.copied_value().ok();
let subcategory = subcategory.and_then(|sc| sc.copied_value().ok());
if let (Some(c), Some(sc)) = (category, subcategory)
&& !sc.is_subcategory_of(c)
{
return Err(EMLErrorKind::InvalidElectionSubcategory).without_span();
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::{EMLParsingMode, EMLRead as _, EMLWrite as _};
#[test]
fn test_parsing_arbitrary_eml_documents() {
let doc = include_str!("../../test-emls/candidate_lists/eml230b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert!(matches!(eml, EML::CandidateLists(_)));
let doc = include_str!("../../test-emls/election_definition/eml110a_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert!(matches!(eml, EML::ElectionDefinition(_)));
let doc = include_str!("../../test-emls/polling_stations/eml110b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert!(matches!(eml, EML::PollingStations(_)));
let doc = include_str!("../../test-emls/election_count/deserialize_eml510b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert!(matches!(eml, EML::ElectionCount(_)));
let doc = include_str!("../../test-emls/election_result/eml520_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert!(matches!(eml, EML::ElectionResult(_)));
}
#[test]
fn parse_and_write_110a_eml_document_should_not_fail() {
let doc = include_str!("../../test-emls/election_definition/eml110a_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert_eq!(eml.to_eml_id(), "110a");
assert_eq!(eml.to_friendly_name(), "Election Definition");
assert!(eml.is_election_definition_doc());
assert!(!eml.is_result_doc());
assert!(eml.as_election_definition_doc().is_some());
assert!(eml.as_result_doc().is_none());
assert!(eml.write_eml_root_str(true, true).is_ok());
}
#[test]
fn parse_and_write_110b_eml_document_should_not_fail() {
let doc = include_str!("../../test-emls/polling_stations/eml110b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert_eq!(eml.to_eml_id(), "110b");
assert_eq!(eml.to_friendly_name(), "Polling Stations");
assert!(eml.is_polling_stations_doc());
assert!(!eml.is_election_definition_doc());
assert!(eml.as_polling_stations_doc().is_some());
assert!(eml.as_election_definition_doc().is_none());
assert!(eml.write_eml_root_str(true, true).is_ok());
}
#[test]
fn parse_and_write_230b_eml_document_should_not_fail() {
let doc = include_str!("../../test-emls/candidate_lists/eml230b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert_eq!(eml.to_eml_id(), "230b");
assert_eq!(eml.to_friendly_name(), "Candidate Lists");
assert!(eml.is_candidate_lists_doc());
assert!(!eml.is_polling_stations_doc());
assert!(eml.as_candidate_lists_doc().is_some());
assert!(eml.as_polling_stations_doc().is_none());
assert!(eml.write_eml_root_str(true, true).is_ok());
}
#[test]
fn parse_and_write_510b_eml_document_should_not_fail() {
let doc = include_str!("../../test-emls/election_count/deserialize_eml510b_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert_eq!(eml.to_eml_id(), "510b");
assert_eq!(eml.to_friendly_name(), "Municipal Count");
assert!(eml.is_count_doc());
assert!(!eml.is_candidate_lists_doc());
assert!(eml.as_count_doc().is_some());
assert!(eml.as_candidate_lists_doc().is_none());
assert!(eml.write_eml_root_str(true, true).is_ok());
}
#[test]
fn parse_and_write_520_eml_document_should_not_fail() {
let doc = include_str!("../../test-emls/election_result/eml520_test.eml.xml");
let eml = EML::parse_eml(doc, EMLParsingMode::Strict)
.ok()
.expect("Failed to parse EML document");
assert_eq!(eml.to_eml_id(), "520");
assert_eq!(eml.to_friendly_name(), "Result");
assert!(eml.is_result_doc());
assert!(!eml.is_count_doc());
assert!(eml.as_result_doc().is_some());
assert!(eml.as_count_doc().is_none());
assert!(eml.write_eml_root_str(true, true).is_ok());
}
}