#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
fn normalized_key(value: &str) -> String {
value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
}
fn non_empty_text(value: impl AsRef<str>) -> Result<String, TraitNameError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(TraitNameError::Empty)
} else {
Ok(trimmed.to_string())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TraitNameError {
Empty,
}
impl fmt::Display for TraitNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("trait text cannot be empty"),
}
}
}
impl Error for TraitNameError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TraitName(String);
impl TraitName {
pub fn new(value: impl AsRef<str>) -> Result<Self, TraitNameError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for TraitName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for TraitName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for TraitName {
type Err = TraitNameError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TraitKind {
Morphological,
Physiological,
Behavioral,
Genetic,
Developmental,
Ecological,
Unknown,
Custom(String),
}
impl fmt::Display for TraitKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Morphological => formatter.write_str("morphological"),
Self::Physiological => formatter.write_str("physiological"),
Self::Behavioral => formatter.write_str("behavioral"),
Self::Genetic => formatter.write_str("genetic"),
Self::Developmental => formatter.write_str("developmental"),
Self::Ecological => formatter.write_str("ecological"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
impl FromStr for TraitKind {
type Err = TraitKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(TraitKindParseError::Empty);
}
match normalized_key(trimmed).as_str() {
"morphological" => Ok(Self::Morphological),
"physiological" => Ok(Self::Physiological),
"behavioral" | "behavioural" => Ok(Self::Behavioral),
"genetic" => Ok(Self::Genetic),
"developmental" => Ok(Self::Developmental),
"ecological" => Ok(Self::Ecological),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TraitKindParseError {
Empty,
}
impl fmt::Display for TraitKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("trait kind cannot be empty"),
}
}
}
impl Error for TraitKindParseError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TraitValue {
name: TraitName,
value: String,
kind: Option<TraitKind>,
}
impl TraitValue {
pub fn new(name: TraitName, value: impl AsRef<str>) -> Result<Self, TraitNameError> {
Ok(Self {
name,
value: non_empty_text(value)?,
kind: None,
})
}
#[must_use]
pub const fn name(&self) -> &TraitName {
&self.name
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub const fn kind(&self) -> Option<&TraitKind> {
self.kind.as_ref()
}
#[must_use]
pub fn with_kind(mut self, kind: TraitKind) -> Self {
self.kind = Some(kind);
self
}
}
impl fmt::Display for TraitValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.name, self.value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Phenotype {
traits: Vec<TraitValue>,
}
impl Phenotype {
#[must_use]
pub const fn new(traits: Vec<TraitValue>) -> Self {
Self { traits }
}
#[must_use]
pub fn traits(&self) -> &[TraitValue] {
&self.traits
}
#[must_use]
pub const fn len(&self) -> usize {
self.traits.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.traits.is_empty()
}
}
impl From<Vec<TraitValue>> for Phenotype {
fn from(traits: Vec<TraitValue>) -> Self {
Self::new(traits)
}
}
#[cfg(test)]
mod tests {
use super::{Phenotype, TraitKind, TraitKindParseError, TraitName, TraitNameError, TraitValue};
#[test]
fn constructs_valid_trait_name() -> Result<(), TraitNameError> {
let name = TraitName::new("leaf shape")?;
assert_eq!(name.as_str(), "leaf shape");
assert_eq!(name.to_string(), "leaf shape");
Ok(())
}
#[test]
fn rejects_empty_trait_name() {
assert_eq!(TraitName::new(" "), Err(TraitNameError::Empty));
}
#[test]
fn displays_and_parses_trait_kind() -> Result<(), TraitKindParseError> {
assert_eq!(TraitKind::Morphological.to_string(), "morphological");
assert_eq!("behavioural".parse::<TraitKind>()?, TraitKind::Behavioral);
assert_eq!("genetic".parse::<TraitKind>()?, TraitKind::Genetic);
Ok(())
}
#[test]
fn parses_custom_trait_kind() -> Result<(), TraitKindParseError> {
assert_eq!(
"seasonal".parse::<TraitKind>()?,
TraitKind::Custom("seasonal".to_string())
);
assert_eq!("".parse::<TraitKind>(), Err(TraitKindParseError::Empty));
Ok(())
}
#[test]
fn constructs_phenotype() -> Result<(), TraitNameError> {
let value = TraitValue::new(TraitName::new("flower color")?, "white")?
.with_kind(TraitKind::Morphological);
let phenotype = Phenotype::new(vec![value.clone()]);
assert_eq!(value.name().as_str(), "flower color");
assert_eq!(value.value(), "white");
assert_eq!(value.kind(), Some(&TraitKind::Morphological));
assert_eq!(value.to_string(), "flower color: white");
assert_eq!(phenotype.len(), 1);
assert_eq!(phenotype.traits(), &[value]);
Ok(())
}
}