1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_token(value: &str) -> String {
8 value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11fn non_empty_text(value: impl AsRef<str>) -> Result<String, InteractionTextError> {
12 let trimmed = value.as_ref().trim();
13
14 if trimmed.is_empty() {
15 Err(InteractionTextError::Empty)
16 } else {
17 Ok(trimmed.to_string())
18 }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum InteractionTextError {
23 Empty,
24}
25
26impl fmt::Display for InteractionTextError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Self::Empty => formatter.write_str("interaction text cannot be empty"),
30 }
31 }
32}
33
34impl Error for InteractionTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub enum SpeciesInteractionKind {
38 Predation,
39 Competition,
40 Mutualism,
41 Commensalism,
42 Parasitism,
43 Amensalism,
44 Herbivory,
45 Symbiosis,
46 Neutralism,
47 Unknown,
48 Custom(String),
49}
50
51impl fmt::Display for SpeciesInteractionKind {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str(match self {
54 Self::Predation => "predation",
55 Self::Competition => "competition",
56 Self::Mutualism => "mutualism",
57 Self::Commensalism => "commensalism",
58 Self::Parasitism => "parasitism",
59 Self::Amensalism => "amensalism",
60 Self::Herbivory => "herbivory",
61 Self::Symbiosis => "symbiosis",
62 Self::Neutralism => "neutralism",
63 Self::Unknown => "unknown",
64 Self::Custom(value) => value.as_str(),
65 })
66 }
67}
68
69impl FromStr for SpeciesInteractionKind {
70 type Err = SpeciesInteractionKindParseError;
71
72 fn from_str(value: &str) -> Result<Self, Self::Err> {
73 let trimmed = value.trim();
74
75 if trimmed.is_empty() {
76 return Err(SpeciesInteractionKindParseError::Empty);
77 }
78
79 Ok(match normalized_token(trimmed).as_str() {
80 "predation" => Self::Predation,
81 "competition" => Self::Competition,
82 "mutualism" => Self::Mutualism,
83 "commensalism" => Self::Commensalism,
84 "parasitism" => Self::Parasitism,
85 "amensalism" => Self::Amensalism,
86 "herbivory" => Self::Herbivory,
87 "symbiosis" => Self::Symbiosis,
88 "neutralism" => Self::Neutralism,
89 "unknown" => Self::Unknown,
90 _ => Self::Custom(trimmed.to_string()),
91 })
92 }
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum SpeciesInteractionKindParseError {
97 Empty,
98}
99
100impl fmt::Display for SpeciesInteractionKindParseError {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::Empty => formatter.write_str("species interaction kind cannot be empty"),
104 }
105 }
106}
107
108impl Error for SpeciesInteractionKindParseError {}
109
110#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum InteractionStrength {
112 Weak,
113 Moderate,
114 Strong,
115 Unknown,
116 Custom(String),
117}
118
119impl fmt::Display for InteractionStrength {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 formatter.write_str(match self {
122 Self::Weak => "weak",
123 Self::Moderate => "moderate",
124 Self::Strong => "strong",
125 Self::Unknown => "unknown",
126 Self::Custom(value) => value.as_str(),
127 })
128 }
129}
130
131impl FromStr for InteractionStrength {
132 type Err = InteractionStrengthParseError;
133
134 fn from_str(value: &str) -> Result<Self, Self::Err> {
135 let trimmed = value.trim();
136
137 if trimmed.is_empty() {
138 return Err(InteractionStrengthParseError::Empty);
139 }
140
141 Ok(match normalized_token(trimmed).as_str() {
142 "weak" => Self::Weak,
143 "moderate" => Self::Moderate,
144 "strong" => Self::Strong,
145 "unknown" => Self::Unknown,
146 _ => Self::Custom(trimmed.to_string()),
147 })
148 }
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum InteractionStrengthParseError {
153 Empty,
154}
155
156impl fmt::Display for InteractionStrengthParseError {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 Self::Empty => formatter.write_str("interaction strength cannot be empty"),
160 }
161 }
162}
163
164impl Error for InteractionStrengthParseError {}
165
166#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct SpeciesInteraction {
168 first: String,
169 second: String,
170 kind: SpeciesInteractionKind,
171 strength: Option<InteractionStrength>,
172}
173
174impl SpeciesInteraction {
175 pub fn new(
178 first: impl AsRef<str>,
179 second: impl AsRef<str>,
180 kind: SpeciesInteractionKind,
181 ) -> Result<Self, InteractionTextError> {
182 Ok(Self {
183 first: non_empty_text(first)?,
184 second: non_empty_text(second)?,
185 kind,
186 strength: None,
187 })
188 }
189
190 #[must_use]
191 pub fn with_strength(mut self, strength: InteractionStrength) -> Self {
192 self.strength = Some(strength);
193 self
194 }
195
196 #[must_use]
197 pub fn first(&self) -> &str {
198 &self.first
199 }
200
201 #[must_use]
202 pub fn second(&self) -> &str {
203 &self.second
204 }
205
206 #[must_use]
207 pub const fn kind(&self) -> &SpeciesInteractionKind {
208 &self.kind
209 }
210
211 #[must_use]
212 pub const fn strength(&self) -> Option<&InteractionStrength> {
213 self.strength.as_ref()
214 }
215}
216
217impl fmt::Display for SpeciesInteraction {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 write!(
220 formatter,
221 "{} -[{}]-> {}",
222 self.first, self.kind, self.second
223 )
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::{
230 InteractionStrength, InteractionTextError, SpeciesInteraction, SpeciesInteractionKind,
231 };
232
233 #[test]
234 fn interaction_kind_display_parse() {
235 assert_eq!(
236 "mutualism".parse::<SpeciesInteractionKind>(),
237 Ok(SpeciesInteractionKind::Mutualism)
238 );
239 assert_eq!(SpeciesInteractionKind::Predation.to_string(), "predation");
240 }
241
242 #[test]
243 fn custom_interaction_kind() {
244 assert_eq!(
245 "facilitation".parse::<SpeciesInteractionKind>(),
246 Ok(SpeciesInteractionKind::Custom("facilitation".to_string()))
247 );
248 }
249
250 #[test]
251 fn interaction_strength_display_parse() {
252 assert_eq!(
253 "strong".parse::<InteractionStrength>(),
254 Ok(InteractionStrength::Strong)
255 );
256 assert_eq!(InteractionStrength::Moderate.to_string(), "moderate");
257 }
258
259 #[test]
260 fn species_interaction_construction() -> Result<(), InteractionTextError> {
261 let interaction =
262 SpeciesInteraction::new("coral", "algae", SpeciesInteractionKind::Mutualism)?
263 .with_strength(InteractionStrength::Strong);
264
265 assert_eq!(interaction.first(), "coral");
266 assert_eq!(interaction.second(), "algae");
267 assert_eq!(interaction.kind(), &SpeciesInteractionKind::Mutualism);
268 assert_eq!(interaction.strength(), Some(&InteractionStrength::Strong));
269 Ok(())
270 }
271}