Skip to main content

use_tissue/
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
11/// Error returned when tissue names are empty.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum TissueNameError {
14    /// The supplied tissue name was empty after trimming whitespace.
15    Empty,
16}
17
18impl fmt::Display for TissueNameError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => formatter.write_str("tissue name cannot be empty"),
22        }
23    }
24}
25
26impl Error for TissueNameError {}
27
28/// A non-empty tissue name.
29#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub struct TissueName(String);
31
32impl TissueName {
33    /// Creates a tissue name from non-empty text.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`TissueNameError::Empty`] when the trimmed name is empty.
38    pub fn new(value: impl AsRef<str>) -> Result<Self, TissueNameError> {
39        let trimmed = value.as_ref().trim();
40
41        if trimmed.is_empty() {
42            Err(TissueNameError::Empty)
43        } else {
44            Ok(Self(trimmed.to_string()))
45        }
46    }
47
48    /// Returns the tissue name text.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Consumes the name and returns the owned string.
55    #[must_use]
56    pub fn into_string(self) -> String {
57        self.0
58    }
59}
60
61impl AsRef<str> for TissueName {
62    fn as_ref(&self) -> &str {
63        self.as_str()
64    }
65}
66
67impl fmt::Display for TissueName {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter.write_str(self.as_str())
70    }
71}
72
73impl FromStr for TissueName {
74    type Err = TissueNameError;
75
76    fn from_str(value: &str) -> Result<Self, Self::Err> {
77        Self::new(value)
78    }
79}
80
81/// Animal and plant tissue vocabulary.
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub enum TissueKind {
84    /// Epithelial tissue.
85    Epithelial,
86    /// Connective tissue.
87    Connective,
88    /// Muscle tissue.
89    Muscle,
90    /// Nervous tissue.
91    Nervous,
92    /// Vascular tissue.
93    Vascular,
94    /// Dermal tissue.
95    Dermal,
96    /// Ground tissue.
97    Ground,
98    /// Meristematic tissue.
99    Meristematic,
100    /// Unknown tissue kind.
101    Unknown,
102    /// Caller-defined tissue kind text.
103    Custom(String),
104}
105
106impl fmt::Display for TissueKind {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Epithelial => formatter.write_str("epithelial"),
110            Self::Connective => formatter.write_str("connective"),
111            Self::Muscle => formatter.write_str("muscle"),
112            Self::Nervous => formatter.write_str("nervous"),
113            Self::Vascular => formatter.write_str("vascular"),
114            Self::Dermal => formatter.write_str("dermal"),
115            Self::Ground => formatter.write_str("ground"),
116            Self::Meristematic => formatter.write_str("meristematic"),
117            Self::Unknown => formatter.write_str("unknown"),
118            Self::Custom(value) => formatter.write_str(value),
119        }
120    }
121}
122
123impl FromStr for TissueKind {
124    type Err = TissueKindParseError;
125
126    fn from_str(value: &str) -> Result<Self, Self::Err> {
127        let trimmed = value.trim();
128
129        if trimmed.is_empty() {
130            return Err(TissueKindParseError::Empty);
131        }
132
133        match normalized_key(trimmed).as_str() {
134            "epithelial" => Ok(Self::Epithelial),
135            "connective" => Ok(Self::Connective),
136            "muscle" => Ok(Self::Muscle),
137            "nervous" => Ok(Self::Nervous),
138            "vascular" => Ok(Self::Vascular),
139            "dermal" => Ok(Self::Dermal),
140            "ground" => Ok(Self::Ground),
141            "meristematic" => Ok(Self::Meristematic),
142            "unknown" => Ok(Self::Unknown),
143            _ => Ok(Self::Custom(trimmed.to_string())),
144        }
145    }
146}
147
148/// Error returned when parsing tissue kinds fails.
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum TissueKindParseError {
151    /// The tissue kind was empty after trimming whitespace.
152    Empty,
153}
154
155impl fmt::Display for TissueKindParseError {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            Self::Empty => formatter.write_str("tissue kind cannot be empty"),
159        }
160    }
161}
162
163impl Error for TissueKindParseError {}
164
165#[cfg(test)]
166mod tests {
167    use super::{TissueKind, TissueKindParseError, TissueName, TissueNameError};
168
169    #[test]
170    fn constructs_valid_tissue_name() -> Result<(), TissueNameError> {
171        let name = TissueName::new("xylem")?;
172
173        assert_eq!(name.as_str(), "xylem");
174        Ok(())
175    }
176
177    #[test]
178    fn rejects_empty_tissue_name() {
179        assert_eq!(TissueName::new("  "), Err(TissueNameError::Empty));
180    }
181
182    #[test]
183    fn displays_and_parses_tissue_kind() -> Result<(), TissueKindParseError> {
184        assert_eq!(TissueKind::Epithelial.to_string(), "epithelial");
185        assert_eq!("ground".parse::<TissueKind>()?, TissueKind::Ground);
186        assert_eq!(
187            "meristematic".parse::<TissueKind>()?,
188            TissueKind::Meristematic
189        );
190        Ok(())
191    }
192
193    #[test]
194    fn parses_custom_tissue_kind() -> Result<(), TissueKindParseError> {
195        assert_eq!(
196            "cartilaginous".parse::<TissueKind>()?,
197            TissueKind::Custom("cartilaginous".to_string())
198        );
199        assert_eq!("".parse::<TissueKind>(), Err(TissueKindParseError::Empty));
200        Ok(())
201    }
202}