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 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 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 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}