Skip to main content

use_organism/
lib.rs

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/// Error returned when organism labels are empty.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum OrganismNameError {
27    /// The supplied value was empty after trimming surrounding whitespace.
28    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/// A stable organism identifier string.
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct OrganismId(String);
44
45impl OrganismId {
46    /// Creates an organism identifier from non-empty text.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`OrganismNameError::Empty`] when the trimmed identifier is empty.
51    pub fn new(value: impl AsRef<str>) -> Result<Self, OrganismNameError> {
52        non_empty_text(value).map(Self)
53    }
54
55    /// Returns the identifier text.
56    #[must_use]
57    pub fn as_str(&self) -> &str {
58        &self.0
59    }
60
61    /// Consumes the identifier and returns the owned string.
62    #[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/// A non-empty organism name.
89#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub struct OrganismName(String);
91
92impl OrganismName {
93    /// Creates an organism name from non-empty text.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`OrganismNameError::Empty`] when the trimmed name is empty.
98    pub fn new(value: impl AsRef<str>) -> Result<Self, OrganismNameError> {
99        non_empty_text(value).map(Self)
100    }
101
102    /// Returns the organism name text.
103    #[must_use]
104    pub fn as_str(&self) -> &str {
105        &self.0
106    }
107
108    /// Consumes the name and returns the owned string.
109    #[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/// Broad organism vocabulary. The `Virus` variant is descriptive and does not assert whether viruses are living organisms.
136#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
137pub enum OrganismKind {
138    /// Animal organisms.
139    Animal,
140    /// Plant organisms.
141    Plant,
142    /// Fungi.
143    Fungus,
144    /// Bacteria.
145    Bacterium,
146    /// Archaea.
147    Archaeon,
148    /// Protists.
149    Protist,
150    /// Viruses, modeled descriptively.
151    Virus,
152    /// Unknown organism kind.
153    Unknown,
154    /// Caller-defined organism kind text.
155    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/// Error returned when parsing organism kinds fails.
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub enum OrganismKindParseError {
201    /// The organism kind was empty after trimming whitespace.
202    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/// A descriptive organism classification record.
216#[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    /// Creates a classification from a broad organism kind.
225    #[must_use]
226    pub const fn new(kind: OrganismKind) -> Self {
227        Self {
228            kind,
229            taxon: None,
230            species: None,
231        }
232    }
233
234    /// Returns the broad organism kind.
235    #[must_use]
236    pub const fn kind(&self) -> &OrganismKind {
237        &self.kind
238    }
239
240    /// Returns the optional taxonomy primitive.
241    #[must_use]
242    pub const fn taxon(&self) -> Option<&Taxon> {
243        self.taxon.as_ref()
244    }
245
246    /// Returns the optional species primitive.
247    #[must_use]
248    pub const fn species(&self) -> Option<&SpeciesName> {
249        self.species.as_ref()
250    }
251
252    /// Adds a taxonomy primitive.
253    #[must_use]
254    pub fn with_taxon(mut self, taxon: Taxon) -> Self {
255        self.taxon = Some(taxon);
256        self
257    }
258
259    /// Adds a species primitive.
260    #[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}