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, Copy, Debug, Eq, PartialEq)]
13pub enum TissueNameError {
14 Empty,
16}
17
18impl fmt::Display for TissueNameError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("tissue name cannot be empty"),
22 }
23 }
24}
25
26impl Error for TissueNameError {}
27
28#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub struct TissueName(String);
31
32impl TissueName {
33 pub fn new(value: impl AsRef<str>) -> Result<Self, TissueNameError> {
39 let trimmed = value.as_ref().trim();
40
41 if trimmed.is_empty() {
42 Err(TissueNameError::Empty)
43 } else {
44 Ok(Self(trimmed.to_string()))
45 }
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53
54 #[must_use]
56 pub fn into_string(self) -> String {
57 self.0
58 }
59}
60
61impl AsRef<str> for TissueName {
62 fn as_ref(&self) -> &str {
63 self.as_str()
64 }
65}
66
67impl fmt::Display for TissueName {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 formatter.write_str(self.as_str())
70 }
71}
72
73impl FromStr for TissueName {
74 type Err = TissueNameError;
75
76 fn from_str(value: &str) -> Result<Self, Self::Err> {
77 Self::new(value)
78 }
79}
80
81#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub enum TissueKind {
84 Epithelial,
86 Connective,
88 Muscle,
90 Nervous,
92 Vascular,
94 Dermal,
96 Ground,
98 Meristematic,
100 Unknown,
102 Custom(String),
104}
105
106impl fmt::Display for TissueKind {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::Epithelial => formatter.write_str("epithelial"),
110 Self::Connective => formatter.write_str("connective"),
111 Self::Muscle => formatter.write_str("muscle"),
112 Self::Nervous => formatter.write_str("nervous"),
113 Self::Vascular => formatter.write_str("vascular"),
114 Self::Dermal => formatter.write_str("dermal"),
115 Self::Ground => formatter.write_str("ground"),
116 Self::Meristematic => formatter.write_str("meristematic"),
117 Self::Unknown => formatter.write_str("unknown"),
118 Self::Custom(value) => formatter.write_str(value),
119 }
120 }
121}
122
123impl FromStr for TissueKind {
124 type Err = TissueKindParseError;
125
126 fn from_str(value: &str) -> Result<Self, Self::Err> {
127 let trimmed = value.trim();
128
129 if trimmed.is_empty() {
130 return Err(TissueKindParseError::Empty);
131 }
132
133 match normalized_key(trimmed).as_str() {
134 "epithelial" => Ok(Self::Epithelial),
135 "connective" => Ok(Self::Connective),
136 "muscle" => Ok(Self::Muscle),
137 "nervous" => Ok(Self::Nervous),
138 "vascular" => Ok(Self::Vascular),
139 "dermal" => Ok(Self::Dermal),
140 "ground" => Ok(Self::Ground),
141 "meristematic" => Ok(Self::Meristematic),
142 "unknown" => Ok(Self::Unknown),
143 _ => Ok(Self::Custom(trimmed.to_string())),
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum TissueKindParseError {
151 Empty,
153}
154
155impl fmt::Display for TissueKindParseError {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Self::Empty => formatter.write_str("tissue kind cannot be empty"),
159 }
160 }
161}
162
163impl Error for TissueKindParseError {}
164
165#[cfg(test)]
166mod tests {
167 use super::{TissueKind, TissueKindParseError, TissueName, TissueNameError};
168
169 #[test]
170 fn constructs_valid_tissue_name() -> Result<(), TissueNameError> {
171 let name = TissueName::new("xylem")?;
172
173 assert_eq!(name.as_str(), "xylem");
174 Ok(())
175 }
176
177 #[test]
178 fn rejects_empty_tissue_name() {
179 assert_eq!(TissueName::new(" "), Err(TissueNameError::Empty));
180 }
181
182 #[test]
183 fn displays_and_parses_tissue_kind() -> Result<(), TissueKindParseError> {
184 assert_eq!(TissueKind::Epithelial.to_string(), "epithelial");
185 assert_eq!("ground".parse::<TissueKind>()?, TissueKind::Ground);
186 assert_eq!(
187 "meristematic".parse::<TissueKind>()?,
188 TissueKind::Meristematic
189 );
190 Ok(())
191 }
192
193 #[test]
194 fn parses_custom_tissue_kind() -> Result<(), TissueKindParseError> {
195 assert_eq!(
196 "cartilaginous".parse::<TissueKind>()?,
197 TissueKind::Custom("cartilaginous".to_string())
198 );
199 assert_eq!("".parse::<TissueKind>(), Err(TissueKindParseError::Empty));
200 Ok(())
201 }
202}