eu4save/
country_tag.rs

1use crate::{Eu4Error, Eu4ErrorKind};
2use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
3use std::{fmt, str::FromStr};
4
5/// Wrapper around a Country's unique three byte tag
6///
7/// ```rust
8/// use eu4save::CountryTag;
9/// let tag: CountryTag = "ENG".parse()?;
10/// assert_eq!(tag.to_string(), String::from("ENG"));
11/// # Ok::<(), Box<dyn std::error::Error>>(())
12/// ```
13#[derive(Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
14pub struct CountryTag([u8; 3]);
15
16impl CountryTag {
17    /// Create a country tag from a byte slice. Returns error if input is not
18    /// three bytes in length and not compose of dashes or alphanumeric data.
19    ///
20    /// ```
21    /// use eu4save::CountryTag;
22    /// let tag: CountryTag = CountryTag::create(b"ENG")?;
23    /// # Ok::<(), Box<dyn std::error::Error>>(())
24    /// ```
25    pub fn create<T: AsRef<[u8]>>(s: T) -> Result<Self, Eu4Error> {
26        if let [a, b, c] = *s.as_ref() {
27            if is_tagc(a) && is_tagc(b) && is_tagc(c) {
28                Ok(CountryTag([a, b, c]))
29            } else {
30                Err(Eu4Error::new(Eu4ErrorKind::CountryTagInvalidCharacters))
31            }
32        } else {
33            Err(Eu4Error::new(Eu4ErrorKind::CountryTagIncorrectSize))
34        }
35    }
36
37    /// An ergonomic shortcut to determine if input byte slice contains the same
38    /// data as the tag
39    /// ```
40    /// use eu4save::CountryTag;
41    /// let tag: CountryTag = CountryTag::create(b"ENG")?;
42    /// assert!(tag.is(b"ENG"));
43    /// # Ok::<(), Box<dyn std::error::Error>>(())
44    /// ```
45    pub fn is<T: AsRef<[u8]>>(&self, s: T) -> bool {
46        self.as_bytes() == s.as_ref()
47    }
48
49    /// Returns the country tag as a byte slice
50    /// ```
51    /// use eu4save::CountryTag;
52    /// let tag: CountryTag = CountryTag::create(b"ENG")?;
53    /// assert_eq!(tag.as_bytes(), b"ENG");
54    /// # Ok::<(), Box<dyn std::error::Error>>(())
55    /// ```
56    pub fn as_bytes(&self) -> &[u8] {
57        &self.0
58    }
59
60    /// Returns the country tag as a string slice
61    /// ```
62    /// use eu4save::CountryTag;
63    /// let tag: CountryTag = CountryTag::create(b"ENG")?;
64    /// assert_eq!(tag.as_str(), "ENG");
65    /// # Ok::<(), Box<dyn std::error::Error>>(())
66    /// ```
67    pub fn as_str(&self) -> &str {
68        // We know that this is safe as the CountryTag constructor only allows
69        // ascii alphanumeric and dashes
70        debug_assert!(std::str::from_utf8(&self.0).is_ok());
71        unsafe { std::str::from_utf8_unchecked(&self.0) }
72    }
73}
74
75#[inline]
76pub(crate) const fn is_tagc(b: u8) -> bool {
77    b.is_ascii_alphanumeric() || b == b'-'
78}
79
80impl FromStr for CountryTag {
81    type Err = Eu4Error;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        CountryTag::create(s)
85    }
86}
87
88impl AsRef<str> for CountryTag {
89    fn as_ref(&self) -> &str {
90        self.as_str()
91    }
92}
93
94impl fmt::Debug for CountryTag {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(self.as_ref())
97    }
98}
99
100impl fmt::Display for CountryTag {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(self.as_ref())
103    }
104}
105
106impl Serialize for CountryTag {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        serializer.serialize_str(self.as_ref())
112    }
113}
114
115impl<'de> Deserialize<'de> for CountryTag {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: Deserializer<'de>,
119    {
120        struct CountryTagVisitor;
121
122        impl<'de> de::Visitor<'de> for CountryTagVisitor {
123            type Value = CountryTag;
124
125            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
126                formatter.write_str("struct CountryTag")
127            }
128
129            fn visit_str<A>(self, v: &str) -> Result<Self::Value, A>
130            where
131                A: de::Error,
132            {
133                v.parse().map_err(de::Error::custom)
134            }
135        }
136
137        deserializer.deserialize_str(CountryTagVisitor)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn tag_order() {
147        let tag1: CountryTag = "AAA".parse().unwrap();
148        let tag2: CountryTag = "BBB".parse().unwrap();
149        assert!(tag1 < tag2);
150    }
151
152    #[test]
153    fn parse_blank_tag() {
154        let tag1: CountryTag = "---".parse().unwrap();
155        assert_eq!(tag1.to_string(), String::from("---"));
156    }
157
158    #[test]
159    fn tag_debug_representation() {
160        let tag1: CountryTag = "FRA".parse().unwrap();
161        let debug = format!("{:?}", tag1);
162        assert_eq!(debug, String::from("FRA"));
163    }
164}