Skip to main content

use_tectonic_plate/
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, 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    /// Creates a tectonic plate name from non-empty text.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`PlateTextError::Empty`] when the trimmed value is empty.
80    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    /// Creates a plate motion descriptor from non-empty text.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`PlateTextError::Empty`] when the trimmed value is empty.
201    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}