rosu_mods/
acronym.rs

1use std::{
2    cmp::Ordering,
3    fmt::{Debug, Display, Formatter, Result as FmtResult},
4    str::FromStr,
5};
6
7use crate::error::AcronymParseError;
8
9/// The acronym of a [`GameMod`].
10///
11/// [`GameMod`]: crate::generated_mods::GameMod
12#[derive(Copy, Clone, PartialEq, Eq, Hash)]
13#[repr(transparent)]
14#[cfg_attr(
15    feature = "rkyv",
16    derive(rkyv::bytecheck::CheckBytes),
17    bytecheck(crate = rkyv::bytecheck),
18)]
19pub struct Acronym([u8; 3]);
20
21impl Acronym {
22    /// Create an [`Acronym`] from a string.
23    ///
24    /// # Safety
25    ///
26    /// The given string must consist of two or three bytes representing capitalized ASCII letters or digits.
27    ///
28    /// # Example
29    /// ```rust
30    /// use rosu_mods::Acronym;
31    ///
32    /// let hd = unsafe { Acronym::from_str_unchecked("HD") };
33    /// assert_eq!(hd.as_str(), "HD");
34    /// ```
35    ///
36    /// Each of the following may lead to undefined behavior, don't do that!
37    /// ```rust,no_run
38    /// # use rosu_mods::Acronym;
39    /// let _ = unsafe { Acronym::from_str_unchecked("HDHR") }; // must be 2 or 3 characters
40    /// let _ = unsafe { Acronym::from_str_unchecked("hd") };   // must be uppercase
41    /// ```
42    pub const unsafe fn from_str_unchecked(s: &str) -> Self {
43        let array = if s.len() == 2 {
44            // SAFETY: `s` is guaranteed to be of length 2
45            let [a, b] = unsafe { *(s.as_ptr().cast::<[u8; 2]>()) };
46
47            [0, a, b]
48        } else {
49            // SAFETY: caller guarantees that `s` is of length 3
50            unsafe { *s.as_ptr().cast::<[u8; 3]>() }
51        };
52
53        Self(array)
54    }
55
56    /// Returns the [`Acronym`] as a string.
57    ///
58    /// # Example
59    /// ```rust
60    /// use rosu_mods::Acronym;
61    ///
62    /// let hd = "HD".parse::<Acronym>().unwrap();
63    /// assert_eq!(hd.as_str(), "HD");
64    /// ```
65    pub fn as_str(&self) -> &str {
66        let start_idx = usize::from(self.0[0] == 0);
67
68        // SAFETY: `self.0` is known to be constructed from a valid string
69        unsafe { std::str::from_utf8_unchecked(&self.0[start_idx..]) }
70    }
71}
72
73impl Debug for Acronym {
74    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
75        Debug::fmt(self.as_str(), f)
76    }
77}
78
79impl Display for Acronym {
80    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
81        f.write_str(self.as_str())
82    }
83}
84
85impl FromStr for Acronym {
86    type Err = AcronymParseError;
87
88    /// Create an [`Acronym`] from a string.
89    ///
90    /// Errors if the acronym consists of fewer than 2 or more than 3 bytes.
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match <[u8; 2]>::try_from(s.as_bytes()) {
93            Ok([a, b]) => Ok(Self([0, a.to_ascii_uppercase(), b.to_ascii_uppercase()])),
94            Err(_) => s
95                .as_bytes()
96                .try_into()
97                .map(|mut array: [u8; 3]| {
98                    array.make_ascii_uppercase();
99
100                    Self(array)
101                })
102                .map_err(|_| AcronymParseError {
103                    acronym: Box::from(s),
104                }),
105        }
106    }
107}
108
109impl PartialOrd for Acronym {
110    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
111        Some(self.cmp(other))
112    }
113}
114
115impl Ord for Acronym {
116    fn cmp(&self, other: &Self) -> Ordering {
117        self.as_str().cmp(other.as_str())
118    }
119}
120
121#[cfg(feature = "serde")]
122#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "serde")))]
123const _: () = {
124    use serde::{
125        de::{Deserialize, Deserializer, Error as DeError, Visitor},
126        ser::{Serialize, Serializer},
127    };
128
129    impl<'de> Deserialize<'de> for Acronym {
130        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
131            struct AcronymVisitor;
132
133            impl Visitor<'_> for AcronymVisitor {
134                type Value = Acronym;
135
136                fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
137                    f.write_str("string")
138                }
139
140                fn visit_str<E: DeError>(self, v: &str) -> Result<Self::Value, E> {
141                    v.parse().map_err(DeError::custom)
142                }
143
144                fn visit_string<E: DeError>(self, v: String) -> Result<Self::Value, E> {
145                    self.visit_str(&v)
146                }
147            }
148
149            d.deserialize_str(AcronymVisitor)
150        }
151    }
152
153    impl Serialize for Acronym {
154        fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
155            s.serialize_str(self.as_str())
156        }
157    }
158};
159
160#[cfg(feature = "rkyv")]
161#[cfg_attr(all(docsrs, not(doctest)), doc(cfg(feature = "rkyv")))]
162const _: () = {
163    use rkyv::{
164        munge::munge, rancor::Fallible, traits::NoUndef, Archive, Deserialize, Place, Portable,
165        Serialize,
166    };
167
168    unsafe impl Portable for Acronym {}
169
170    unsafe impl NoUndef for Acronym {}
171
172    impl Archive for Acronym {
173        type Archived = Self;
174        type Resolver = ();
175
176        fn resolve(&self, (): Self::Resolver, out: Place<Self::Archived>) {
177            munge!(let Self(out) = out);
178            self.0.resolve([(); 3], out);
179        }
180    }
181
182    impl<S: Fallible + ?Sized> Serialize<S> for Acronym {
183        fn serialize(&self, s: &mut S) -> Result<(), S::Error> {
184            self.0.serialize(s).map(|_| ())
185        }
186    }
187
188    impl<D: Fallible + ?Sized> Deserialize<Self, D> for Acronym {
189        fn deserialize(&self, _: &mut D) -> Result<Self, D::Error> {
190            Ok(*self)
191        }
192    }
193};