Skip to main content

use_life_stage/
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/// Life stage vocabulary for animals, plants, fungi, and microbes.
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum LifeStage {
14    /// Embryo stage.
15    Embryo,
16    /// Larval stage.
17    Larva,
18    /// Juvenile stage.
19    Juvenile,
20    /// Adult stage.
21    Adult,
22    /// Senescent stage.
23    Senescent,
24    /// Seed stage.
25    Seed,
26    /// Seedling stage.
27    Seedling,
28    /// Vegetative stage.
29    Vegetative,
30    /// Flowering stage.
31    Flowering,
32    /// Fruiting stage.
33    Fruiting,
34    /// Spore stage.
35    Spore,
36    /// Unknown stage.
37    Unknown,
38    /// Caller-defined life stage text.
39    Custom(String),
40}
41
42impl fmt::Display for LifeStage {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Embryo => formatter.write_str("embryo"),
46            Self::Larva => formatter.write_str("larva"),
47            Self::Juvenile => formatter.write_str("juvenile"),
48            Self::Adult => formatter.write_str("adult"),
49            Self::Senescent => formatter.write_str("senescent"),
50            Self::Seed => formatter.write_str("seed"),
51            Self::Seedling => formatter.write_str("seedling"),
52            Self::Vegetative => formatter.write_str("vegetative"),
53            Self::Flowering => formatter.write_str("flowering"),
54            Self::Fruiting => formatter.write_str("fruiting"),
55            Self::Spore => formatter.write_str("spore"),
56            Self::Unknown => formatter.write_str("unknown"),
57            Self::Custom(value) => formatter.write_str(value),
58        }
59    }
60}
61
62impl FromStr for LifeStage {
63    type Err = LifeStageParseError;
64
65    fn from_str(value: &str) -> Result<Self, Self::Err> {
66        let trimmed = value.trim();
67
68        if trimmed.is_empty() {
69            return Err(LifeStageParseError::Empty);
70        }
71
72        match normalized_key(trimmed).as_str() {
73            "embryo" => Ok(Self::Embryo),
74            "larva" | "larval" => Ok(Self::Larva),
75            "juvenile" => Ok(Self::Juvenile),
76            "adult" => Ok(Self::Adult),
77            "senescent" => Ok(Self::Senescent),
78            "seed" => Ok(Self::Seed),
79            "seedling" => Ok(Self::Seedling),
80            "vegetative" => Ok(Self::Vegetative),
81            "flowering" => Ok(Self::Flowering),
82            "fruiting" => Ok(Self::Fruiting),
83            "spore" => Ok(Self::Spore),
84            "unknown" => Ok(Self::Unknown),
85            _ => Ok(Self::Custom(trimmed.to_string())),
86        }
87    }
88}
89
90/// Error returned when parsing life stages fails.
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum LifeStageParseError {
93    /// The life stage was empty after trimming whitespace.
94    Empty,
95}
96
97impl fmt::Display for LifeStageParseError {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Empty => formatter.write_str("life stage cannot be empty"),
101        }
102    }
103}
104
105impl Error for LifeStageParseError {}
106
107/// Error returned when development stage labels are empty.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum DevelopmentStageError {
110    /// The supplied label was empty after trimming whitespace.
111    Empty,
112}
113
114impl fmt::Display for DevelopmentStageError {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Empty => formatter.write_str("development stage label cannot be empty"),
118        }
119    }
120}
121
122impl Error for DevelopmentStageError {}
123
124/// A descriptive development stage record.
125#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct DevelopmentStage {
127    stage: LifeStage,
128    label: Option<String>,
129}
130
131impl DevelopmentStage {
132    /// Creates a development stage from a life stage.
133    #[must_use]
134    pub const fn new(stage: LifeStage) -> Self {
135        Self { stage, label: None }
136    }
137
138    /// Returns the life stage.
139    #[must_use]
140    pub const fn stage(&self) -> &LifeStage {
141        &self.stage
142    }
143
144    /// Returns the optional descriptive label.
145    #[must_use]
146    pub fn label(&self) -> Option<&str> {
147        self.label.as_deref()
148    }
149
150    /// Adds a descriptive non-empty label.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`DevelopmentStageError::Empty`] when the trimmed label is empty.
155    pub fn with_label(mut self, label: impl AsRef<str>) -> Result<Self, DevelopmentStageError> {
156        let trimmed = label.as_ref().trim();
157
158        if trimmed.is_empty() {
159            return Err(DevelopmentStageError::Empty);
160        }
161
162        self.label = Some(trimmed.to_string());
163        Ok(self)
164    }
165}
166
167impl fmt::Display for DevelopmentStage {
168    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self.label() {
170            Some(label) => write!(formatter, "{}: {label}", self.stage),
171            None => self.stage.fmt(formatter),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{DevelopmentStage, DevelopmentStageError, LifeStage, LifeStageParseError};
179
180    #[test]
181    fn displays_and_parses_life_stage() -> Result<(), LifeStageParseError> {
182        assert_eq!(LifeStage::Adult.to_string(), "adult");
183        assert_eq!("larval".parse::<LifeStage>()?, LifeStage::Larva);
184        assert_eq!("spore".parse::<LifeStage>()?, LifeStage::Spore);
185        Ok(())
186    }
187
188    #[test]
189    fn parses_custom_life_stage() -> Result<(), LifeStageParseError> {
190        assert_eq!(
191            "dormant".parse::<LifeStage>()?,
192            LifeStage::Custom("dormant".to_string())
193        );
194        assert_eq!("".parse::<LifeStage>(), Err(LifeStageParseError::Empty));
195        Ok(())
196    }
197
198    #[test]
199    fn parses_plant_stage_variants() -> Result<(), LifeStageParseError> {
200        assert_eq!("seed".parse::<LifeStage>()?, LifeStage::Seed);
201        assert_eq!("seedling".parse::<LifeStage>()?, LifeStage::Seedling);
202        assert_eq!("flowering".parse::<LifeStage>()?, LifeStage::Flowering);
203        Ok(())
204    }
205
206    #[test]
207    fn parses_animal_stage_variants() -> Result<(), LifeStageParseError> {
208        assert_eq!("embryo".parse::<LifeStage>()?, LifeStage::Embryo);
209        assert_eq!("juvenile".parse::<LifeStage>()?, LifeStage::Juvenile);
210        assert_eq!("adult".parse::<LifeStage>()?, LifeStage::Adult);
211        Ok(())
212    }
213
214    #[test]
215    fn constructs_development_stage() -> Result<(), DevelopmentStageError> {
216        let stage = DevelopmentStage::new(LifeStage::Vegetative).with_label("rosette")?;
217
218        assert_eq!(stage.stage(), &LifeStage::Vegetative);
219        assert_eq!(stage.label(), Some("rosette"));
220        assert_eq!(stage.to_string(), "vegetative: rosette");
221        Ok(())
222    }
223}