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, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum LifeStage {
14 Embryo,
16 Larva,
18 Juvenile,
20 Adult,
22 Senescent,
24 Seed,
26 Seedling,
28 Vegetative,
30 Flowering,
32 Fruiting,
34 Spore,
36 Unknown,
38 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum LifeStageParseError {
93 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum DevelopmentStageError {
110 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct DevelopmentStage {
127 stage: LifeStage,
128 label: Option<String>,
129}
130
131impl DevelopmentStage {
132 #[must_use]
134 pub const fn new(stage: LifeStage) -> Self {
135 Self { stage, label: None }
136 }
137
138 #[must_use]
140 pub const fn stage(&self) -> &LifeStage {
141 &self.stage
142 }
143
144 #[must_use]
146 pub fn label(&self) -> Option<&str> {
147 self.label.as_deref()
148 }
149
150 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}