1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_species::SpeciesName;
8use use_taxonomy::Taxon;
9
10fn non_empty_text(value: impl AsRef<str>) -> Result<String, OrganismNameError> {
11 let trimmed = value.as_ref().trim();
12
13 if trimmed.is_empty() {
14 Err(OrganismNameError::Empty)
15 } else {
16 Ok(trimmed.to_string())
17 }
18}
19
20fn normalized_key(value: &str) -> String {
21 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum OrganismNameError {
27 Empty,
29}
30
31impl fmt::Display for OrganismNameError {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 Self::Empty => formatter.write_str("organism label cannot be empty"),
35 }
36 }
37}
38
39impl Error for OrganismNameError {}
40
41#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct OrganismId(String);
44
45impl OrganismId {
46 pub fn new(value: impl AsRef<str>) -> Result<Self, OrganismNameError> {
52 non_empty_text(value).map(Self)
53 }
54
55 #[must_use]
57 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60
61 #[must_use]
63 pub fn into_string(self) -> String {
64 self.0
65 }
66}
67
68impl AsRef<str> for OrganismId {
69 fn as_ref(&self) -> &str {
70 self.as_str()
71 }
72}
73
74impl fmt::Display for OrganismId {
75 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76 formatter.write_str(self.as_str())
77 }
78}
79
80impl FromStr for OrganismId {
81 type Err = OrganismNameError;
82
83 fn from_str(value: &str) -> Result<Self, Self::Err> {
84 Self::new(value)
85 }
86}
87
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub struct OrganismName(String);
91
92impl OrganismName {
93 pub fn new(value: impl AsRef<str>) -> Result<Self, OrganismNameError> {
99 non_empty_text(value).map(Self)
100 }
101
102 #[must_use]
104 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107
108 #[must_use]
110 pub fn into_string(self) -> String {
111 self.0
112 }
113}
114
115impl AsRef<str> for OrganismName {
116 fn as_ref(&self) -> &str {
117 self.as_str()
118 }
119}
120
121impl fmt::Display for OrganismName {
122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123 formatter.write_str(self.as_str())
124 }
125}
126
127impl FromStr for OrganismName {
128 type Err = OrganismNameError;
129
130 fn from_str(value: &str) -> Result<Self, Self::Err> {
131 Self::new(value)
132 }
133}
134
135#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137pub enum OrganismKind {
138 Animal,
140 Plant,
142 Fungus,
144 Bacterium,
146 Archaeon,
148 Protist,
150 Virus,
152 Unknown,
154 Custom(String),
156}
157
158impl fmt::Display for OrganismKind {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 match self {
161 Self::Animal => formatter.write_str("animal"),
162 Self::Plant => formatter.write_str("plant"),
163 Self::Fungus => formatter.write_str("fungus"),
164 Self::Bacterium => formatter.write_str("bacterium"),
165 Self::Archaeon => formatter.write_str("archaeon"),
166 Self::Protist => formatter.write_str("protist"),
167 Self::Virus => formatter.write_str("virus"),
168 Self::Unknown => formatter.write_str("unknown"),
169 Self::Custom(value) => formatter.write_str(value),
170 }
171 }
172}
173
174impl FromStr for OrganismKind {
175 type Err = OrganismKindParseError;
176
177 fn from_str(value: &str) -> Result<Self, Self::Err> {
178 let trimmed = value.trim();
179
180 if trimmed.is_empty() {
181 return Err(OrganismKindParseError::Empty);
182 }
183
184 match normalized_key(trimmed).as_str() {
185 "animal" | "animals" => Ok(Self::Animal),
186 "plant" | "plants" => Ok(Self::Plant),
187 "fungus" | "fungi" => Ok(Self::Fungus),
188 "bacterium" | "bacteria" => Ok(Self::Bacterium),
189 "archaeon" | "archaea" => Ok(Self::Archaeon),
190 "protist" | "protists" => Ok(Self::Protist),
191 "virus" | "viruses" => Ok(Self::Virus),
192 "unknown" => Ok(Self::Unknown),
193 _ => Ok(Self::Custom(trimmed.to_string())),
194 }
195 }
196}
197
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub enum OrganismKindParseError {
201 Empty,
203}
204
205impl fmt::Display for OrganismKindParseError {
206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::Empty => formatter.write_str("organism kind cannot be empty"),
209 }
210 }
211}
212
213impl Error for OrganismKindParseError {}
214
215#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
217pub struct OrganismClassification {
218 kind: OrganismKind,
219 taxon: Option<Taxon>,
220 species: Option<SpeciesName>,
221}
222
223impl OrganismClassification {
224 #[must_use]
226 pub const fn new(kind: OrganismKind) -> Self {
227 Self {
228 kind,
229 taxon: None,
230 species: None,
231 }
232 }
233
234 #[must_use]
236 pub const fn kind(&self) -> &OrganismKind {
237 &self.kind
238 }
239
240 #[must_use]
242 pub const fn taxon(&self) -> Option<&Taxon> {
243 self.taxon.as_ref()
244 }
245
246 #[must_use]
248 pub const fn species(&self) -> Option<&SpeciesName> {
249 self.species.as_ref()
250 }
251
252 #[must_use]
254 pub fn with_taxon(mut self, taxon: Taxon) -> Self {
255 self.taxon = Some(taxon);
256 self
257 }
258
259 #[must_use]
261 pub fn with_species(mut self, species: SpeciesName) -> Self {
262 self.species = Some(species);
263 self
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::{
270 OrganismClassification, OrganismKind, OrganismKindParseError, OrganismName,
271 OrganismNameError,
272 };
273 use use_species::{BinomialName, GenusName, SpeciesName, SpecificEpithet};
274 use use_taxonomy::{Taxon, TaxonName, TaxonomicRank};
275
276 #[test]
277 fn constructs_valid_organism_name() -> Result<(), OrganismNameError> {
278 let name = OrganismName::new("Arabidopsis")?;
279
280 assert_eq!(name.as_str(), "Arabidopsis");
281 assert_eq!(name.to_string(), "Arabidopsis");
282 Ok(())
283 }
284
285 #[test]
286 fn rejects_empty_organism_name() {
287 assert_eq!(OrganismName::new(" "), Err(OrganismNameError::Empty));
288 }
289
290 #[test]
291 fn displays_and_parses_organism_kinds() -> Result<(), OrganismKindParseError> {
292 assert_eq!(OrganismKind::Bacterium.to_string(), "bacterium");
293 assert_eq!("plants".parse::<OrganismKind>()?, OrganismKind::Plant);
294 assert_eq!("viruses".parse::<OrganismKind>()?, OrganismKind::Virus);
295 Ok(())
296 }
297
298 #[test]
299 fn parses_custom_organism_kind() -> Result<(), OrganismKindParseError> {
300 assert_eq!(
301 "lichen-forming association".parse::<OrganismKind>()?,
302 OrganismKind::Custom("lichen-forming association".to_string())
303 );
304 assert_eq!(
305 "".parse::<OrganismKind>(),
306 Err(OrganismKindParseError::Empty)
307 );
308 Ok(())
309 }
310
311 #[test]
312 fn constructs_organism_classification() {
313 let taxon = Taxon::new(
314 TaxonomicRank::Genus,
315 TaxonName::new("Homo").expect("valid taxon"),
316 );
317 let species = SpeciesName::from(BinomialName::new(
318 GenusName::new("Homo").expect("valid genus"),
319 SpecificEpithet::new("sapiens").expect("valid epithet"),
320 ));
321 let classification = OrganismClassification::new(OrganismKind::Animal)
322 .with_taxon(taxon.clone())
323 .with_species(species.clone());
324
325 assert_eq!(classification.kind(), &OrganismKind::Animal);
326 assert_eq!(classification.taxon(), Some(&taxon));
327 assert_eq!(classification.species(), Some(&species));
328 }
329}