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, PlateTextError> {
8 let original = value.as_ref();
9
10 if original.trim().is_empty() {
11 Err(PlateTextError::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 PlateTextError {
43 Empty,
44}
45
46impl fmt::Display for PlateTextError {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Self::Empty => formatter.write_str("tectonic plate text cannot be empty"),
50 }
51 }
52}
53
54impl Error for PlateTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum PlateParseError {
58 Empty,
59}
60
61impl fmt::Display for PlateParseError {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Empty => formatter.write_str("tectonic plate vocabulary cannot be empty"),
65 }
66 }
67}
68
69impl Error for PlateParseError {}
70
71#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub struct TectonicPlateName(String);
73
74impl TectonicPlateName {
75 pub fn new(value: impl AsRef<str>) -> Result<Self, PlateTextError> {
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 TectonicPlateName {
91 fn as_ref(&self) -> &str {
92 self.as_str()
93 }
94}
95
96impl fmt::Display for TectonicPlateName {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str(self.as_str())
99 }
100}
101
102impl FromStr for TectonicPlateName {
103 type Err = PlateTextError;
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 PlateKind {
112 Continental,
113 Oceanic,
114 Microplate,
115 Unknown,
116 Custom(String),
117}
118
119impl fmt::Display for PlateKind {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::Continental => formatter.write_str("continental"),
123 Self::Oceanic => formatter.write_str("oceanic"),
124 Self::Microplate => formatter.write_str("microplate"),
125 Self::Unknown => formatter.write_str("unknown"),
126 Self::Custom(value) => formatter.write_str(value),
127 }
128 }
129}
130
131impl FromStr for PlateKind {
132 type Err = PlateParseError;
133
134 fn from_str(value: &str) -> Result<Self, Self::Err> {
135 let trimmed = value.trim();
136
137 if trimmed.is_empty() {
138 return Err(PlateParseError::Empty);
139 }
140
141 match normalized_token(trimmed).as_str() {
142 "continental" => Ok(Self::Continental),
143 "oceanic" => Ok(Self::Oceanic),
144 "microplate" => Ok(Self::Microplate),
145 "unknown" => Ok(Self::Unknown),
146 _ => Ok(Self::Custom(trimmed.to_string())),
147 }
148 }
149}
150
151#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub enum PlateBoundaryKind {
153 Convergent,
154 Divergent,
155 Transform,
156 Unknown,
157 Custom(String),
158}
159
160impl fmt::Display for PlateBoundaryKind {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 Self::Convergent => formatter.write_str("convergent"),
164 Self::Divergent => formatter.write_str("divergent"),
165 Self::Transform => formatter.write_str("transform"),
166 Self::Unknown => formatter.write_str("unknown"),
167 Self::Custom(value) => formatter.write_str(value),
168 }
169 }
170}
171
172impl FromStr for PlateBoundaryKind {
173 type Err = PlateParseError;
174
175 fn from_str(value: &str) -> Result<Self, Self::Err> {
176 let trimmed = value.trim();
177
178 if trimmed.is_empty() {
179 return Err(PlateParseError::Empty);
180 }
181
182 match normalized_token(trimmed).as_str() {
183 "convergent" => Ok(Self::Convergent),
184 "divergent" => Ok(Self::Divergent),
185 "transform" => Ok(Self::Transform),
186 "unknown" => Ok(Self::Unknown),
187 _ => Ok(Self::Custom(trimmed.to_string())),
188 }
189 }
190}
191
192#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
193pub struct PlateMotion(String);
194
195impl PlateMotion {
196 pub fn new(value: impl AsRef<str>) -> Result<Self, PlateTextError> {
202 non_empty_text(value).map(Self)
203 }
204
205 #[must_use]
206 pub fn as_str(&self) -> &str {
207 &self.0
208 }
209}
210
211impl AsRef<str> for PlateMotion {
212 fn as_ref(&self) -> &str {
213 self.as_str()
214 }
215}
216
217impl fmt::Display for PlateMotion {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 formatter.write_str(self.as_str())
220 }
221}
222
223impl FromStr for PlateMotion {
224 type Err = PlateTextError;
225
226 fn from_str(value: &str) -> Result<Self, Self::Err> {
227 Self::new(value)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::{
234 PlateBoundaryKind, PlateKind, PlateMotion, PlateParseError, PlateTextError,
235 TectonicPlateName,
236 };
237
238 #[test]
239 fn valid_plate_name() -> Result<(), PlateTextError> {
240 let name = TectonicPlateName::new("Pacific Plate")?;
241
242 assert_eq!(name.as_str(), "Pacific Plate");
243 Ok(())
244 }
245
246 #[test]
247 fn empty_plate_name_rejected() {
248 assert_eq!(TectonicPlateName::new(" "), Err(PlateTextError::Empty));
249 }
250
251 #[test]
252 fn plate_kind_display_parse() -> Result<(), PlateParseError> {
253 assert_eq!(PlateKind::Continental.to_string(), "continental");
254 assert_eq!("oceanic".parse::<PlateKind>()?, PlateKind::Oceanic);
255 Ok(())
256 }
257
258 #[test]
259 fn plate_boundary_kind_display_parse() -> Result<(), PlateParseError> {
260 assert_eq!(PlateBoundaryKind::Convergent.to_string(), "convergent");
261 assert_eq!(
262 "transform".parse::<PlateBoundaryKind>()?,
263 PlateBoundaryKind::Transform
264 );
265 Ok(())
266 }
267
268 #[test]
269 fn plate_motion_wrapper() -> Result<(), PlateTextError> {
270 let motion = PlateMotion::new("northwest")?;
271
272 assert_eq!(motion.as_str(), "northwest");
273 Ok(())
274 }
275}