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