Skip to main content

use_fault/
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, FaultTextError> {
8    let original = value.as_ref();
9
10    if original.trim().is_empty() {
11        Err(FaultTextError::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 FaultTextError {
43    Empty,
44}
45
46impl fmt::Display for FaultTextError {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Empty => formatter.write_str("fault text cannot be empty"),
50        }
51    }
52}
53
54impl Error for FaultTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum FaultParseError {
58    Empty,
59}
60
61impl fmt::Display for FaultParseError {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::Empty => formatter.write_str("fault vocabulary cannot be empty"),
65        }
66    }
67}
68
69impl Error for FaultParseError {}
70
71#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub struct FaultName(String);
73
74impl FaultName {
75    /// Creates a fault name from non-empty text.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`FaultTextError::Empty`] when the trimmed value is empty.
80    pub fn new(value: impl AsRef<str>) -> Result<Self, FaultTextError> {
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 FaultName {
91    fn as_ref(&self) -> &str {
92        self.as_str()
93    }
94}
95
96impl fmt::Display for FaultName {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        formatter.write_str(self.as_str())
99    }
100}
101
102impl FromStr for FaultName {
103    type Err = FaultTextError;
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 FaultKind {
112    Normal,
113    Reverse,
114    Thrust,
115    StrikeSlip,
116    ObliqueSlip,
117    Transform,
118    Unknown,
119    Custom(String),
120}
121
122impl fmt::Display for FaultKind {
123    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::Normal => formatter.write_str("normal"),
126            Self::Reverse => formatter.write_str("reverse"),
127            Self::Thrust => formatter.write_str("thrust"),
128            Self::StrikeSlip => formatter.write_str("strike-slip"),
129            Self::ObliqueSlip => formatter.write_str("oblique-slip"),
130            Self::Transform => formatter.write_str("transform"),
131            Self::Unknown => formatter.write_str("unknown"),
132            Self::Custom(value) => formatter.write_str(value),
133        }
134    }
135}
136
137impl FromStr for FaultKind {
138    type Err = FaultParseError;
139
140    fn from_str(value: &str) -> Result<Self, Self::Err> {
141        let trimmed = value.trim();
142
143        if trimmed.is_empty() {
144            return Err(FaultParseError::Empty);
145        }
146
147        match normalized_token(trimmed).as_str() {
148            "normal" => Ok(Self::Normal),
149            "reverse" => Ok(Self::Reverse),
150            "thrust" => Ok(Self::Thrust),
151            "strike-slip" => Ok(Self::StrikeSlip),
152            "oblique-slip" => Ok(Self::ObliqueSlip),
153            "transform" => Ok(Self::Transform),
154            "unknown" => Ok(Self::Unknown),
155            _ => Ok(Self::Custom(trimmed.to_string())),
156        }
157    }
158}
159
160#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub struct FaultMovement(String);
162
163impl FaultMovement {
164    /// Creates a fault movement descriptor from non-empty text.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`FaultTextError::Empty`] when the trimmed value is empty.
169    pub fn new(value: impl AsRef<str>) -> Result<Self, FaultTextError> {
170        non_empty_text(value).map(Self)
171    }
172
173    #[must_use]
174    pub fn as_str(&self) -> &str {
175        &self.0
176    }
177}
178
179impl AsRef<str> for FaultMovement {
180    fn as_ref(&self) -> &str {
181        self.as_str()
182    }
183}
184
185impl fmt::Display for FaultMovement {
186    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187        formatter.write_str(self.as_str())
188    }
189}
190
191impl FromStr for FaultMovement {
192    type Err = FaultTextError;
193
194    fn from_str(value: &str) -> Result<Self, Self::Err> {
195        Self::new(value)
196    }
197}
198
199#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
200pub enum FaultActivity {
201    Active,
202    PotentiallyActive,
203    Inactive,
204    Reactivated,
205    Unknown,
206    Custom(String),
207}
208
209impl fmt::Display for FaultActivity {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Active => formatter.write_str("active"),
213            Self::PotentiallyActive => formatter.write_str("potentially-active"),
214            Self::Inactive => formatter.write_str("inactive"),
215            Self::Reactivated => formatter.write_str("reactivated"),
216            Self::Unknown => formatter.write_str("unknown"),
217            Self::Custom(value) => formatter.write_str(value),
218        }
219    }
220}
221
222impl FromStr for FaultActivity {
223    type Err = FaultParseError;
224
225    fn from_str(value: &str) -> Result<Self, Self::Err> {
226        let trimmed = value.trim();
227
228        if trimmed.is_empty() {
229            return Err(FaultParseError::Empty);
230        }
231
232        match normalized_token(trimmed).as_str() {
233            "active" => Ok(Self::Active),
234            "potentially-active" => Ok(Self::PotentiallyActive),
235            "inactive" => Ok(Self::Inactive),
236            "reactivated" => Ok(Self::Reactivated),
237            "unknown" => Ok(Self::Unknown),
238            _ => Ok(Self::Custom(trimmed.to_string())),
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::{
246        FaultActivity, FaultKind, FaultMovement, FaultName, FaultParseError, FaultTextError,
247    };
248
249    #[test]
250    fn valid_fault_name() -> Result<(), FaultTextError> {
251        let name = FaultName::new("Wasatch Fault")?;
252
253        assert_eq!(name.as_str(), "Wasatch Fault");
254        Ok(())
255    }
256
257    #[test]
258    fn empty_fault_name_rejected() {
259        assert_eq!(FaultName::new("\t"), Err(FaultTextError::Empty));
260    }
261
262    #[test]
263    fn fault_kind_display_parse() -> Result<(), FaultParseError> {
264        assert_eq!(FaultKind::StrikeSlip.to_string(), "strike-slip");
265        assert_eq!("reverse".parse::<FaultKind>()?, FaultKind::Reverse);
266        Ok(())
267    }
268
269    #[test]
270    fn fault_movement_wrapper() -> Result<(), FaultTextError> {
271        let movement = FaultMovement::new("dextral")?;
272
273        assert_eq!(movement.as_str(), "dextral");
274        Ok(())
275    }
276
277    #[test]
278    fn fault_activity_display_parse() -> Result<(), FaultParseError> {
279        assert_eq!(
280            FaultActivity::PotentiallyActive.to_string(),
281            "potentially-active"
282        );
283        assert_eq!(
284            "inactive".parse::<FaultActivity>()?,
285            FaultActivity::Inactive
286        );
287        Ok(())
288    }
289}