Skip to main content

use_formation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, FormationTextError> {
8    let original = value.as_ref();
9
10    if original.trim().is_empty() {
11        Err(FormationTextError::Empty)
12    } else {
13        Ok(original.to_string())
14    }
15}
16
17fn normalized_token(value: &str) -> String {
18    let mut normalized = String::with_capacity(value.len());
19    let mut previous_separator = false;
20
21    for character in value.trim().chars() {
22        if character.is_ascii_alphanumeric() {
23            normalized.push(character.to_ascii_lowercase());
24            previous_separator = false;
25        } else if (character.is_whitespace() || character == '-' || character == '_')
26            && !previous_separator
27            && !normalized.is_empty()
28        {
29            normalized.push('-');
30            previous_separator = true;
31        }
32    }
33
34    if normalized.ends_with('-') {
35        let _ = normalized.pop();
36    }
37
38    normalized
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum FormationTextError {
43    Empty,
44}
45
46impl fmt::Display for FormationTextError {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Empty => formatter.write_str("formation text cannot be empty"),
50        }
51    }
52}
53
54impl Error for FormationTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum FormationParseError {
58    Empty,
59}
60
61impl fmt::Display for FormationParseError {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::Empty => formatter.write_str("formation vocabulary cannot be empty"),
65        }
66    }
67}
68
69impl Error for FormationParseError {}
70
71#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub struct FormationName(String);
73
74impl FormationName {
75    /// Creates a formation name from non-empty text.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`FormationTextError::Empty`] when the trimmed value is empty.
80    pub fn new(value: impl AsRef<str>) -> Result<Self, FormationTextError> {
81        non_empty_text(value).map(Self)
82    }
83
84    #[must_use]
85    pub fn as_str(&self) -> &str {
86        &self.0
87    }
88}
89
90impl AsRef<str> for FormationName {
91    fn as_ref(&self) -> &str {
92        self.as_str()
93    }
94}
95
96impl fmt::Display for FormationName {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        formatter.write_str(self.as_str())
99    }
100}
101
102impl FromStr for FormationName {
103    type Err = FormationTextError;
104
105    fn from_str(value: &str) -> Result<Self, Self::Err> {
106        Self::new(value)
107    }
108}
109
110#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum FormationKind {
112    Formation,
113    Group,
114    Supergroup,
115    Member,
116    Bed,
117    Unknown,
118    Custom(String),
119}
120
121impl fmt::Display for FormationKind {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            Self::Formation => formatter.write_str("formation"),
125            Self::Group => formatter.write_str("group"),
126            Self::Supergroup => formatter.write_str("supergroup"),
127            Self::Member => formatter.write_str("member"),
128            Self::Bed => formatter.write_str("bed"),
129            Self::Unknown => formatter.write_str("unknown"),
130            Self::Custom(value) => formatter.write_str(value),
131        }
132    }
133}
134
135impl FromStr for FormationKind {
136    type Err = FormationParseError;
137
138    fn from_str(value: &str) -> Result<Self, Self::Err> {
139        let trimmed = value.trim();
140
141        if trimmed.is_empty() {
142            return Err(FormationParseError::Empty);
143        }
144
145        match normalized_token(trimmed).as_str() {
146            "formation" => Ok(Self::Formation),
147            "group" => Ok(Self::Group),
148            "supergroup" => Ok(Self::Supergroup),
149            "member" => Ok(Self::Member),
150            "bed" => Ok(Self::Bed),
151            "unknown" => Ok(Self::Unknown),
152            _ => Ok(Self::Custom(trimmed.to_string())),
153        }
154    }
155}
156
157#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
158pub struct FormationMember(String);
159
160impl FormationMember {
161    /// Creates a formation member from non-empty text.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`FormationTextError::Empty`] when the trimmed value is empty.
166    pub fn new(value: impl AsRef<str>) -> Result<Self, FormationTextError> {
167        non_empty_text(value).map(Self)
168    }
169
170    #[must_use]
171    pub fn as_str(&self) -> &str {
172        &self.0
173    }
174}
175
176impl AsRef<str> for FormationMember {
177    fn as_ref(&self) -> &str {
178        self.as_str()
179    }
180}
181
182impl fmt::Display for FormationMember {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        formatter.write_str(self.as_str())
185    }
186}
187
188impl FromStr for FormationMember {
189    type Err = FormationTextError;
190
191    fn from_str(value: &str) -> Result<Self, Self::Err> {
192        Self::new(value)
193    }
194}
195
196#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
197pub struct FormationGroup(String);
198
199impl FormationGroup {
200    /// Creates a formation group from non-empty text.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`FormationTextError::Empty`] when the trimmed value is empty.
205    pub fn new(value: impl AsRef<str>) -> Result<Self, FormationTextError> {
206        non_empty_text(value).map(Self)
207    }
208
209    #[must_use]
210    pub fn as_str(&self) -> &str {
211        &self.0
212    }
213}
214
215impl AsRef<str> for FormationGroup {
216    fn as_ref(&self) -> &str {
217        self.as_str()
218    }
219}
220
221impl fmt::Display for FormationGroup {
222    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223        formatter.write_str(self.as_str())
224    }
225}
226
227impl FromStr for FormationGroup {
228    type Err = FormationTextError;
229
230    fn from_str(value: &str) -> Result<Self, Self::Err> {
231        Self::new(value)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::{
238        FormationGroup, FormationKind, FormationMember, FormationName, FormationParseError,
239        FormationTextError,
240    };
241
242    #[test]
243    fn valid_formation_name() -> Result<(), FormationTextError> {
244        let name = FormationName::new("Morrison Formation")?;
245
246        assert_eq!(name.as_str(), "Morrison Formation");
247        Ok(())
248    }
249
250    #[test]
251    fn empty_formation_name_rejected() {
252        assert_eq!(FormationName::new("   "), Err(FormationTextError::Empty));
253    }
254
255    #[test]
256    fn formation_kind_display_parse() -> Result<(), FormationParseError> {
257        assert_eq!(FormationKind::Supergroup.to_string(), "supergroup");
258        assert_eq!("group".parse::<FormationKind>()?, FormationKind::Group);
259        Ok(())
260    }
261
262    #[test]
263    fn formation_member_wrapper() -> Result<(), FormationTextError> {
264        let member = FormationMember::new("Brushy Basin Member")?;
265
266        assert_eq!(member.as_str(), "Brushy Basin Member");
267        Ok(())
268    }
269
270    #[test]
271    fn formation_group_wrapper() -> Result<(), FormationTextError> {
272        let group = FormationGroup::new("Chinle Group")?;
273
274        assert_eq!(group.as_str(), "Chinle Group");
275        Ok(())
276    }
277}