Skip to main content

use_trait/
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
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/// Error returned when trait labels are empty.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum TraitNameError {
24    /// The supplied value was empty after trimming surrounding whitespace.
25    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/// A non-empty biological trait name.
39#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct TraitName(String);
41
42impl TraitName {
43    /// Creates a trait name from non-empty text.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`TraitNameError::Empty`] when the trimmed name is empty.
48    pub fn new(value: impl AsRef<str>) -> Result<Self, TraitNameError> {
49        non_empty_text(value).map(Self)
50    }
51
52    /// Returns the trait name text.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57
58    /// Consumes the name and returns the owned string.
59    #[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/// Broad biological trait families.
86#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub enum TraitKind {
88    /// Shape, size, color, and other structural traits.
89    Morphological,
90    /// Functional traits.
91    Physiological,
92    /// Behavior-associated traits.
93    Behavioral,
94    /// Genetic trait labels.
95    Genetic,
96    /// Developmental traits.
97    Developmental,
98    /// Ecological trait labels.
99    Ecological,
100    /// Unknown trait kind.
101    Unknown,
102    /// Caller-defined trait kind text.
103    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/// Error returned when parsing trait kinds fails.
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub enum TraitKindParseError {
147    /// The trait kind was empty after trimming whitespace.
148    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/// A descriptive trait value.
162#[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    /// Creates a trait value from a trait name and non-empty descriptive value.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`TraitNameError::Empty`] when the trimmed value is empty.
175    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    /// Returns the trait name.
184    #[must_use]
185    pub const fn name(&self) -> &TraitName {
186        &self.name
187    }
188
189    /// Returns the descriptive trait value.
190    #[must_use]
191    pub fn value(&self) -> &str {
192        &self.value
193    }
194
195    /// Returns the optional trait kind.
196    #[must_use]
197    pub const fn kind(&self) -> Option<&TraitKind> {
198        self.kind.as_ref()
199    }
200
201    /// Adds a trait kind.
202    #[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/// An ordered phenotype description.
216#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
217pub struct Phenotype {
218    traits: Vec<TraitValue>,
219}
220
221impl Phenotype {
222    /// Creates a phenotype from caller-supplied trait order.
223    #[must_use]
224    pub const fn new(traits: Vec<TraitValue>) -> Self {
225        Self { traits }
226    }
227
228    /// Returns trait values in caller-supplied order.
229    #[must_use]
230    pub fn traits(&self) -> &[TraitValue] {
231        &self.traits
232    }
233
234    /// Returns the trait count.
235    #[must_use]
236    pub const fn len(&self) -> usize {
237        self.traits.len()
238    }
239
240    /// Returns `true` when no trait values are present.
241    #[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}