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, StratumTextError> {
8 let original = value.as_ref();
9
10 if original.trim().is_empty() {
11 Err(StratumTextError::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 StratumTextError {
43 Empty,
44}
45
46impl fmt::Display for StratumTextError {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Empty => formatter.write_str("stratum text cannot be empty"),
50 }
51 }
52}
53
54impl Error for StratumTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum StratumParseError {
58 Empty,
59}
60
61impl fmt::Display for StratumParseError {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Empty => formatter.write_str("stratum vocabulary cannot be empty"),
65 }
66 }
67}
68
69impl Error for StratumParseError {}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum StratumOrderError {
73 InvalidNumber,
74}
75
76impl fmt::Display for StratumOrderError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::InvalidNumber => formatter.write_str("stratum order must be a valid integer"),
80 }
81 }
82}
83
84impl Error for StratumOrderError {}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum StratumThicknessError {
88 InvalidNumber,
89 NonFinite,
90 Negative,
91}
92
93impl fmt::Display for StratumThicknessError {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::InvalidNumber => formatter.write_str("stratum thickness must be a valid number"),
97 Self::NonFinite => formatter.write_str("stratum thickness must be finite"),
98 Self::Negative => formatter.write_str("stratum thickness cannot be negative"),
99 }
100 }
101}
102
103impl Error for StratumThicknessError {}
104
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct StratumName(String);
107
108impl StratumName {
109 pub fn new(value: impl AsRef<str>) -> Result<Self, StratumTextError> {
115 non_empty_text(value).map(Self)
116 }
117
118 #[must_use]
119 pub fn as_str(&self) -> &str {
120 &self.0
121 }
122}
123
124impl AsRef<str> for StratumName {
125 fn as_ref(&self) -> &str {
126 self.as_str()
127 }
128}
129
130impl fmt::Display for StratumName {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 formatter.write_str(self.as_str())
133 }
134}
135
136impl FromStr for StratumName {
137 type Err = StratumTextError;
138
139 fn from_str(value: &str) -> Result<Self, Self::Err> {
140 Self::new(value)
141 }
142}
143
144#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub enum StratumKind {
146 Bed,
147 Layer,
148 Seam,
149 Lens,
150 Horizon,
151 Member,
152 Unknown,
153 Custom(String),
154}
155
156impl fmt::Display for StratumKind {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::Bed => formatter.write_str("bed"),
160 Self::Layer => formatter.write_str("layer"),
161 Self::Seam => formatter.write_str("seam"),
162 Self::Lens => formatter.write_str("lens"),
163 Self::Horizon => formatter.write_str("horizon"),
164 Self::Member => formatter.write_str("member"),
165 Self::Unknown => formatter.write_str("unknown"),
166 Self::Custom(value) => formatter.write_str(value),
167 }
168 }
169}
170
171impl FromStr for StratumKind {
172 type Err = StratumParseError;
173
174 fn from_str(value: &str) -> Result<Self, Self::Err> {
175 let trimmed = value.trim();
176
177 if trimmed.is_empty() {
178 return Err(StratumParseError::Empty);
179 }
180
181 match normalized_token(trimmed).as_str() {
182 "bed" => Ok(Self::Bed),
183 "layer" => Ok(Self::Layer),
184 "seam" => Ok(Self::Seam),
185 "lens" => Ok(Self::Lens),
186 "horizon" => Ok(Self::Horizon),
187 "member" => Ok(Self::Member),
188 "unknown" => Ok(Self::Unknown),
189 _ => Ok(Self::Custom(trimmed.to_string())),
190 }
191 }
192}
193
194#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
195pub struct StratumOrder(u32);
196
197impl StratumOrder {
198 #[must_use]
199 pub const fn new(position: u32) -> Self {
200 Self(position)
201 }
202
203 #[must_use]
204 pub const fn position(self) -> u32 {
205 self.0
206 }
207}
208
209impl fmt::Display for StratumOrder {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 write!(formatter, "{}", self.0)
212 }
213}
214
215impl FromStr for StratumOrder {
216 type Err = StratumOrderError;
217
218 fn from_str(value: &str) -> Result<Self, Self::Err> {
219 let parsed = value
220 .trim()
221 .parse::<u32>()
222 .map_err(|_| StratumOrderError::InvalidNumber)?;
223 Ok(Self::new(parsed))
224 }
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
228pub struct StratumThickness(f64);
229
230impl StratumThickness {
231 pub fn new(meters: f64) -> Result<Self, StratumThicknessError> {
238 if !meters.is_finite() {
239 return Err(StratumThicknessError::NonFinite);
240 }
241
242 if meters < 0.0 {
243 return Err(StratumThicknessError::Negative);
244 }
245
246 Ok(Self(meters))
247 }
248
249 #[must_use]
250 pub const fn meters(self) -> f64 {
251 self.0
252 }
253}
254
255impl fmt::Display for StratumThickness {
256 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(formatter, "{}", self.0)
258 }
259}
260
261impl FromStr for StratumThickness {
262 type Err = StratumThicknessError;
263
264 fn from_str(value: &str) -> Result<Self, Self::Err> {
265 let parsed = value
266 .trim()
267 .parse::<f64>()
268 .map_err(|_| StratumThicknessError::InvalidNumber)?;
269 Self::new(parsed)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::{
276 StratumKind, StratumName, StratumOrder, StratumParseError, StratumTextError,
277 StratumThickness, StratumThicknessError,
278 };
279
280 #[test]
281 fn valid_stratum_name() -> Result<(), StratumTextError> {
282 let name = StratumName::new("Brushy Basin")?;
283
284 assert_eq!(name.as_str(), "Brushy Basin");
285 Ok(())
286 }
287
288 #[test]
289 fn empty_stratum_name_rejected() {
290 assert_eq!(StratumName::new("\n"), Err(StratumTextError::Empty));
291 }
292
293 #[test]
294 fn stratum_kind_display_parse() -> Result<(), StratumParseError> {
295 assert_eq!(StratumKind::Horizon.to_string(), "horizon");
296 assert_eq!("bed".parse::<StratumKind>()?, StratumKind::Bed);
297 Ok(())
298 }
299
300 #[test]
301 fn stratum_order_construction() {
302 let order = StratumOrder::new(2);
303
304 assert_eq!(order.position(), 2);
305 assert_eq!(order.to_string(), "2");
306 }
307
308 #[test]
309 fn valid_stratum_thickness() -> Result<(), StratumThicknessError> {
310 let thickness = StratumThickness::new(12.5)?;
311
312 assert!((thickness.meters() - 12.5).abs() < f64::EPSILON);
313 Ok(())
314 }
315
316 #[test]
317 fn negative_stratum_thickness_rejected() {
318 assert_eq!(
319 StratumThickness::new(-0.1),
320 Err(StratumThicknessError::Negative)
321 );
322 }
323}