Skip to main content

use_organ/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_key(value: &str) -> String {
8    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, OrganNameError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(OrganNameError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21/// Error returned when organ labels are empty.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum OrganNameError {
24    /// The supplied value was empty after trimming surrounding whitespace.
25    Empty,
26}
27
28impl fmt::Display for OrganNameError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Empty => formatter.write_str("organ label cannot be empty"),
32        }
33    }
34}
35
36impl Error for OrganNameError {}
37
38/// A non-empty organ name.
39#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct OrganName(String);
41
42impl OrganName {
43    /// Creates an organ name from non-empty text.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`OrganNameError::Empty`] when the trimmed name is empty.
48    pub fn new(value: impl AsRef<str>) -> Result<Self, OrganNameError> {
49        non_empty_text(value).map(Self)
50    }
51
52    /// Returns the organ name text.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57
58    /// Consumes the name and returns the owned string.
59    #[must_use]
60    pub fn into_string(self) -> String {
61        self.0
62    }
63}
64
65impl AsRef<str> for OrganName {
66    fn as_ref(&self) -> &str {
67        self.as_str()
68    }
69}
70
71impl fmt::Display for OrganName {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter.write_str(self.as_str())
74    }
75}
76
77impl FromStr for OrganName {
78    type Err = OrganNameError;
79
80    fn from_str(value: &str) -> Result<Self, Self::Err> {
81        Self::new(value)
82    }
83}
84
85/// A simple organ-system reference label.
86#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct OrganSystemRef(String);
88
89impl OrganSystemRef {
90    /// Creates an organ-system reference from non-empty text.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`OrganNameError::Empty`] when the trimmed label is empty.
95    pub fn new(value: impl AsRef<str>) -> Result<Self, OrganNameError> {
96        non_empty_text(value).map(Self)
97    }
98
99    /// Returns the reference text.
100    #[must_use]
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104
105    /// Consumes the reference and returns the owned string.
106    #[must_use]
107    pub fn into_string(self) -> String {
108        self.0
109    }
110}
111
112impl AsRef<str> for OrganSystemRef {
113    fn as_ref(&self) -> &str {
114        self.as_str()
115    }
116}
117
118impl fmt::Display for OrganSystemRef {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        formatter.write_str(self.as_str())
121    }
122}
123
124impl FromStr for OrganSystemRef {
125    type Err = OrganNameError;
126
127    fn from_str(value: &str) -> Result<Self, Self::Err> {
128        Self::new(value)
129    }
130}
131
132/// Broad animal and plant organ vocabulary.
133#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum OrganKind {
135    /// Heart.
136    Heart,
137    /// Lung.
138    Lung,
139    /// Brain.
140    Brain,
141    /// Liver.
142    Liver,
143    /// Kidney.
144    Kidney,
145    /// Leaf.
146    Leaf,
147    /// Root.
148    Root,
149    /// Stem.
150    Stem,
151    /// Flower.
152    Flower,
153    /// Seed.
154    Seed,
155    /// Unknown organ kind.
156    Unknown,
157    /// Caller-defined organ kind text.
158    Custom(String),
159}
160
161impl fmt::Display for OrganKind {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Heart => formatter.write_str("heart"),
165            Self::Lung => formatter.write_str("lung"),
166            Self::Brain => formatter.write_str("brain"),
167            Self::Liver => formatter.write_str("liver"),
168            Self::Kidney => formatter.write_str("kidney"),
169            Self::Leaf => formatter.write_str("leaf"),
170            Self::Root => formatter.write_str("root"),
171            Self::Stem => formatter.write_str("stem"),
172            Self::Flower => formatter.write_str("flower"),
173            Self::Seed => formatter.write_str("seed"),
174            Self::Unknown => formatter.write_str("unknown"),
175            Self::Custom(value) => formatter.write_str(value),
176        }
177    }
178}
179
180impl FromStr for OrganKind {
181    type Err = OrganKindParseError;
182
183    fn from_str(value: &str) -> Result<Self, Self::Err> {
184        let trimmed = value.trim();
185
186        if trimmed.is_empty() {
187            return Err(OrganKindParseError::Empty);
188        }
189
190        match normalized_key(trimmed).as_str() {
191            "heart" => Ok(Self::Heart),
192            "lung" | "lungs" => Ok(Self::Lung),
193            "brain" => Ok(Self::Brain),
194            "liver" => Ok(Self::Liver),
195            "kidney" => Ok(Self::Kidney),
196            "leaf" | "leaves" => Ok(Self::Leaf),
197            "root" => Ok(Self::Root),
198            "stem" => Ok(Self::Stem),
199            "flower" => Ok(Self::Flower),
200            "seed" => Ok(Self::Seed),
201            "unknown" => Ok(Self::Unknown),
202            _ => Ok(Self::Custom(trimmed.to_string())),
203        }
204    }
205}
206
207/// Error returned when parsing organ kinds fails.
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum OrganKindParseError {
210    /// The organ kind was empty after trimming whitespace.
211    Empty,
212}
213
214impl fmt::Display for OrganKindParseError {
215    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            Self::Empty => formatter.write_str("organ kind cannot be empty"),
218        }
219    }
220}
221
222impl Error for OrganKindParseError {}
223
224#[cfg(test)]
225mod tests {
226    use super::{OrganKind, OrganKindParseError, OrganName, OrganNameError};
227
228    #[test]
229    fn constructs_valid_organ_name() -> Result<(), OrganNameError> {
230        let name = OrganName::new("leaf")?;
231
232        assert_eq!(name.as_str(), "leaf");
233        Ok(())
234    }
235
236    #[test]
237    fn rejects_empty_organ_name() {
238        assert_eq!(OrganName::new(""), Err(OrganNameError::Empty));
239    }
240
241    #[test]
242    fn displays_and_parses_organ_kind() -> Result<(), OrganKindParseError> {
243        assert_eq!(OrganKind::Heart.to_string(), "heart");
244        assert_eq!("kidney".parse::<OrganKind>()?, OrganKind::Kidney);
245        Ok(())
246    }
247
248    #[test]
249    fn parses_plant_organ_variants() -> Result<(), OrganKindParseError> {
250        assert_eq!("leaf".parse::<OrganKind>()?, OrganKind::Leaf);
251        assert_eq!("root".parse::<OrganKind>()?, OrganKind::Root);
252        assert_eq!("flower".parse::<OrganKind>()?, OrganKind::Flower);
253        Ok(())
254    }
255
256    #[test]
257    fn parses_custom_organ_kind() -> Result<(), OrganKindParseError> {
258        assert_eq!(
259            "hypha".parse::<OrganKind>()?,
260            OrganKind::Custom("hypha".to_string())
261        );
262        assert_eq!(" ".parse::<OrganKind>(), Err(OrganKindParseError::Empty));
263        Ok(())
264    }
265}