Skip to main content

eml_nl/utils/
contest_id.rs

1use std::sync::LazyLock;
2
3use regex::Regex;
4use thiserror::Error;
5
6use crate::{EMLError, EMLValueResultExt, utils::StringValueData};
7
8/// Regular expression for validating ContestId values.
9static CONTEST_ID_RE: LazyLock<Regex> = LazyLock::new(|| {
10    Regex::new(r"^([1-9]\d*|geen|alle|M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))$")
11        .expect("Failed to compile Contest ID regex")
12});
13
14/// A string of type ContestId as defined in the EML_NL specification
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16#[repr(transparent)]
17pub struct ContestId(String);
18
19impl ContestId {
20    /// Create a new `ContestId` from a string, validating its format
21    pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
22        StringValueData::parse_from_str(s.as_ref()).wrap_value_error()
23    }
24
25    /// Get the raw string value of the `ContestId`
26    pub fn value(&self) -> &str {
27        &self.0
28    }
29
30    /// Check if the `ContestId` is "geen"
31    pub fn is_geen(&self) -> bool {
32        self.0 == "geen"
33    }
34
35    /// Check if the `ContestId` is "alle"
36    pub fn is_alle(&self) -> bool {
37        self.0 == "alle"
38    }
39
40    /// Create a `ContestId` representing "geen"
41    pub fn geen() -> Self {
42        ContestId("geen".to_string())
43    }
44
45    /// Create a `ContestId` representing "alle"
46    pub fn alle() -> Self {
47        ContestId("alle".to_string())
48    }
49}
50
51/// Error returned when a string could not be parsed as a [`ContestId`]
52#[derive(Debug, Clone, Error)]
53#[error("Invalid contest id: {0}")]
54pub struct InvalidContestIdError(String);
55
56impl StringValueData for ContestId {
57    type Error = InvalidContestIdError;
58
59    fn parse_from_str(s: &str) -> Result<Self, Self::Error>
60    where
61        Self: Sized,
62    {
63        if !s.is_empty() && CONTEST_ID_RE.is_match(s) {
64            Ok(ContestId(s.to_string()))
65        } else {
66            Err(InvalidContestIdError(s.to_string()))
67        }
68    }
69
70    fn to_raw_value(&self) -> String {
71        self.0.clone()
72    }
73}
74
75/// A ContestId representing a fixed "geen" value
76#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub struct ContestIdGeen;
78
79impl ContestIdGeen {
80    /// The fixed string value for 'geen'
81    pub const GEEN: &str = "geen";
82
83    /// Create a new `ContestIdGeen`
84    pub fn new() -> Self {
85        ContestIdGeen
86    }
87
88    /// Convert to a regular [`ContestId`]
89    pub fn to_contest_id(&self) -> ContestId {
90        ContestId::geen()
91    }
92}
93
94impl Default for ContestIdGeen {
95    fn default() -> Self {
96        ContestIdGeen::new()
97    }
98}
99
100/// Error returned when a string could not be parsed as a [`ContestIdGeen`]
101#[derive(Debug, Clone, Error)]
102#[error("Invalid contest id, expected 'geen': {0}")]
103pub struct InvalidContestIdGeenError(String);
104
105impl StringValueData for ContestIdGeen {
106    type Error = InvalidContestIdGeenError;
107
108    fn parse_from_str(s: &str) -> Result<Self, Self::Error>
109    where
110        Self: Sized,
111    {
112        if s == Self::GEEN {
113            Ok(ContestIdGeen)
114        } else {
115            Err(InvalidContestIdGeenError(s.to_string()))
116        }
117    }
118
119    fn to_raw_value(&self) -> String {
120        Self::GEEN.to_string()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_contest_id_regex_compiles() {
130        LazyLock::force(&CONTEST_ID_RE);
131    }
132
133    #[test]
134    fn test_valid_contest_ids() {
135        let valid_ids = [
136            "1", "12345", "geen", "alle", "III", "IV", "V", "C", "D", "MMMM", "CM",
137        ];
138        for id in valid_ids {
139            assert!(
140                ContestId::new(id).is_ok(),
141                "ContestId should accept valid id: {}",
142                id
143            );
144        }
145    }
146
147    #[test]
148    fn test_invalid_contest_ids() {
149        let invalid_ids = ["", "0", "0123", "abc", "123abc", "-1", "MMMMM", "IC"];
150        for id in invalid_ids {
151            assert!(
152                ContestId::new(id).is_err(),
153                "ContestId should reject invalid id: {}",
154                id
155            );
156        }
157    }
158
159    #[test]
160    fn test_contest_id_types() {
161        let geen = ContestId::geen();
162        assert_eq!(geen.value(), "geen");
163        assert!(geen.is_geen());
164        assert!(!geen.is_alle());
165
166        let alle = ContestId::alle();
167        assert_eq!(alle.value(), "alle");
168        assert!(!alle.is_geen());
169        assert!(alle.is_alle());
170    }
171
172    #[test]
173    fn test_contest_id_geen() {
174        let valid_geen = "geen";
175        let invalid_geen = "alle";
176        assert!(ContestIdGeen::parse_from_str(valid_geen).is_ok());
177        assert!(ContestIdGeen::parse_from_str(invalid_geen).is_err());
178    }
179
180    #[test]
181    fn test_contest_id_geen_to_contest_id() {
182        let geen = ContestIdGeen::new();
183        let contest_id = geen.to_contest_id();
184        assert_eq!(contest_id.value(), "geen");
185    }
186}