Skip to main content

use_species_interaction/
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_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    /// # Errors
176    /// Returns `InteractionTextError::Empty` when `first` or `second` is blank.
177    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}