Skip to main content

use_food_web/
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, FoodWebTextError> {
12    let trimmed = value.as_ref().trim();
13
14    if trimmed.is_empty() {
15        Err(FoodWebTextError::Empty)
16    } else {
17        Ok(trimmed.to_string())
18    }
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum FoodWebTextError {
23    Empty,
24}
25
26impl fmt::Display for FoodWebTextError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Empty => formatter.write_str("food web text cannot be empty"),
30        }
31    }
32}
33
34impl Error for FoodWebTextError {}
35
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct FoodWebName(String);
38
39impl FoodWebName {
40    /// # Errors
41    /// Returns `FoodWebTextError::Empty` when `value` is blank.
42    pub fn new(value: impl AsRef<str>) -> Result<Self, FoodWebTextError> {
43        non_empty_text(value).map(Self)
44    }
45
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl fmt::Display for FoodWebName {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        formatter.write_str(self.as_str())
55    }
56}
57
58impl FromStr for FoodWebName {
59    type Err = FoodWebTextError;
60
61    fn from_str(value: &str) -> Result<Self, Self::Err> {
62        Self::new(value)
63    }
64}
65
66#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
67pub enum FeedingRelation {
68    Consumes,
69    PreysOn,
70    Grazes,
71    Parasitizes,
72    Scavenges,
73    Decomposes,
74    Unknown,
75    Custom(String),
76}
77
78impl fmt::Display for FeedingRelation {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(match self {
81            Self::Consumes => "consumes",
82            Self::PreysOn => "preys-on",
83            Self::Grazes => "grazes",
84            Self::Parasitizes => "parasitizes",
85            Self::Scavenges => "scavenges",
86            Self::Decomposes => "decomposes",
87            Self::Unknown => "unknown",
88            Self::Custom(value) => value.as_str(),
89        })
90    }
91}
92
93impl FromStr for FeedingRelation {
94    type Err = FeedingRelationParseError;
95
96    fn from_str(value: &str) -> Result<Self, Self::Err> {
97        let trimmed = value.trim();
98
99        if trimmed.is_empty() {
100            return Err(FeedingRelationParseError::Empty);
101        }
102
103        Ok(match normalized_token(trimmed).as_str() {
104            "consumes" => Self::Consumes,
105            "preys-on" => Self::PreysOn,
106            "grazes" => Self::Grazes,
107            "parasitizes" => Self::Parasitizes,
108            "scavenges" => Self::Scavenges,
109            "decomposes" => Self::Decomposes,
110            "unknown" => Self::Unknown,
111            _ => Self::Custom(trimmed.to_string()),
112        })
113    }
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum FeedingRelationParseError {
118    Empty,
119}
120
121impl fmt::Display for FeedingRelationParseError {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            Self::Empty => formatter.write_str("feeding relation cannot be empty"),
125        }
126    }
127}
128
129impl Error for FeedingRelationParseError {}
130
131#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub enum EnergyFlowDirection {
133    ProducerToConsumer,
134    PreyToPredator,
135    DetritusToDecomposer,
136    Unknown,
137    Custom(String),
138}
139
140impl fmt::Display for EnergyFlowDirection {
141    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
142        formatter.write_str(match self {
143            Self::ProducerToConsumer => "producer-to-consumer",
144            Self::PreyToPredator => "prey-to-predator",
145            Self::DetritusToDecomposer => "detritus-to-decomposer",
146            Self::Unknown => "unknown",
147            Self::Custom(value) => value.as_str(),
148        })
149    }
150}
151
152impl FromStr for EnergyFlowDirection {
153    type Err = EnergyFlowDirectionParseError;
154
155    fn from_str(value: &str) -> Result<Self, Self::Err> {
156        let trimmed = value.trim();
157
158        if trimmed.is_empty() {
159            return Err(EnergyFlowDirectionParseError::Empty);
160        }
161
162        Ok(match normalized_token(trimmed).as_str() {
163            "producer-to-consumer" => Self::ProducerToConsumer,
164            "prey-to-predator" => Self::PreyToPredator,
165            "detritus-to-decomposer" => Self::DetritusToDecomposer,
166            "unknown" => Self::Unknown,
167            _ => Self::Custom(trimmed.to_string()),
168        })
169    }
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum EnergyFlowDirectionParseError {
174    Empty,
175}
176
177impl fmt::Display for EnergyFlowDirectionParseError {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::Empty => formatter.write_str("energy flow direction cannot be empty"),
181        }
182    }
183}
184
185impl Error for EnergyFlowDirectionParseError {}
186
187#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub struct FoodWebLink {
189    source: String,
190    target: String,
191    relation: FeedingRelation,
192}
193
194impl FoodWebLink {
195    /// # Errors
196    /// Returns `FoodWebTextError::Empty` when `source` or `target` is blank.
197    pub fn new(
198        source: impl AsRef<str>,
199        target: impl AsRef<str>,
200        relation: FeedingRelation,
201    ) -> Result<Self, FoodWebTextError> {
202        Ok(Self {
203            source: non_empty_text(source)?,
204            target: non_empty_text(target)?,
205            relation,
206        })
207    }
208
209    #[must_use]
210    pub fn source(&self) -> &str {
211        &self.source
212    }
213
214    #[must_use]
215    pub fn target(&self) -> &str {
216        &self.target
217    }
218
219    #[must_use]
220    pub const fn relation(&self) -> &FeedingRelation {
221        &self.relation
222    }
223}
224
225impl fmt::Display for FoodWebLink {
226    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(
228            formatter,
229            "{} -[{}]-> {}",
230            self.source, self.relation, self.target
231        )
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::{EnergyFlowDirection, FeedingRelation, FoodWebLink, FoodWebName, FoodWebTextError};
238
239    #[test]
240    fn valid_food_web_name() -> Result<(), FoodWebTextError> {
241        let name = FoodWebName::new("shelf web")?;
242
243        assert_eq!(name.as_str(), "shelf web");
244        Ok(())
245    }
246
247    #[test]
248    fn empty_food_web_name_rejected() {
249        assert_eq!(FoodWebName::new(""), Err(FoodWebTextError::Empty));
250    }
251
252    #[test]
253    fn feeding_relation_display_parse() {
254        assert_eq!(
255            "consumes".parse::<FeedingRelation>(),
256            Ok(FeedingRelation::Consumes)
257        );
258        assert_eq!(FeedingRelation::PreysOn.to_string(), "preys-on");
259    }
260
261    #[test]
262    fn energy_flow_direction_display_parse() {
263        assert_eq!(
264            "prey-to-predator".parse::<EnergyFlowDirection>(),
265            Ok(EnergyFlowDirection::PreyToPredator)
266        );
267        assert_eq!(
268            EnergyFlowDirection::DetritusToDecomposer.to_string(),
269            "detritus-to-decomposer"
270        );
271    }
272
273    #[test]
274    fn food_web_link_construction() -> Result<(), FoodWebTextError> {
275        let link = FoodWebLink::new("zooplankton", "anchovy", FeedingRelation::Consumes)?;
276
277        assert_eq!(link.source(), "zooplankton");
278        assert_eq!(link.target(), "anchovy");
279        assert_eq!(link.to_string(), "zooplankton -[consumes]-> anchovy");
280        Ok(())
281    }
282}