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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum BiologicalSystemNameError {
14 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub enum BiologicalSystemKind {
31 Nervous,
33 Circulatory,
35 Respiratory,
37 Digestive,
39 Endocrine,
41 Immune,
43 Reproductive,
45 Musculoskeletal,
47 RootSystem,
49 ShootSystem,
51 VascularSystem,
53 Unknown,
55 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum BiologicalSystemKindParseError {
110 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct BiologicalSystem {
127 name: String,
128 kind: BiologicalSystemKind,
129}
130
131impl BiologicalSystem {
132 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 #[must_use]
155 pub fn name(&self) -> &str {
156 &self.name
157 }
158
159 #[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}