Skip to main content

use_biological_system/
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 biological system names are empty.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum BiologicalSystemNameError {
14    /// The supplied system name was empty after trimming whitespace.
15    Empty,
16}
17
18impl fmt::Display for BiologicalSystemNameError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => formatter.write_str("biological system name cannot be empty"),
22        }
23    }
24}
25
26impl Error for BiologicalSystemNameError {}
27
28/// Biological system kind vocabulary.
29#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub enum BiologicalSystemKind {
31    /// Nervous system.
32    Nervous,
33    /// Circulatory system.
34    Circulatory,
35    /// Respiratory system.
36    Respiratory,
37    /// Digestive system.
38    Digestive,
39    /// Endocrine system.
40    Endocrine,
41    /// Immune system.
42    Immune,
43    /// Reproductive system.
44    Reproductive,
45    /// Musculoskeletal system.
46    Musculoskeletal,
47    /// Plant root system.
48    RootSystem,
49    /// Plant shoot system.
50    ShootSystem,
51    /// Vascular system.
52    VascularSystem,
53    /// Unknown biological system kind.
54    Unknown,
55    /// Caller-defined biological system kind text.
56    Custom(String),
57}
58
59impl fmt::Display for BiologicalSystemKind {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::Nervous => formatter.write_str("nervous"),
63            Self::Circulatory => formatter.write_str("circulatory"),
64            Self::Respiratory => formatter.write_str("respiratory"),
65            Self::Digestive => formatter.write_str("digestive"),
66            Self::Endocrine => formatter.write_str("endocrine"),
67            Self::Immune => formatter.write_str("immune"),
68            Self::Reproductive => formatter.write_str("reproductive"),
69            Self::Musculoskeletal => formatter.write_str("musculoskeletal"),
70            Self::RootSystem => formatter.write_str("root-system"),
71            Self::ShootSystem => formatter.write_str("shoot-system"),
72            Self::VascularSystem => formatter.write_str("vascular-system"),
73            Self::Unknown => formatter.write_str("unknown"),
74            Self::Custom(value) => formatter.write_str(value),
75        }
76    }
77}
78
79impl FromStr for BiologicalSystemKind {
80    type Err = BiologicalSystemKindParseError;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        let trimmed = value.trim();
84
85        if trimmed.is_empty() {
86            return Err(BiologicalSystemKindParseError::Empty);
87        }
88
89        match normalized_key(trimmed).as_str() {
90            "nervous" | "nervous-system" => Ok(Self::Nervous),
91            "circulatory" | "circulatory-system" => Ok(Self::Circulatory),
92            "respiratory" | "respiratory-system" => Ok(Self::Respiratory),
93            "digestive" | "digestive-system" => Ok(Self::Digestive),
94            "endocrine" | "endocrine-system" => Ok(Self::Endocrine),
95            "immune" | "immune-system" => Ok(Self::Immune),
96            "reproductive" | "reproductive-system" => Ok(Self::Reproductive),
97            "musculoskeletal" | "musculoskeletal-system" => Ok(Self::Musculoskeletal),
98            "root-system" => Ok(Self::RootSystem),
99            "shoot-system" => Ok(Self::ShootSystem),
100            "vascular-system" => Ok(Self::VascularSystem),
101            "unknown" => Ok(Self::Unknown),
102            _ => Ok(Self::Custom(trimmed.to_string())),
103        }
104    }
105}
106
107/// Error returned when parsing biological system kinds fails.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum BiologicalSystemKindParseError {
110    /// The biological system kind was empty after trimming whitespace.
111    Empty,
112}
113
114impl fmt::Display for BiologicalSystemKindParseError {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Empty => formatter.write_str("biological system kind cannot be empty"),
118        }
119    }
120}
121
122impl Error for BiologicalSystemKindParseError {}
123
124/// A descriptive biological system record.
125#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct BiologicalSystem {
127    name: String,
128    kind: BiologicalSystemKind,
129}
130
131impl BiologicalSystem {
132    /// Creates a biological system from non-empty name text and a kind.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`BiologicalSystemNameError::Empty`] when the trimmed name is empty.
137    pub fn new(
138        name: impl AsRef<str>,
139        kind: BiologicalSystemKind,
140    ) -> Result<Self, BiologicalSystemNameError> {
141        let trimmed = name.as_ref().trim();
142
143        if trimmed.is_empty() {
144            return Err(BiologicalSystemNameError::Empty);
145        }
146
147        Ok(Self {
148            name: trimmed.to_string(),
149            kind,
150        })
151    }
152
153    /// Returns the system name.
154    #[must_use]
155    pub fn name(&self) -> &str {
156        &self.name
157    }
158
159    /// Returns the system kind.
160    #[must_use]
161    pub const fn kind(&self) -> &BiologicalSystemKind {
162        &self.kind
163    }
164}
165
166impl fmt::Display for BiologicalSystem {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str(self.name())
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::{
175        BiologicalSystem, BiologicalSystemKind, BiologicalSystemKindParseError,
176        BiologicalSystemNameError,
177    };
178
179    #[test]
180    fn constructs_valid_biological_system_name() -> Result<(), BiologicalSystemNameError> {
181        let system = BiologicalSystem::new("root system", BiologicalSystemKind::RootSystem)?;
182
183        assert_eq!(system.name(), "root system");
184        assert_eq!(system.kind(), &BiologicalSystemKind::RootSystem);
185        Ok(())
186    }
187
188    #[test]
189    fn rejects_empty_system_name() {
190        assert_eq!(
191            BiologicalSystem::new("   ", BiologicalSystemKind::Unknown),
192            Err(BiologicalSystemNameError::Empty)
193        );
194    }
195
196    #[test]
197    fn displays_and_parses_system_kind() -> Result<(), BiologicalSystemKindParseError> {
198        assert_eq!(BiologicalSystemKind::Nervous.to_string(), "nervous");
199        assert_eq!(
200            "respiratory system".parse::<BiologicalSystemKind>()?,
201            BiologicalSystemKind::Respiratory
202        );
203        Ok(())
204    }
205
206    #[test]
207    fn parses_plant_system_variants() -> Result<(), BiologicalSystemKindParseError> {
208        assert_eq!(
209            "root system".parse::<BiologicalSystemKind>()?,
210            BiologicalSystemKind::RootSystem
211        );
212        assert_eq!(
213            "shoot-system".parse::<BiologicalSystemKind>()?,
214            BiologicalSystemKind::ShootSystem
215        );
216        assert_eq!(
217            "vascular system".parse::<BiologicalSystemKind>()?,
218            BiologicalSystemKind::VascularSystem
219        );
220        Ok(())
221    }
222
223    #[test]
224    fn parses_custom_system_kind() -> Result<(), BiologicalSystemKindParseError> {
225        assert_eq!(
226            "water-vascular".parse::<BiologicalSystemKind>()?,
227            BiologicalSystemKind::Custom("water-vascular".to_string())
228        );
229        assert_eq!(
230            "".parse::<BiologicalSystemKind>(),
231            Err(BiologicalSystemKindParseError::Empty)
232        );
233        Ok(())
234    }
235}