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, TraitNameError> {
12 let trimmed = value.as_ref().trim();
13
14 if trimmed.is_empty() {
15 Err(TraitNameError::Empty)
16 } else {
17 Ok(trimmed.to_string())
18 }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum TraitNameError {
24 Empty,
26}
27
28impl fmt::Display for TraitNameError {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Empty => formatter.write_str("trait text cannot be empty"),
32 }
33 }
34}
35
36impl Error for TraitNameError {}
37
38#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct TraitName(String);
41
42impl TraitName {
43 pub fn new(value: impl AsRef<str>) -> Result<Self, TraitNameError> {
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 TraitName {
66 fn as_ref(&self) -> &str {
67 self.as_str()
68 }
69}
70
71impl fmt::Display for TraitName {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 formatter.write_str(self.as_str())
74 }
75}
76
77impl FromStr for TraitName {
78 type Err = TraitNameError;
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 enum TraitKind {
88 Morphological,
90 Physiological,
92 Behavioral,
94 Genetic,
96 Developmental,
98 Ecological,
100 Unknown,
102 Custom(String),
104}
105
106impl fmt::Display for TraitKind {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::Morphological => formatter.write_str("morphological"),
110 Self::Physiological => formatter.write_str("physiological"),
111 Self::Behavioral => formatter.write_str("behavioral"),
112 Self::Genetic => formatter.write_str("genetic"),
113 Self::Developmental => formatter.write_str("developmental"),
114 Self::Ecological => formatter.write_str("ecological"),
115 Self::Unknown => formatter.write_str("unknown"),
116 Self::Custom(value) => formatter.write_str(value),
117 }
118 }
119}
120
121impl FromStr for TraitKind {
122 type Err = TraitKindParseError;
123
124 fn from_str(value: &str) -> Result<Self, Self::Err> {
125 let trimmed = value.trim();
126
127 if trimmed.is_empty() {
128 return Err(TraitKindParseError::Empty);
129 }
130
131 match normalized_key(trimmed).as_str() {
132 "morphological" => Ok(Self::Morphological),
133 "physiological" => Ok(Self::Physiological),
134 "behavioral" | "behavioural" => Ok(Self::Behavioral),
135 "genetic" => Ok(Self::Genetic),
136 "developmental" => Ok(Self::Developmental),
137 "ecological" => Ok(Self::Ecological),
138 "unknown" => Ok(Self::Unknown),
139 _ => Ok(Self::Custom(trimmed.to_string())),
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum TraitKindParseError {
147 Empty,
149}
150
151impl fmt::Display for TraitKindParseError {
152 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153 match self {
154 Self::Empty => formatter.write_str("trait kind cannot be empty"),
155 }
156 }
157}
158
159impl Error for TraitKindParseError {}
160
161#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub struct TraitValue {
164 name: TraitName,
165 value: String,
166 kind: Option<TraitKind>,
167}
168
169impl TraitValue {
170 pub fn new(name: TraitName, value: impl AsRef<str>) -> Result<Self, TraitNameError> {
176 Ok(Self {
177 name,
178 value: non_empty_text(value)?,
179 kind: None,
180 })
181 }
182
183 #[must_use]
185 pub const fn name(&self) -> &TraitName {
186 &self.name
187 }
188
189 #[must_use]
191 pub fn value(&self) -> &str {
192 &self.value
193 }
194
195 #[must_use]
197 pub const fn kind(&self) -> Option<&TraitKind> {
198 self.kind.as_ref()
199 }
200
201 #[must_use]
203 pub fn with_kind(mut self, kind: TraitKind) -> Self {
204 self.kind = Some(kind);
205 self
206 }
207}
208
209impl fmt::Display for TraitValue {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 write!(formatter, "{}: {}", self.name, self.value)
212 }
213}
214
215#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
217pub struct Phenotype {
218 traits: Vec<TraitValue>,
219}
220
221impl Phenotype {
222 #[must_use]
224 pub const fn new(traits: Vec<TraitValue>) -> Self {
225 Self { traits }
226 }
227
228 #[must_use]
230 pub fn traits(&self) -> &[TraitValue] {
231 &self.traits
232 }
233
234 #[must_use]
236 pub const fn len(&self) -> usize {
237 self.traits.len()
238 }
239
240 #[must_use]
242 pub const fn is_empty(&self) -> bool {
243 self.traits.is_empty()
244 }
245}
246
247impl From<Vec<TraitValue>> for Phenotype {
248 fn from(traits: Vec<TraitValue>) -> Self {
249 Self::new(traits)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::{Phenotype, TraitKind, TraitKindParseError, TraitName, TraitNameError, TraitValue};
256
257 #[test]
258 fn constructs_valid_trait_name() -> Result<(), TraitNameError> {
259 let name = TraitName::new("leaf shape")?;
260
261 assert_eq!(name.as_str(), "leaf shape");
262 assert_eq!(name.to_string(), "leaf shape");
263 Ok(())
264 }
265
266 #[test]
267 fn rejects_empty_trait_name() {
268 assert_eq!(TraitName::new(" "), Err(TraitNameError::Empty));
269 }
270
271 #[test]
272 fn displays_and_parses_trait_kind() -> Result<(), TraitKindParseError> {
273 assert_eq!(TraitKind::Morphological.to_string(), "morphological");
274 assert_eq!("behavioural".parse::<TraitKind>()?, TraitKind::Behavioral);
275 assert_eq!("genetic".parse::<TraitKind>()?, TraitKind::Genetic);
276 Ok(())
277 }
278
279 #[test]
280 fn parses_custom_trait_kind() -> Result<(), TraitKindParseError> {
281 assert_eq!(
282 "seasonal".parse::<TraitKind>()?,
283 TraitKind::Custom("seasonal".to_string())
284 );
285 assert_eq!("".parse::<TraitKind>(), Err(TraitKindParseError::Empty));
286 Ok(())
287 }
288
289 #[test]
290 fn constructs_phenotype() -> Result<(), TraitNameError> {
291 let value = TraitValue::new(TraitName::new("flower color")?, "white")?
292 .with_kind(TraitKind::Morphological);
293 let phenotype = Phenotype::new(vec![value.clone()]);
294
295 assert_eq!(value.name().as_str(), "flower color");
296 assert_eq!(value.value(), "white");
297 assert_eq!(value.kind(), Some(&TraitKind::Morphological));
298 assert_eq!(value.to_string(), "flower color: white");
299 assert_eq!(phenotype.len(), 1);
300 assert_eq!(phenotype.traits(), &[value]);
301 Ok(())
302 }
303}