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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum OrganNameError {
24 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct OrganName(String);
41
42impl OrganName {
43 pub fn new(value: impl AsRef<str>) -> Result<Self, OrganNameError> {
49 non_empty_text(value).map(Self)
50 }
51
52 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57
58 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub struct OrganSystemRef(String);
88
89impl OrganSystemRef {
90 pub fn new(value: impl AsRef<str>) -> Result<Self, OrganNameError> {
96 non_empty_text(value).map(Self)
97 }
98
99 #[must_use]
101 pub fn as_str(&self) -> &str {
102 &self.0
103 }
104
105 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum OrganKind {
135 Heart,
137 Lung,
139 Brain,
141 Liver,
143 Kidney,
145 Leaf,
147 Root,
149 Stem,
151 Flower,
153 Seed,
155 Unknown,
157 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum OrganKindParseError {
210 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}